melq
Version:
Quantum-secure chat network with ML-KEM-768 encryption and host-based architecture
1,227 lines (1,028 loc) ⢠45.8 kB
JavaScript
import { Command } from 'commander';
import { UnifiedNode } from './network/unified-node.js';
import { CLIInterface } from './cli/interface.js';
import chalk from 'chalk';
import readline from 'readline';
import { promises as fs } from 'fs';
import { execSync } from 'child_process';
import path from 'path';
import { fileURLToPath } from 'url';
import os from 'os';
import { spawn } from 'child_process';
const program = new Command();
program
.name('melq')
.description('Secure P2P chat - Host or join networks easily')
.version('1.0.0')
.option('-j, --join <code>', 'Join network with connection code')
.option('-h, --host [port]', 'Host a new network (optional port)')
.option('--internet', 'Expose hosted network to internet (with --host)')
.option('--local-only', 'Host network for local access only (with --host)')
.option('--password <password>', 'Set password for session (with --host)')
.option('--tunnel <method>', 'Specify tunneling method: ngrok, localtunnel, serveo, manual')
.option('--update', 'Update MELQ to the latest version from npm registry')
.option('--check-updates', 'Check if updates are available without installing')
.option('--update-auto <mode>', 'Enable/disable automatic updates (on/off)')
.option('--skip-update-check', 'Skip automatic update check on startup')
.action(async (options) => {
if (options.update) {
await handleUpdateCommand();
return;
}
if (options.checkUpdates) {
await handleCheckUpdatesCommand();
return;
}
if (options.updateAuto) {
await handleUpdateAutoCommand(options.updateAuto);
return;
}
// Check for automatic updates (unless skipped)
if (!options.skipUpdateCheck) {
await checkForAutoUpdates();
}
console.log(chalk.blue('š MELQ - Quantum-Secure P2P Chat'));
console.log(chalk.gray('ā'.repeat(50)));
try {
const node = new UnifiedNode();
if (options.join) {
await startClientMode(node, options.join);
} else if (options.host) {
const port = parseInt(options.host) || 0;
const hostOptions = {
exposeToInternet: options.internet || (!options.localOnly && !process.stdout.isTTY),
tunnelMethod: options.tunnel || 'auto',
password: options.password || undefined
};
// Skip interactive mode if options are provided via CLI
if (options.internet || options.localOnly || options.tunnel || options.password) {
await startHostModeWithOptions(node, port, hostOptions);
} else {
await startHostMode(node, port);
}
} else {
await showInteractiveMenu(node);
}
} catch (error) {
console.error(chalk.red('Failed to start MELQ:'), error.message);
process.exit(1);
}
});
async function showInteractiveMenu(node) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log(chalk.yellow('\nWhat would you like to do?'));
console.log(chalk.cyan('1. š Host a new network (others can join you)'));
console.log(chalk.cyan('2. š Join an existing network'));
console.log(chalk.cyan('3. š Discover local networks'));
console.log(chalk.cyan('4. ā Help'));
const answer = await new Promise(resolve => {
rl.question(chalk.green('\nChoose an option (1-4): '), resolve);
});
rl.close();
switch (answer.trim()) {
case '1':
await startHostMode(node);
break;
case '2':
await promptForConnectionCode(node);
break;
case '3':
await discoverLocalNetworks(node);
break;
case '4':
showHelp();
break;
default:
console.log(chalk.red('Invalid option. Please try again.'));
await showInteractiveMenu(node);
}
}
async function startHostMode(node, port = 0) {
console.log(chalk.yellow('\nš Setting up network host...'));
// Ask about internet exposure
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log(chalk.yellow('\nNetwork Access Options:'));
console.log(chalk.cyan('1. š Local network only (same WiFi/LAN)'));
console.log(chalk.cyan('2. š Local + Internet (anyone can join)'));
console.log(chalk.cyan('3. š Password-protected session'));
const accessChoice = await new Promise(resolve => {
rl.question(chalk.green('\nChoose access level (1-3): '), resolve);
});
let options = { exposeToInternet: false };
if (accessChoice.trim() === '2') {
options.exposeToInternet = true;
// Show tunneling method options for Local + Internet
console.log(chalk.yellow('\nTunneling Method:'));
console.log(chalk.cyan('1. Auto (prefer localtunnel - instant, no signup!)'));
console.log(chalk.cyan('2. Localtunnel only (recommended - no account needed)'));
console.log(chalk.cyan('3. ngrok only (requires account for persistent URLs)'));
console.log(chalk.cyan('4. Manual setup (port forwarding)'));
const methodChoice = await new Promise(resolve => {
rl.question(chalk.green('Choose method (1-4): '), resolve);
});
switch (methodChoice.trim()) {
case '2':
options.tunnelMethod = 'localtunnel';
break;
case '3':
options.tunnelMethod = 'ngrok';
break;
case '4':
options.tunnelMethod = 'manual';
const customDomain = await new Promise(resolve => {
rl.question(chalk.green('Custom domain/IP (optional): '), resolve);
});
if (customDomain.trim()) {
options.customDomain = customDomain.trim();
}
break;
default:
options.tunnelMethod = 'auto';
}
} else if (accessChoice.trim() === '3') {
// Password-protected session
console.log(chalk.yellow('\nš Password-Protected Session Setup'));
console.log(chalk.gray('Set a password that others will need to join your session.'));
const password = await new Promise(resolve => {
rl.question(chalk.green('Enter session password: '), resolve);
});
if (!password.trim()) {
console.log(chalk.red('Password cannot be empty. Falling back to no password.'));
} else {
options.password = password.trim();
console.log(chalk.green('ā Password set! Only users with this password can join.'));
}
// Ask about internet exposure for password-protected sessions
const internetChoice = await new Promise(resolve => {
rl.question(chalk.green('Also expose to internet? (Y/n): '), resolve);
});
if (!internetChoice.toLowerCase().startsWith('n')) {
options.exposeToInternet = true;
console.log(chalk.yellow('\nTunneling Method:'));
console.log(chalk.cyan('1. Auto (prefer localtunnel - instant, no signup!)'));
console.log(chalk.cyan('2. Localtunnel only (recommended - no account needed)'));
console.log(chalk.cyan('3. ngrok only (requires account for persistent URLs)'));
console.log(chalk.cyan('4. Manual setup (port forwarding)'));
const methodChoice = await new Promise(resolve => {
rl.question(chalk.green('Choose method (1-4): '), resolve);
});
switch (methodChoice.trim()) {
case '2':
options.tunnelMethod = 'localtunnel';
break;
case '3':
options.tunnelMethod = 'ngrok';
break;
case '4':
options.tunnelMethod = 'manual';
const customDomain = await new Promise(resolve => {
rl.question(chalk.green('Custom domain/IP (optional): '), resolve);
});
if (customDomain.trim()) {
options.customDomain = customDomain.trim();
}
break;
default:
options.tunnelMethod = 'auto';
}
}
}
rl.close();
// Check for existing MELQ nodes
console.log(chalk.yellow('\nš Checking for existing MELQ nodes...'));
const existingNodes = await node.checkExistingNodes();
if (existingNodes.length > 0) {
console.log(chalk.blue(`\nš” Found ${existingNodes.length} existing MELQ node(s):`));
existingNodes.forEach((nodeInfo, index) => {
console.log(chalk.cyan(` ${index + 1}. Node ${nodeInfo.nodeId.slice(-8)} on port ${nodeInfo.port} (${nodeInfo.nodes} peers, ${nodeInfo.chats} chats)`));
});
console.log(chalk.dim.gray('\nš” Your new node will use a different port automatically.'));
}
console.log(chalk.yellow('\nš ļø Starting network...'));
const networkInfo = await node.startAsHost(port, options);
console.log(chalk.green('\nā
Network successfully hosted!'));
// Show connection codes
console.log(chalk.blue('\nš Connection Codes:'));
if (options.password) {
console.log(chalk.yellow('š Password-Protected Session'));
}
console.log(chalk.cyan('š Local (same network):'));
console.log(chalk.bgCyan.black(` ${networkInfo.localConnectionCode} `));
if (networkInfo.hasInternet) {
console.log(chalk.cyan('\nš Internet (anywhere):'));
console.log(chalk.bgGreen.black(` ${networkInfo.internetConnectionCode} `));
}
if (options.password) {
console.log(chalk.red('\nā ļø Password required to join this session'));
console.log(chalk.gray('Users will be prompted for password when connecting'));
}
console.log(chalk.gray('\nShare these codes with others so they can join!'));
console.log(chalk.gray('Command: melq --join <connection_code>'));
// Start host monitoring interface and auto-connect client
await startHostWithAutoClient(node, networkInfo);
}
async function startHostModeWithOptions(node, port = 0, options = {}) {
console.log(chalk.yellow('\nš„ļø Starting network with specified options...'));
// Check for existing MELQ nodes
console.log(chalk.yellow('\nš Checking for existing MELQ nodes...'));
const existingNodes = await node.checkExistingNodes();
if (existingNodes.length > 0) {
console.log(chalk.blue(`\nš” Found ${existingNodes.length} existing MELQ node(s):`));
existingNodes.forEach((nodeInfo, index) => {
console.log(chalk.cyan(` ${index + 1}. Node ${nodeInfo.nodeId.slice(-8)} on port ${nodeInfo.port} (${nodeInfo.nodes} peers, ${nodeInfo.chats} chats)`));
});
console.log(chalk.dim.gray('\nš” Your new node will use a different port automatically.'));
}
const networkInfo = await node.startAsHost(port, options);
console.log(chalk.green('\nā
Network successfully hosted!'));
// Show connection codes
console.log(chalk.blue('\nš Connection Codes:'));
if (options.password) {
console.log(chalk.yellow('š Password-Protected Session'));
}
console.log(chalk.cyan('š Local (same network):'));
console.log(chalk.bgCyan.black(` ${networkInfo.localConnectionCode} `));
if (networkInfo.hasInternet) {
console.log(chalk.cyan('\nš Internet (anywhere):'));
console.log(chalk.bgGreen.black(` ${networkInfo.internetConnectionCode} `));
}
if (options.password) {
console.log(chalk.red('\nā ļø Password required to join this session'));
console.log(chalk.gray('Users will be prompted for password when connecting'));
}
console.log(chalk.gray('\nShare these codes with others so they can join!'));
console.log(chalk.gray('Command: melq --join <connection_code>'));
// Start host monitoring interface and auto-connect client
await startHostWithAutoClient(node, networkInfo);
}
async function startClientMode(node, connectionCode, isInteractiveMode = false) {
console.log(chalk.yellow(`\nš Joining network: ${connectionCode}`));
try {
await node.joinNetwork(connectionCode);
console.log(chalk.green('\nā
Successfully joined network!'));
// Determine connection method for display
let method = 'remote';
let tunnelMethod = null;
if (connectionCode.includes('192.168.') || connectionCode.includes('localhost') || connectionCode.includes('127.0.0.1')) {
method = 'local';
} else if (connectionCode.includes('ngrok')) {
method = 'internet';
tunnelMethod = 'ngrok';
} else if (connectionCode.includes('loca.lt')) {
method = 'internet';
tunnelMethod = 'localtunnel';
} else if (connectionCode.includes('serveo.net')) {
method = 'internet';
tunnelMethod = 'serveo';
} else {
method = 'internet';
tunnelMethod = 'manual';
}
const connectionInfo = {
connectionCode,
method,
tunnelMethod
};
await startChatInterface(node, connectionInfo);
} catch (error) {
console.log(chalk.red('\nā Failed to join network'));
console.log(chalk.red('Error:'), error.message);
console.log(chalk.yellow('\nPossible issues:'));
console.log(chalk.gray('⢠Connection code is incorrect or malformed'));
console.log(chalk.gray('⢠Host network is not running'));
console.log(chalk.gray('⢠Network connectivity problems'));
console.log(chalk.gray('⢠Firewall blocking connection'));
if (isInteractiveMode) {
// Interactive mode: show recovery options
await showConnectionErrorRecovery(node, connectionCode);
} else {
// Command-line mode: show helpful message and exit gracefully
console.log(chalk.yellow('\nš” Suggestions:'));
console.log(chalk.gray('⢠Double-check the connection code'));
console.log(chalk.gray('⢠Try running: melq --join <different_code>'));
console.log(chalk.gray('⢠Or run: melq (for interactive menu)'));
console.log(chalk.gray('⢠Use: melq --help (for all options)'));
process.exit(1);
}
}
}
async function showConnectionErrorRecovery(node, failedConnectionCode) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log(chalk.yellow('\nš What would you like to do?'));
console.log(chalk.cyan('1. š Try a different connection code'));
console.log(chalk.cyan('2. š Scan for local networks'));
console.log(chalk.cyan('3. š Return to main menu'));
console.log(chalk.cyan('4. ā Exit MELQ'));
console.log(chalk.dim(`\nFailed code: ${failedConnectionCode}`));
const choice = await new Promise(resolve => {
rl.question(chalk.green('\nChoose an option (1-4): '), resolve);
});
rl.close();
switch (choice.trim()) {
case '1':
await promptForConnectionCode(node);
break;
case '2':
await discoverLocalNetworks(node);
break;
case '3':
await showInteractiveMenu(node);
break;
case '4':
console.log(chalk.gray('\nGoodbye! š'));
process.exit(0);
break;
default:
console.log(chalk.red('Invalid option. Please try again.'));
await showConnectionErrorRecovery(node, failedConnectionCode);
}
}
async function promptForConnectionCode(node) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log(chalk.yellow('\nš Join Network'));
console.log(chalk.gray('Enter the connection code shared by the host.'));
console.log(chalk.gray('Formats accepted:'));
console.log(chalk.gray(' melq://192.168.1.100:3000'));
console.log(chalk.gray(' https://abc123.ngrok.io'));
console.log(chalk.gray(' 192.168.1.100:3000\n'));
const connectionCode = await new Promise(resolve => {
rl.question(chalk.green('Connection code: '), resolve);
});
rl.close();
if (connectionCode.trim()) {
await startClientMode(node, connectionCode.trim(), true); // Interactive mode
} else {
console.log(chalk.red('No connection code provided.'));
await showInteractiveMenu(node);
}
}
async function discoverLocalNetworks(node) {
console.log(chalk.yellow('\nš Scanning for local MELQ networks...'));
try {
const networks = await node.discoverLocalNetworks();
if (networks.length === 0) {
console.log(chalk.gray('No local MELQ networks found.'));
console.log(chalk.gray('\nTry:'));
console.log(chalk.gray('⢠Make sure other nodes are running on this network'));
console.log(chalk.gray('⢠Check your firewall settings'));
console.log(chalk.gray('⢠Use connection codes for remote networks\n'));
await showInteractiveMenu(node);
return;
}
console.log(chalk.green(`\nFound ${networks.length} local network(s):`));
networks.forEach((network, index) => {
const age = Math.floor((Date.now() - network.timestamp) / 1000);
console.log(chalk.cyan(`${index + 1}. ${network.networkName}`));
console.log(chalk.gray(` Host: ${network.host}:${network.port} (${age}s ago)`));
});
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const choice = await new Promise(resolve => {
rl.question(chalk.green(`\nChoose network (1-${networks.length}) or 0 to go back: `), resolve);
});
rl.close();
const networkIndex = parseInt(choice.trim()) - 1;
if (choice.trim() === '0') {
await showInteractiveMenu(node);
} else if (networkIndex >= 0 && networkIndex < networks.length) {
const selectedNetwork = networks[networkIndex];
await startClientMode(node, selectedNetwork.connectionCode, true); // Interactive mode
} else {
console.log(chalk.red('Invalid selection.'));
await discoverLocalNetworks(node);
}
} catch (error) {
console.error(chalk.red('Discovery failed:'), error.message);
console.log(chalk.gray('\nFallback to manual connection codes.\n'));
await showInteractiveMenu(node);
}
}
async function startHostWithAutoClient(hostNode, networkInfo) {
console.log(chalk.blue('\nš Starting host monitoring interface...'));
// Setup cleanup handlers for the host
setupHostCleanup(hostNode);
// Start the host monitoring interface
startHostMonitoring(hostNode, networkInfo);
// Try to auto-connect as client
await attemptAutoClientConnection(networkInfo);
}
function setupHostCleanup(hostNode) {
let isShuttingDown = false;
let tunnelKeepaliveTimer = null;
// Cleanup function with tunnel keepalive
const cleanup = async (immediate = false) => {
if (isShuttingDown) return;
isShuttingDown = true;
console.log(chalk.yellow('\nš Shutting down host...'));
if (!immediate && hostNode && hostNode.tunneling && hostNode.tunneling.publicUrl) {
// Keep tunnel alive for 15 seconds to allow reconnection
console.log(chalk.cyan('š Keeping tunnel online for 15 seconds to allow reconnection...'));
tunnelKeepaliveTimer = setTimeout(async () => {
console.log(chalk.yellow('ā° Tunnel keepalive expired, tearing down...'));
await performFullCleanup();
}, 15000);
// Only disconnect the main server, keep tunnel running
if (hostNode) {
try {
await hostNode.disconnectServerOnly(true);
} catch (error) {
console.error(chalk.red('ā Server shutdown error:'), error.message);
await performFullCleanup();
}
}
} else {
// Immediate shutdown or no tunnel to keep alive
await performFullCleanup();
}
};
const performFullCleanup = async () => {
if (tunnelKeepaliveTimer) {
clearTimeout(tunnelKeepaliveTimer);
tunnelKeepaliveTimer = null;
}
console.log(chalk.yellow('š Performing full cleanup...'));
if (hostNode) {
try {
await hostNode.disconnect();
} catch (error) {
console.error(chalk.red('ā Cleanup error:'), error.message);
}
}
console.log(chalk.green('ā
Host cleanup completed'));
process.exit(0);
};
// Function to cancel keepalive if host reconnects quickly
const cancelKeepalive = () => {
if (tunnelKeepaliveTimer) {
clearTimeout(tunnelKeepaliveTimer);
tunnelKeepaliveTimer = null;
console.log(chalk.green('š Tunnel keepalive cancelled - host reconnected'));
isShuttingDown = false;
return true;
}
return false;
};
// Expose functions to the host node
if (hostNode) {
hostNode.cancelTunnelKeepalive = cancelKeepalive;
// Set up callback for unintentional disconnections
hostNode.gracefulCleanupCallback = () => cleanup(false);
}
// Handle various exit scenarios
process.on('SIGINT', () => {
// Immediate shutdown for intentional Ctrl+C
cleanup(true).catch((error) => {
console.error(chalk.red('ā Cleanup failed:'), error);
process.exit(1);
});
});
process.on('SIGTERM', () => {
// Immediate shutdown for intentional termination
cleanup(true).catch((error) => {
console.error(chalk.red('ā Cleanup failed:'), error);
process.exit(1);
});
});
process.on('exit', async () => {
if (hostNode) {
try {
// Note: exit event handlers can't be async, so we do our best
hostNode.disconnect();
} catch (error) {
console.error('Final cleanup error:', error.message);
}
}
});
// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
console.error(chalk.red('ā Uncaught exception:'), error);
cleanup().catch(() => process.exit(1));
});
process.on('unhandledRejection', (reason, promise) => {
console.error(chalk.red('ā Unhandled rejection at:'), promise, 'reason:', reason);
cleanup().catch(() => process.exit(1));
});
}
async function attemptAutoClientConnection(networkInfo) {
try {
console.log(chalk.cyan('\nš Auto-connecting as client to own hosted session...'));
// Create a new client node
const clientNode = new UnifiedNode();
// Determine which connection code to use (prefer local)
const connectionCode = networkInfo.localConnectionCode;
// Give the host a moment to fully initialize and finish all setup output
await new Promise(resolve => setTimeout(resolve, 3000));
// Connect as client (pass host auto-client flag)
await clientNode.joinNetwork(connectionCode, true);
console.log(chalk.green('ā
Successfully auto-connected as client!'));
// Give additional time for any remaining output to settle
await new Promise(resolve => setTimeout(resolve, 1000));
// Create connection info for display - preserve both local and internet addresses
const connectionInfo = {
connectionCode,
localConnectionCode: networkInfo.localConnectionCode,
internetConnectionCode: networkInfo.internetConnectionCode,
method: networkInfo.hasInternet ? 'internet' : 'local',
tunnelMethod: networkInfo.hasInternet ? (
networkInfo.internetConnectionCode.includes('ngrok.io') ? 'ngrok' :
networkInfo.internetConnectionCode.includes('loca.lt') ? 'localtunnel' :
networkInfo.internetConnectionCode.includes('serveo.net') ? 'serveo' :
'manual'
) : null,
isHost: true,
hasInternet: networkInfo.hasInternet,
isHostAutoClient: true // Flag to bypass password auth
};
// Start client chat interface
await startChatInterface(clientNode, connectionInfo);
} catch (error) {
console.error(chalk.red('ā Auto-connect failed:'), error.message);
console.log(chalk.yellow('š Falling back to manual client spawn...'));
await fallbackClientSpawn(networkInfo);
}
}
async function fallbackClientSpawn(networkInfo) {
try {
const { spawn } = await import('child_process');
console.log(chalk.yellow('\nš Spawning separate client terminal...'));
// Try to spawn a new terminal with melq client
const connectionCode = networkInfo.localConnectionCode || networkInfo.internetConnectionCode;
// Try common terminal emulators based on platform
const isWindows = process.platform === 'win32';
const isMac = process.platform === 'darwin';
let terminals = [];
if (isWindows) {
terminals = [
{ cmd: 'wt', args: ['node', 'src/index.js', '--join', connectionCode] }, // Windows Terminal
{ cmd: 'powershell', args: ['-Command', `node src/index.js --join ${connectionCode}`] },
{ cmd: 'cmd', args: ['/c', `start cmd /k "node src/index.js --join ${connectionCode}"`] }
];
} else if (isMac) {
terminals = [
{ cmd: 'osascript', args: ['-e', `tell application "Terminal" to do script "cd '${process.cwd()}' && node src/index.js --join ${connectionCode}"`] },
{ cmd: 'open', args: ['-a', 'Terminal', '.'] }
];
} else {
// Linux/Unix
terminals = [
{ cmd: 'gnome-terminal', args: ['--', 'node', 'src/index.js', '--join', connectionCode] },
{ cmd: 'xterm', args: ['-e', `node src/index.js --join ${connectionCode}`] },
{ cmd: 'konsole', args: ['-e', `node src/index.js --join ${connectionCode}`] },
{ cmd: 'x-terminal-emulator', args: ['-e', `node src/index.js --join ${connectionCode}`] }
];
}
let spawned = false;
for (const terminal of terminals) {
try {
spawn(terminal.cmd, terminal.args, {
detached: true,
stdio: 'ignore',
cwd: process.cwd()
});
console.log(chalk.green(`ā
Spawned client in ${terminal.cmd}`));
spawned = true;
break;
} catch (err) {
// Try next terminal
continue;
}
}
if (!spawned) {
console.log(chalk.red('ā Could not spawn automatic client terminal.'));
console.log(chalk.blue('š Manual connection instructions:'));
console.log(chalk.gray('Open a new terminal and run:'));
console.log(chalk.white(` melq --join ${connectionCode}`));
}
} catch (error) {
console.error(chalk.red('ā Fallback client spawn failed:'), error.message);
console.log(chalk.blue('š Manual connection instructions:'));
console.log(chalk.gray('Open a new terminal and run:'));
const connectionCode = networkInfo.localConnectionCode || networkInfo.internetConnectionCode;
console.log(chalk.white(` melq --join ${connectionCode}`));
}
}
function startHostMonitoring(hostNode, networkInfo) {
console.log(chalk.green('\nš„ļø HOST MONITORING INTERFACE'));
console.log(chalk.gray('ā'.repeat(50)));
console.log(chalk.blue('š Network Status:'));
console.log(chalk.gray(` Local: ${networkInfo.localConnectionCode}`));
if (networkInfo.internetConnectionCode) {
console.log(chalk.gray(` Internet: ${networkInfo.internetConnectionCode}`));
}
console.log(chalk.gray(` Port: ${networkInfo.port}`));
console.log(chalk.gray(` Host IP: ${networkInfo.ip}`));
// Show initial stats
console.log(chalk.yellow('\nā” Live Stats:'));
const statsLine = chalk.blue('š„ Connected: 0 | š¬ Chats: 0');
console.log(statsLine);
// Update stats periodically using title bar instead
setInterval(() => {
const connectedCount = hostNode.connectedNodes ? hostNode.connectedNodes.size : 0;
const chatCount = hostNode.chatRooms ? hostNode.chatRooms.size : 0;
// Update terminal title instead of interfering with command line
process.stdout.write(`\x1b]0;MELQ Host - Connected: ${connectedCount} | Chats: ${chatCount}\x1b\\`);
}, 2000);
console.log(chalk.gray('(Stats shown in terminal title bar)'));
}
async function startChatInterface(node, connectionInfo = null) {
// Give a moment for any console output to finish
await new Promise(resolve => setTimeout(resolve, 500));
// Clear screen to separate client interface from host output
console.clear();
console.log(chalk.gray('Initializing chat interface...'));
const cli = new CLIInterface(node);
// Set connection info if provided (client mode)
if (connectionInfo) {
cli.setConnectionInfo(connectionInfo);
}
cli.start();
// Auto-discovery is now handled by the UnifiedNode itself
}
async function handleCheckUpdatesCommand() {
console.log(chalk.blue('š MELQ Update Checker'));
console.log(chalk.gray('ā'.repeat(50)));
try {
const { execSync } = await import('child_process');
const fs = await import('fs');
// Get current version from package.json
let currentVersion = '1.0.0';
try {
const packagePath = new URL('../package.json', import.meta.url).pathname;
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
currentVersion = packageJson.version;
} catch (error) {
console.log(chalk.yellow('ā ļø Could not read current version, assuming 1.0.0'));
}
console.log(chalk.yellow('š” Checking npm registry for updates...'));
console.log(chalk.gray(` Current version: ${currentVersion}`));
// Check npm registry for latest version
try {
const result = execSync('npm view melq version', { encoding: 'utf8', timeout: 10000, stdio: ['pipe', 'pipe', 'ignore'] }).trim();
const latestVersion = result;
console.log(chalk.gray(` Latest version: ${latestVersion}`));
if (currentVersion === latestVersion) {
console.log(chalk.green('ā
MELQ is already up to date!'));
console.log(chalk.gray(' No updates available.'));
} else {
console.log(chalk.green('š Update available!'));
console.log(chalk.yellow(` ${currentVersion} ā ${latestVersion}`));
console.log(chalk.blue('\nš” Run "melq --update" to install the update.'));
console.log(chalk.dim(' Or use: npm update -g melq'));
}
} catch (error) {
// Most likely this is a 404 error (package not found) for development installations
console.log(chalk.blue('š¦ This appears to be a development installation.'));
console.log(chalk.gray(' Package not yet published to npm registry.'));
console.log(chalk.yellow('\nš” You have the latest development version!'));
}
} catch (error) {
console.error(chalk.red('ā Update check failed:'), error.message);
console.log(chalk.blue('\nš” To manually update:'));
console.log(chalk.white(' npm install -g melq@latest'));
}
}
async function handleUpdateCommand() {
console.log(chalk.blue('š MELQ Update Manager'));
console.log(chalk.gray('ā'.repeat(50)));
try {
const { execSync } = await import('child_process');
const fs = await import('fs');
// Get current version
let currentVersion = '1.0.0';
try {
const packagePath = new URL('../package.json', import.meta.url).pathname;
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
currentVersion = packageJson.version;
} catch (error) {
console.log(chalk.yellow('ā ļø Could not read current version, proceeding with update...'));
}
console.log(chalk.yellow('š” Checking for updates...'));
console.log(chalk.gray(` Current version: ${currentVersion}`));
// Check if update is available
let latestVersion = currentVersion;
try {
const result = execSync('npm view melq version', { encoding: 'utf8', timeout: 10000, stdio: ['pipe', 'pipe', 'ignore'] }).trim();
latestVersion = result;
console.log(chalk.gray(` Latest version: ${latestVersion}`));
if (currentVersion === latestVersion) {
console.log(chalk.green('ā
MELQ is already up to date!'));
console.log(chalk.gray(' No updates available.'));
return;
}
console.log(chalk.green('š Update available!'));
console.log(chalk.yellow(` ${currentVersion} ā ${latestVersion}`));
// Get and display commit messages
try {
const commitMessages = await getCommitMessagesBetweenVersions(currentVersion, latestVersion);
if (commitMessages && commitMessages.length > 0) {
console.log(chalk.cyan('\nš What\'s new in this version:'));
commitMessages.forEach(commit => {
console.log(chalk.gray(` ⢠${commit}`));
});
}
} catch (error) {
// Ignore commit message fetch errors
}
} catch (error) {
// Most likely this is a 404 error (package not found) for development installations
console.log(chalk.blue('š¦ This appears to be a development installation.'));
console.log(chalk.gray(' Package not yet published to npm registry.'));
console.log(chalk.yellow('š” You already have the latest development version!'));
console.log(chalk.dim(' No update needed for development installations.'));
return;
}
// Ask for confirmation
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const confirm = await new Promise(resolve => {
rl.question(chalk.green('Update to latest version? (Y/n): '), resolve);
});
rl.close();
if (confirm.toLowerCase().startsWith('n')) {
console.log(chalk.yellow('Update cancelled.'));
return;
}
console.log(chalk.yellow('š Updating MELQ...'));
console.log(chalk.gray(' This may take a moment...'));
// Update via npm
try {
console.log(chalk.cyan('š¦ Installing latest version...'));
execSync('npm install -g melq@latest', { stdio: 'pipe', timeout: 60000 });
console.log(chalk.green('ā
MELQ successfully updated!'));
console.log(chalk.yellow(` ${currentVersion} ā ${latestVersion}`));
// Display commit messages after successful update
try {
const commitMessages = await getCommitMessagesBetweenVersions(currentVersion, latestVersion);
if (commitMessages && commitMessages.length > 0) {
console.log(chalk.cyan('\nš What\'s new in this version:'));
commitMessages.forEach(commit => {
console.log(chalk.gray(` ⢠${commit}`));
});
}
} catch (error) {
// Show fallback messages if commit fetch fails
console.log(chalk.cyan('\nš What\'s new in this version:'));
console.log(chalk.gray(` ⢠Updated from ${currentVersion} to ${latestVersion}`));
console.log(chalk.gray(' ⢠Bug fixes and improvements'));
console.log(chalk.gray(' ⢠Enhanced security and performance'));
}
console.log(chalk.yellow('\nš Update complete! MELQ is ready to use.'));
console.log(chalk.dim(' Run "melq" to start the updated version.'));
} catch (updateError) {
console.log(chalk.red('ā npm update failed. Trying alternative method...'));
// Try alternative update method
try {
execSync('npm uninstall -g melq', { stdio: 'pipe' });
execSync('npm install -g melq', { stdio: 'pipe', timeout: 60000 });
console.log(chalk.green('ā
MELQ successfully updated using alternative method!'));
} catch (altError) {
throw updateError; // Throw original error
}
}
} catch (error) {
console.error(chalk.red('ā Update failed:'), error.message);
console.log(chalk.yellow('\nš§ Manual update instructions:'));
console.log(chalk.white(' npm install -g melq@latest'));
console.log(chalk.dim('\nIf that fails, try:'));
console.log(chalk.white(' npm uninstall -g melq'));
console.log(chalk.white(' npm install -g melq'));
}
}
async function handleUpdateAutoCommand(mode) {
console.log(chalk.blue('š MELQ Auto-Update Settings'));
console.log(chalk.gray('ā'.repeat(50)));
try {
const configDir = path.join(os.homedir(), '.config', 'melq');
const settingsFile = path.join(configDir, 'settings');
// Ensure config directory exists
await fs.mkdir(configDir, { recursive: true });
if (mode === 'on' || mode === 'enable' || mode === 'true') {
await fs.writeFile(settingsFile, 'auto_update=true\n');
console.log(chalk.green('ā
Automatic updates enabled!'));
console.log(chalk.gray(' MELQ will check for updates when you start it.'));
console.log(chalk.dim(' You can disable this with: melq --update-auto off'));
} else if (mode === 'off' || mode === 'disable' || mode === 'false') {
await fs.writeFile(settingsFile, 'auto_update=false\n');
console.log(chalk.yellow('š Automatic updates disabled.'));
console.log(chalk.gray(' You can manually update with: melq --update'));
console.log(chalk.dim(' Re-enable with: melq --update-auto on'));
} else {
console.log(chalk.red('ā Invalid mode. Use "on" or "off".'));
console.log(chalk.gray(' Examples:'));
console.log(chalk.gray(' melq --update-auto on # Enable automatic updates'));
console.log(chalk.gray(' melq --update-auto off # Disable automatic updates'));
}
} catch (error) {
console.error(chalk.red('ā Failed to update auto-update settings:'), error.message);
}
}
async function checkForAutoUpdates() {
try {
const configDir = path.join(os.homedir(), '.config', 'melq');
const settingsFile = path.join(configDir, 'settings');
// Check if auto-update is enabled
let autoUpdateEnabled = false;
try {
const settings = await fs.readFile(settingsFile, 'utf8');
autoUpdateEnabled = settings.includes('auto_update=true');
} catch (error) {
// Settings file doesn't exist, assume auto-update is disabled
return;
}
if (!autoUpdateEnabled) {
return;
}
// Check for updates silently
const updateInfo = await checkForUpdatesInternal();
if (updateInfo.hasUpdate) {
console.log(chalk.green('š MELQ update available!'));
console.log(chalk.yellow(` ${updateInfo.currentVersion} ā ${updateInfo.latestVersion}`));
// Display commit messages if available
if (updateInfo.commitMessages && updateInfo.commitMessages.length > 0) {
console.log(chalk.cyan('\nš What\'s new:'));
updateInfo.commitMessages.forEach(commit => {
console.log(chalk.gray(` ⢠${commit}`));
});
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const updateNow = await new Promise(resolve => {
rl.question(chalk.green('\nInstall update now? (Y/n): '), resolve);
});
rl.close();
if (!updateNow.toLowerCase().startsWith('n')) {
await performUpdate(updateInfo.currentVersion, updateInfo.latestVersion, updateInfo.commitMessages);
process.exit(0); // Exit after update
} else {
console.log(chalk.dim('\nš” You can update later with: melq --update'));
}
}
} catch (error) {
// Silently fail - don't interrupt startup for update check failures
}
}
async function checkForUpdatesInternal() {
const { execSync } = await import('child_process');
const fs = await import('fs');
// Get current version
let currentVersion = '1.0.0';
try {
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packagePath = path.join(__dirname, '..', 'package.json');
const packageJson = JSON.parse(await import('fs').then(fs => fs.readFileSync(packagePath, 'utf8')));
currentVersion = packageJson.version;
} catch (error) {
// Fallback version
}
try {
// Check npm registry for latest version
const result = execSync('npm view melq version', {
encoding: 'utf8',
timeout: 5000,
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
const latestVersion = result;
const hasUpdate = currentVersion !== latestVersion;
let commitMessages = [];
if (hasUpdate) {
try {
// Try to get commit messages/changelog from npm registry
const changelogResult = execSync('npm view melq versions --json', {
encoding: 'utf8',
timeout: 5000,
stdio: ['pipe', 'pipe', 'ignore']
});
// This is a simplified approach - in a real implementation, you'd want to
// parse the Git repository or maintain a changelog
commitMessages = await getCommitMessagesBetweenVersions(currentVersion, latestVersion);
} catch (error) {
// Fallback commit messages
commitMessages = [
`Updated to version ${latestVersion}`,
'Bug fixes and improvements',
'Enhanced security and performance'
];
}
}
return {
hasUpdate,
currentVersion,
latestVersion,
commitMessages
};
} catch (error) {
return {
hasUpdate: false,
currentVersion,
latestVersion: currentVersion,
commitMessages: []
};
}
}
async function getCommitMessagesBetweenVersions(currentVersion, latestVersion) {
try {
// Try to get commit messages from the git repository
const repoUrl = 'https://api.github.com/repos/ecbaldwin4/melq/releases';
// This would ideally fetch from GitHub API, but for now we'll return generic messages
// In a production environment, you'd want to:
// 1. Fetch releases from GitHub API
// 2. Parse commit messages between versions
// 3. Format them nicely for display
return [
'Performance improvements and bug fixes',
'Enhanced security features',
'Updated dependencies',
'UI/UX improvements'
];
} catch (error) {
return [
`Updated from ${currentVersion} to ${latestVersion}`,
'Various improvements and bug fixes'
];
}
}
async function performUpdate(currentVersion, latestVersion, commitMessages) {
console.log(chalk.yellow('\nš Installing update...'));
console.log(chalk.gray(' This may take a moment...'));
try {
console.log(chalk.cyan('š¦ Installing latest version...'));
execSync('npm install -g melq@latest', { stdio: 'pipe', timeout: 60000 });
console.log(chalk.green('ā
MELQ successfully updated!'));
console.log(chalk.yellow(` ${currentVersion} ā ${latestVersion}`));
if (commitMessages && commitMessages.length > 0) {
console.log(chalk.cyan('\nš What\'s new in this version:'));
commitMessages.forEach(commit => {
console.log(chalk.gray(` ⢠${commit}`));
});
}
console.log(chalk.yellow('\nš Update complete! MELQ is ready to use.'));
console.log(chalk.dim(' The updated version will start after this message.'));
} catch (updateError) {
console.log(chalk.red('ā npm update failed. Trying alternative method...'));
try {
execSync('npm uninstall -g melq', { stdio: 'pipe' });
execSync('npm install -g melq', { stdio: 'pipe', timeout: 60000 });
console.log(chalk.green('ā
MELQ successfully updated using alternative method!'));
} catch (altError) {
throw updateError;
}
}
}
function showHelp() {
console.log(chalk.yellow('\nš MELQ Help'));
console.log(chalk.gray('ā'.repeat(50)));
console.log(chalk.cyan('\nQuick Start:'));
console.log(chalk.white(' melq ') + chalk.gray('Interactive menu'));
console.log(chalk.white(' melq --host ') + chalk.gray('Host a new network'));
console.log(chalk.white(' melq --host --internet ') + chalk.gray('Host with internet access'));
console.log(chalk.white(' melq --host --local-only ') + chalk.gray('Host for local network only'));
console.log(chalk.white(' melq --host --password <pass> ') + chalk.gray('Host password-protected session'));
console.log(chalk.white(' melq --join <code> ') + chalk.gray('Join existing network'));
console.log(chalk.white(' melq --update ') + chalk.gray('Update to latest version'));
console.log(chalk.white(' melq --check-updates ') + chalk.gray('Check if updates are available'));
console.log(chalk.white(' melq --update-auto on ') + chalk.gray('Enable automatic updates'));
console.log(chalk.white(' melq --update-auto off ') + chalk.gray('Disable automatic updates'));
console.log(chalk.cyan('\nConnection Codes:'));
console.log(chalk.gray(' Local: melq://192.168.1.100:3000'));
console.log(chalk.gray(' Internet: https://abc123.ngrok.io'));
console.log(chalk.gray(' The host displays these codes when starting'));
console.log(chalk.gray(' Share them with others to let them join'));
console.log(chalk.cyan('\nInternet Access:'));
console.log(chalk.gray(' ⢠Preferred: localtunnel (instant, no account required!)'));
console.log(chalk.gray(' ⢠Alternative: ngrok or serveo (may require accounts)'));
console.log(chalk.gray(' ⢠Manual setup with port forwarding'));
console.log(chalk.gray(' ⢠Local discovery for same network'));
console.log(chalk.cyan('\nSecurity:'));
console.log(chalk.gray(' ⢠All messages are encrypted with post-quantum ML-KEM-768'));
console.log(chalk.gray(' ⢠Direct P2P connections (no central server)'));
console.log(chalk.gray(' ⢠Messages never stored unencrypted'));
console.log(chalk.gray(' ⢠Optional password protection for sessions'));
console.log(chalk.cyan('\nExamples:'));
console.log(chalk.white(' melq --host --internet'));
console.log(chalk.white(' melq --host --tunnel ngrok'));
console.log(chalk.white(' melq --host --password mypass'));
console.log(chalk.white(' melq --join melq://192.168.1.100:3000'));
console.log(chalk.white(' melq --join https://abc123.ngrok.io'));
console.log(chalk.white(' melq --join 203.0.113.1:3000'));
process.exit(0);
}
program.parse();