UNPKG

polaris-cli-tool

Version:

Polaris CLI - Modern Development Workspace Manager for Distributed Compute Resources

1,361 lines (1,210 loc) 41.7 kB
#!/usr/bin/env node import boxen from 'boxen'; import chalk from 'chalk'; import SingleBar from 'cli-progress'; import Table from 'cli-table3'; import { Command } from 'commander'; import figlet from 'figlet'; import fs, { readFileSync } from 'fs'; import gradient from 'gradient-string'; import inquirer from 'inquirer'; import fetch from 'node-fetch'; import ora from 'ora'; import path, { dirname } from 'path'; import { Client } from 'ssh2'; import { fileURLToPath } from 'url'; import xlsx from 'xlsx'; // Resolve __filename and __dirname const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Read version from package.json const packageJsonPath = path.resolve(__dirname, 'package.json'); const { version } = JSON.parse(readFileSync(packageJsonPath, 'utf8')); // Initialize commander const program = new Command(); program .name('polaris') .description('Polaris CLI - Modern Development Workspace Manager') .version(version, '-v, --version', 'Output the current version'); // Display ASCII art title function displayTitle() { const subtitle = ` ╭──────────────────────────────────────────────╮ │ ⚡ DISTRIBUTED COMPUTE SUBNET MANAGER │ ╰──────────────────────────────────────────────╯`; console.log('\n'); console.log( gradient('#00ff87', '#00ffff', '#0099ff').multiline( figlet.textSync('POLARIS', { font: 'ANSI Shadow', horizontalLayout: 'full', width: 130, whitespaceBreak: true, }) ) ); console.log(gradient.mind(subtitle)); } function transformComputeResource(resource) { const baseResource = { id: resource.id, resource_type: resource.resource_type, location: resource.location, network: { internal_ip: resource.internal_ip, ssh: resource.ssh, open_ports: resource.open_ports ? resource.open_ports.split(',').map(p => p.trim()) : [], // Fallback to empty array if undefined }, hourly_price: resource.hourly_price, ram: resource.ram, storage: { type: resource.storage_type, capacity: resource.storage_capacity, read_speed: resource.storage_read_speed, write_speed: resource.storage_write_speed, }, }; if (resource.resource_type.toUpperCase() === 'CPU') { return { ...baseResource, cpu_specs: { op_modes: resource.cpu_op_modes, address_sizes: resource.cpu_address_sizes, byte_order: resource.cpu_byte_order, total_cpus: resource.total_cpus, online_cpus: resource.online_cpus, vendor_id: resource.vendor_id, cpu_name: resource.cpu_name, cpu_family: resource.cpu_family, model: resource.model, threads_per_core: resource.threads_per_core, cores_per_socket: resource.cores_per_socket, sockets: resource.sockets, stepping: resource.stepping, cpu_max_mhz: resource.cpu_max_mhz, cpu_min_mhz: resource.cpu_min_mhz, }, }; } else if (resource.resource_type.toUpperCase() === 'GPU') { return { ...baseResource, gpu_specs: { gpu_name: resource.gpu_name, memory_size: resource.memory_size, gpu_cores: resource.gpu_cores, clock_speed: resource.clock_speed, cuda_cores: resource.cuda_cores, tensor_cores: resource.tensor_cores, power_consumption: resource.power_consumption, }, }; } } // Validate resources function validateResources(resources) { return resources.every((resource) => { if ( resource.resource_type.toUpperCase() === 'GPU' && !resource.gpu_specs ) { console.log( chalk.red(`Error: GPU resource ${resource.id} missing GPU specifications`) ); return false; } if ( resource.resource_type.toUpperCase() === 'CPU' && !resource.cpu_specs ) { console.log( chalk.red(`Error: CPU resource ${resource.id} missing CPU specifications`) ); return false; } return true; }); } async function addComputeToMiner() { console.log('\n' + chalk.cyan.bold('💻 Add Compute Resources to Existing Miner') + '\n'); // Get miner ID const { minerId } = await inquirer.prompt([ { type: 'input', name: 'minerId', message: chalk.cyan('🔑 Enter your Miner ID:'), validate: (input) => (input.trim() ? true : 'Miner ID is required'), } ]); // Display compute resource template console.log('\n' + chalk.cyan.bold('📋 Compute Resource Template') + '\n'); const table = new Table({ head: [ chalk.cyan.bold('Field'), chalk.cyan.bold('Type'), chalk.cyan.bold('Required For'), chalk.cyan.bold('Description') ], style: { head: [], border: ['gray'] }, }); table.push( ['id', 'String', 'ALL', 'Unique identifier'], ['resource_type', 'CPU/GPU', 'ALL', 'Type of resource'], ['location', 'String', 'ALL', 'Resource location'], ['internal_ip', 'String', 'ALL', 'Internal IP address'], ['ssh', 'String', 'ALL', 'SSH connection string'], ['open_ports', 'String', 'ALL', 'Comma-separated ports'], ['hourly_price', 'Number', 'ALL', 'Price per hour'], ['ram', 'String', 'ALL', 'RAM specification'], ['storage_type', 'NVME/SSD/HDD', 'ALL', 'Storage type'], ['storage_capacity', 'String', 'ALL', 'Storage capacity'], ['storage_read_speed', 'String', 'ALL', 'Read speed'], ['storage_write_speed', 'String', 'ALL', 'Write speed'] ); console.log( boxen(table.toString(), { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'cyan', }) ); // Get Excel file const { filePath } = await inquirer.prompt([ { type: 'input', name: 'filePath', message: chalk.cyan('\n📂 Drag and drop your compute resources Excel file here (or enter path): '), validate: (input) => { const trimmedInput = input.trim().replace(/^"+|"+$/g, ''); const normalizedPath = path.normalize(trimmedInput); return fs.existsSync(normalizedPath) && normalizedPath.endsWith('.xlsx') ? true : chalk.red('Please provide a valid Excel file (.xlsx)'); }, } ]); const cleanPath = filePath.trim().replace(/^["']|["']$/g, ''); const normalizedPath = path.normalize(cleanPath); // Process Excel file const workbook = xlsx.readFile(normalizedPath); const sheetName = workbook.SheetNames[0]; const computeResources = xlsx.utils.sheet_to_json(workbook.Sheets[sheetName]); // Transform resources const transformedResources = computeResources.map(resource => transformComputeResource(resource) ).filter(Boolean); // Display submission for verification console.log('\n' + chalk.cyan.bold('📝 Please verify the resources to add:') + '\n'); console.log( boxen(JSON.stringify(transformedResources, null, 2), { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'yellow', }) ); // Confirmation const { confirm } = await inquirer.prompt([ { type: 'confirm', name: 'confirm', message: chalk.cyan('Do you want to add these compute resources?'), default: false, } ]); if (confirm) { const spinner = ora('Validating resources...').start(); if (!validateResources(transformedResources)) { spinner.fail('Resource validation failed'); return null; } spinner.text = 'Adding compute resources...'; try { const response = await fetch(`https://orchestrator-gekh.onrender.com/api/v1/miners/${minerId}/resources/bulk`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(transformedResources), }); if (!response.ok) { const errorData = await response.json(); spinner.fail(chalk.red(`Failed to add resources: ${JSON.stringify(errorData)}`)); return null; } const result = await response.json(); spinner.succeed(chalk.green('Resources added successfully!')); // Display the response console.log('\n' + chalk.cyan.bold('📡 Addition Status') + '\n'); const responseBox = boxen( `${chalk.green.bold('✨ ' + result.message)}\n\n` + `${chalk.blue.bold('Miner ID:')} ${chalk.yellow(result.miner_id)}\n\n` + `${chalk.blue.bold('Added Resources:')}\n${result.added_resources .map((id) => chalk.cyan(`• ${id}`)) .join('\n')}`, { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'green', title: '🎉 Resources Added', titleAlignment: 'center', } ); console.log(responseBox); return result; } catch (error) { spinner.fail(chalk.red(`Error adding resources: ${error.message}`)); console.log(chalk.yellow('\nTip: Make sure the API server is running at https://orchestrator-gekh.onrender.com')); return null; } } else { console.log(chalk.yellow('Operation cancelled.')); return null; } } // Display file info in workspace format function displayFileInfo( originalPath, destinationPath, status = 'PROCESSING', resourceCount = null ) { const fileStats = fs.statSync(originalPath); const fileName = path.basename(originalPath); const fileSize = (fileStats.size / (1024 * 1024)).toFixed(2); const fileId = Math.random().toString(36).substring(2, 15); const info = [ chalk.bold('Workspace Info\n'), `Name ${chalk.white(fileName)}`, `ID ${chalk.cyan(fileId)}`, `Size ${chalk.yellow(fileSize + ' MB')}`, `State ${ status === 'PROCESSING' ? chalk.yellow(status) : chalk.green(status) }`, `Location ${chalk.white(destinationPath)}`, `Created ${chalk.white(new Date().toISOString())}`, ]; if (resourceCount !== null) { info.push(`Resources ${chalk.yellow(resourceCount + ' compute units')}`); } console.log('\n'); console.log( boxen(info.join('\n'), { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'white', }) ); return fileId; } async function registerMiner() { console.log('\n' + chalk.cyan.bold('⛏️ Miner Registration') + '\n'); // Basic miner info collection const minerInfo = await inquirer.prompt([ { type: 'input', name: 'name', message: chalk.cyan('Enter miner name:'), validate: (input) => (input.trim() ? true : 'Name is required'), }, { type: 'input', name: 'location', message: chalk.cyan('Enter location (e.g., NYC):'), validate: (input) => (input.trim() ? true : 'Location is required'), }, { type: 'input', name: 'description', message: chalk.cyan('Enter cluster description:'), validate: (input) => (input.trim() ? true : 'Description is required'), } ]); // Display compute resource template console.log('\n' + chalk.cyan.bold('📋 Compute Resource Template') + '\n'); const table = new Table({ head: [ chalk.cyan.bold('Field'), chalk.cyan.bold('Type'), chalk.cyan.bold('Required For'), chalk.cyan.bold('Description') ], style: { head: [], border: ['gray'] }, }); table.push( ['id', 'String', 'ALL', 'Unique identifier'], ['resource_type', 'CPU/GPU', 'ALL', 'Type of resource'], ['location', 'String', 'ALL', 'Resource location'], ['internal_ip', 'String', 'ALL', 'Internal IP address'], ['ssh', 'String', 'ALL', 'SSH connection string'], ['open_ports', 'String', 'ALL', 'Comma-separated ports'], ['hourly_price', 'Number', 'ALL', 'Price per hour'], ['ram', 'String', 'ALL', 'RAM specification'], ['storage_type', 'NVME/SSD/HDD', 'ALL', 'Storage type'], ['storage_capacity', 'String', 'ALL', 'Storage capacity'], ['storage_read_speed', 'String', 'ALL', 'Read speed'], ['storage_write_speed', 'String', 'ALL', 'Write speed'], ['cpu_specs.*', 'Object', 'CPU only', 'CPU specifications'], ['gpu_specs.*', 'Object', 'GPU only', 'GPU specifications'] ); console.log( boxen(table.toString(), { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'cyan', }) ); // Excel file collection const { filePath } = await inquirer.prompt([ { type: 'input', name: 'filePath', message: chalk.cyan('\n📂 Drag and drop your compute resources Excel file here (or enter path): '), validate: (input) => { const trimmedInput = input.trim().replace(/^"+|"+$/g, ''); const normalizedPath = path.normalize(trimmedInput); return fs.existsSync(normalizedPath) && normalizedPath.endsWith('.xlsx') ? true : chalk.red('Please provide a valid Excel file (.xlsx)'); }, } ]); // Clean up the path const cleanPath = filePath.trim().replace(/^["']|["']$/g, ''); const normalizedPath = path.normalize(cleanPath); // Process Excel file const workbook = xlsx.readFile(normalizedPath); const sheetName = workbook.SheetNames[0]; const computeResources = xlsx.utils.sheet_to_json(workbook.Sheets[sheetName]); // Transform resources const transformedResources = computeResources.map(resource => { const baseResource = { id: resource.id, resource_type: resource.resource_type, location: resource.location, network: { internal_ip: resource.internal_ip, ssh: resource.ssh, password: resource.password, open_ports: resource.open_ports.split(',').map(p => p.trim()) }, hourly_price: resource.hourly_price, ram: resource.ram, storage: { type: resource.storage_type, capacity: resource.storage_capacity, read_speed: resource.storage_read_speed, write_speed: resource.storage_write_speed } }; if (resource.resource_type.toUpperCase() === 'CPU') { return { ...baseResource, cpu_specs: { op_modes: resource.cpu_op_modes, address_sizes: resource.cpu_address_sizes, byte_order: resource.cpu_byte_order, total_cpus: parseInt(resource.total_cpus), online_cpus: resource.online_cpus, vendor_id: resource.vendor_id, cpu_name: resource.cpu_name, cpu_family: parseInt(resource.cpu_family), model: parseInt(resource.model), threads_per_core: parseInt(resource.threads_per_core), cores_per_socket: parseInt(resource.cores_per_socket), sockets: parseInt(resource.sockets), stepping: resource.stepping ? parseInt(resource.stepping) : null, cpu_max_mhz: parseFloat(resource.cpu_max_mhz), cpu_min_mhz: parseFloat(resource.cpu_min_mhz) } }; } else if (resource.resource_type.toUpperCase() === 'GPU') { return { ...baseResource, gpu_specs: { gpu_name: resource.gpu_name, memory_size: resource.memory_size, cuda_cores: resource.cuda_cores ? parseInt(resource.cuda_cores) : null, clock_speed: resource.clock_speed, power_consumption: resource.power_consumption } }; } return null; }).filter(Boolean); // Construct final submission object const submission = { name: minerInfo.name, location: minerInfo.location, description: minerInfo.description, compute_resources: transformedResources }; // Display submission for verification console.log('\n' + chalk.cyan.bold('📝 Please verify your submission:') + '\n'); console.log( boxen(JSON.stringify(submission, null, 2), { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'yellow', }) ); // Confirmation const { confirm } = await inquirer.prompt([ { type: 'confirm', name: 'confirm', message: chalk.cyan('Do you want to submit this registration?'), default: false, } ]); if (confirm) { const spinner = ora('Validating resources...').start(); // Validate resources for (const resource of transformedResources) { // Validate network configuration if (!resource.network || !resource.network.internal_ip || !resource.network.ssh || !resource.network.open_ports) { spinner.fail('Network configuration is missing or invalid'); return null; } // Validate resource type specific configurations if (resource.resource_type === 'CPU' && !resource.cpu_specs) { spinner.fail(`CPU resource ${resource.id} missing CPU specifications`); return null; } if (resource.resource_type === 'GPU' && !resource.gpu_specs) { spinner.fail(`GPU resource ${resource.id} missing GPU specifications`); return null; } } spinner.text = 'Submitting registration...'; try { const response = await fetch('https://orchestrator-gekh.onrender.com/api/v1/miners/', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(submission), }); if (!response.ok) { const errorData = await response.json(); spinner.fail(chalk.red(`Registration failed: ${JSON.stringify(errorData)}`)); return null; } const result = await response.json(); spinner.succeed(chalk.green('Registration submitted successfully!')); // Display response console.log('\n' + chalk.cyan.bold('📡 Registration Status') + '\n'); const responseBox = boxen( `${chalk.green.bold('✨ ' + result.message)}\n\n` + `${chalk.blue.bold('Miner ID:')} ${chalk.yellow(result.miner_id)}\n\n` + `${chalk.blue.bold('Compute Resources:')}\n${result.compute_resources .map((id) => chalk.cyan(`• ${id}`)) .join('\n')}`, { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'green', title: '🎉 Registration Complete', titleAlignment: 'center', } ); console.log(responseBox); // Save confirmation console.log( boxen( chalk.yellow.bold('Important: Save your Miner ID\n\n') + `Your Miner ID is: ${chalk.cyan.bold(result.miner_id)}\n` + chalk.gray('You will need this ID to manage your compute resources.'), { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'yellow', title: '🔑 Keep Safe', titleAlignment: 'center', } ) ); return result; } catch (error) { spinner.fail(chalk.red(`Error submitting registration: ${error.message}`)); console.log(chalk.yellow('\nTip: Make sure the API server is running at https://orchestrator-gekh.onrender.com')); return null; } } else { console.log(chalk.yellow('Registration cancelled.')); return null; } } // Process compute resources with dynamic loading async function processComputeResources(filePath) { const spinner = ora({ text: 'Initializing compute resource processing...', spinner: 'dots12', color: 'cyan', }).start(); const uploadDir = path.join(__dirname, 'uploaded_resources'); if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir); } const fileName = `compute_${Date.now()}_${path.basename(filePath)}`; const destinationPath = path.join(uploadDir, fileName); try { spinner.text = 'Copying resource file...'; fs.copyFileSync(filePath, destinationPath); spinner.succeed('File copied successfully'); const fileId = displayFileInfo(filePath, destinationPath); const multibar = new SingleBar.MultiBar({ clearOnComplete: false, hideCursor: true, format: '{bar} | {percentage}% | {task}', barCompleteChar: '\u2588', barIncompleteChar: '\u2591', barsize: 30, }); const steps = [ { task: 'Validating resource specifications', duration: 1500 }, { task: 'Processing compute capabilities ', duration: 2000 }, { task: 'Configuring network settings ', duration: 1000 }, { task: 'Establishing secure connection ', duration: 800 }, ]; const bars = steps.map((step) => multibar.create(100, 0, { task: step.task }) ); for (let i = 0; i < steps.length; i++) { for (let progress = 0; progress <= 100; progress += 2) { bars[i].update(progress); await new Promise((r) => setTimeout(r, steps[i].duration / 50)); } bars[i].update(100); if (i < steps.length - 1) { await new Promise((r) => setTimeout(r, 200)); } } multibar.stop(); const workbook = xlsx.readFile(destinationPath); const sheetName = workbook.SheetNames[0]; const data = xlsx.utils.sheet_to_json(workbook.Sheets[sheetName]); displayFileInfo(destinationPath, destinationPath, 'RUNNING', data.length); const forwardSpinner = ora({ text: 'Forwarding compute node port to 6300...', spinner: 'dots12', color: 'cyan', }).start(); await new Promise((r) => setTimeout(r, 1500)); forwardSpinner.succeed('Port forwarded successfully'); const browserBar = new SingleBar.SingleBar({ format: 'Opening browser {bar} {percentage}% | {value}/{total}s', barCompleteChar: '█', barIncompleteChar: '░', barsize: 30, }); browserBar.start(100, 0); for (let i = 0; i <= 100; i += 2) { browserBar.update(i); await new Promise((r) => setTimeout(r, 30)); } browserBar.stop(); return data; } catch (error) { spinner.fail(chalk.red(`Error: ${error.message}`)); return null; } } async function connectSSH(host, username, password = null) { return new Promise((resolve, reject) => { const conn = new Client(); conn.on('ready', () => { resolve(conn); }).on('error', (err) => { reject(err); }); // Connection config const config = { host: host, username: username, password: password, // Add key-based auth support tryKeyboard: true, keepaliveInterval: 10000 }; conn.connect(config); }); } async function executeCommand(conn, command) { return new Promise((resolve, reject) => { conn.exec(command, (err, stream) => { if (err) reject(err); let output = ''; stream.on('data', (data) => { output += data; }); stream.on('end', () => { resolve(output); }); stream.on('error', (err) => { reject(err); }); }); }); } async function runSubnet() { // Get miner ID const { minerId } = await inquirer.prompt([ { type: 'input', name: 'minerId', message: chalk.cyan('🔑 Enter your Miner ID:'), prefix: '→', validate: (input) => { if (!input.trim()) { return chalk.red('Miner ID is required'); } return true; }, } ]); const spinner = ora({ text: '🚀 Initializing Polaris Subnet...', spinner: 'dots12', color: 'cyan', }).start(); try { // Fetch miner data const response = await fetch( `https://orchestrator-gekh.onrender.com/api/v1/miners/${minerId}`, { method: 'GET', headers: { 'Content-Type': 'application/json', } } ); if (!response.ok) { spinner.fail(chalk.red('Failed to fetch miner data.')); console.log(chalk.red(`Error: ${response.statusText}`)); return; } const minerData = await response.json(); spinner.succeed(chalk.green('✨ Subnet initialized successfully!')); console.log( boxen( `${chalk.cyan('Network Status')}\n\n` + `${chalk.blue('●')} Miner ID: ${chalk.cyan(minerId)}\n` + `${chalk.blue('●')} Active Nodes: ${chalk.green(minerData.compute_resources.length)}\n` + `${chalk.blue('●')} Network Health: ${chalk.green('98.5%')}`, { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'green', } ) ); // Prepare choices for resource selection const choices = minerData.compute_resources.map(resource => ({ name: `${chalk.blue(resource.id)}${chalk.yellow(resource.resource_type)} [${chalk.green(resource.location)}]`, value: resource })); // Resource selection const { selectedResource } = await inquirer.prompt([ { type: 'list', name: 'selectedResource', message: chalk.cyan('Select a compute resource to manage:'), choices: choices, pageSize: 10 } ]); // Action selection const { action } = await inquirer.prompt([ { type: 'list', name: 'action', message: chalk.cyan(`Select action for ${chalk.yellow(selectedResource.id)}:`), choices: [ { name: '🚀 Run Resource', value: 'run' }, { name: '📊 View Logs', value: 'logs' } ] } ]); const sshSpinner = ora({ text: `Establishing SSH connection to ${selectedResource.network.ssh}...`, spinner: 'dots12', color: 'cyan', }).start(); try { // Parse SSH URL const sshUrl = new URL(selectedResource.network.ssh); const host = sshUrl.hostname; const username = sshUrl.username; let password; // Check if password is required if (!selectedResource.network.ssh.includes('id_rsa')) { sshSpinner.stop(); const { sshPassword } = await inquirer.prompt([ { type: 'password', name: 'sshPassword', message: chalk.cyan(`Enter SSH password for ${username}@${host}:`), mask: '*' } ]); password = sshPassword; sshSpinner.start(); } // Establish SSH connection const conn = await connectSSH(host, username, password); sshSpinner.succeed('SSH connection established'); if (action === 'run') { const runSpinner = ora({ text: 'Starting compute resource...', spinner: 'dots12', color: 'cyan', }).start(); try { // Check system resources const cpuInfo = await executeCommand(conn, 'cat /proc/cpuinfo | grep "model name" | head -n1'); const memInfo = await executeCommand(conn, 'free -h'); const diskInfo = await executeCommand(conn, 'df -h /'); runSpinner.succeed('Resource information retrieved'); console.log( boxen( `${chalk.green.bold('✨ Resource Status')}\n\n` + `${chalk.blue('●')} ID: ${chalk.cyan(selectedResource.id)}\n` + `${chalk.blue('●')} Type: ${chalk.yellow(selectedResource.resource_type)}\n` + `${chalk.blue('●')} CPU: ${chalk.white(cpuInfo.split(':')[1].trim())}\n` + `${chalk.blue('●')} Status: ${chalk.green('Connected')}\n\n` + chalk.gray('System Information:\n') + chalk.white(memInfo) + '\n' + chalk.white(diskInfo), { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'green', title: '🖥️ Compute Resource', titleAlignment: 'center', } ) ); // Interactive shell option const { startShell } = await inquirer.prompt([ { type: 'confirm', name: 'startShell', message: chalk.cyan('Would you like to start an interactive shell?'), default: false } ]); if (startShell) { conn.shell((err, stream) => { if (err) throw err; // Handle shell input/output stream.on('data', (data) => { process.stdout.write(data); }); stream.stderr.on('data', (data) => { process.stderr.write(data); }); stream.on('close', () => { console.log(chalk.yellow('\nSSH connection closed')); conn.end(); }); // Pipe process stdin to stream process.stdin.pipe(stream); }); } else { conn.end(); } } catch (error) { runSpinner.fail(chalk.red(`Failed to execute commands: ${error.message}`)); conn.end(); } } else if (action === 'logs') { const logSpinner = ora({ text: 'Fetching Docker information...', spinner: 'dots12', color: 'cyan', }).start(); try { // Fetch Docker images const dockerImages = await executeCommand(conn, 'docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"'); const imageLines = dockerImages.trim().split('\n'); // Fetch Docker containers const dockerContainers = await executeCommand(conn, 'docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"'); const containerLines = dockerContainers.trim().split('\n'); logSpinner.succeed('Docker information retrieved successfully'); // Display Docker Images in a table const imageTable = new Table({ head: [chalk.cyan('Repository'), chalk.cyan('Tag'), chalk.cyan('Size')], style: { head: [], border: [] } }); // First line is the header, skip it for (let i = 1; i < imageLines.length; i++) { const [repo, tag, size] = imageLines[i].split('\t'); imageTable.push([chalk.green(repo), chalk.yellow(tag), chalk.blue(size)]); } console.log( boxen( chalk.bold('🛠️ Docker Images') + '\n\n' + imageTable.toString(), { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'blue', title: '📦 Docker Images', titleAlignment: 'center' } ) ); // Display Docker Containers in a table const containerTable = new Table({ head: [chalk.cyan('Name'), chalk.cyan('Image'), chalk.cyan('Status'), chalk.cyan('Ports')], style: { head: [], border: [] } }); // First line is the header, skip it for (let i = 1; i < containerLines.length; i++) { const [name, image, status, ports] = containerLines[i].split('\t'); containerTable.push([chalk.green(name), chalk.yellow(image), chalk.white(status), chalk.magenta(ports)]); } console.log( boxen( chalk.bold('📡 Running Containers') + '\n\n' + containerTable.toString(), { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'cyan', title: '🌐 Containers', titleAlignment: 'center' } ) ); conn.end(); } catch (error) { logSpinner.fail(chalk.red(`Failed to retrieve Docker information: ${error.message}`)); conn.end(); } } } catch (error) { sshSpinner.fail(chalk.red(`SSH connection failed: ${error.message}`)); } } catch (error) { spinner.fail(chalk.red(`Failed to initialize subnet: ${error.message}`)); console.log(chalk.yellow('\nTip: Ensure the API server is running at https://orchestrator-gekh.onrender.com')); } } // View miner compute resources async function viewMinerCompute() { console.log('\n' + chalk.cyan.bold('👀 View Miner Compute Resources') + '\n'); const { minerId } = await inquirer.prompt([ { type: 'input', name: 'minerId', message: chalk.cyan('🔑 Enter the Miner ID:'), validate: (input) => { if (!input.trim()) { return chalk.red('Miner ID is required'); } return true; }, }, ]); const spinner = ora({ text: 'Fetching miner data...', spinner: 'dots12', color: 'cyan', }).start(); try { const response = await fetch( `https://orchestrator-gekh.onrender.com/api/v1/miners/${minerId}`, { method: 'GET', headers: { 'Content-Type': 'application/json', }, } ); if (!response.ok) { spinner.fail(chalk.red('Failed to fetch miner data.')); console.log(chalk.red(`Error: ${response.statusText}`)); return; } const minerData = await response.json(); spinner.succeed(chalk.green('Miner data fetched successfully!')); // Display miner details const minerInfo = [ chalk.bold('Miner Details\n'), `ID ${chalk.cyan(minerData.id)}`, `Name ${chalk.white(minerData.name)}`, `Location ${chalk.yellow(minerData.location)}`, `Description ${chalk.white(minerData.description)}`, `Created At ${chalk.white( new Date(minerData.created_at).toLocaleString() )}`, `Updated At ${chalk.white( new Date(minerData.updated_at).toLocaleString() )}`, ]; console.log('\n'); console.log( boxen(minerInfo.join('\n'), { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'white', title: '📄 Miner Information', titleAlignment: 'center', }) ); // Prepare compute resources table const resourceTable = new Table({ head: [ chalk.cyan.bold('ID'), chalk.cyan.bold('Type'), chalk.cyan.bold('Location'), chalk.cyan.bold('Price/Hr'), chalk.cyan.bold('RAM'), chalk.cyan.bold('Storage'), chalk.cyan.bold('Specs'), ], style: { head: [], border: [] }, wordWrap: true, colWidths: [15, 7, 10, 10, 10, 15, 40], }); minerData.compute_resources.forEach((resource) => { const storageInfo = `${resource.storage.type} ${resource.storage.capacity}\nRead: ${resource.storage.read_speed}\nWrite: ${resource.storage.write_speed}`; let specs = ''; if ( resource.resource_type.toUpperCase() === 'CPU' && resource.cpu_specs ) { specs = `CPU Name: ${resource.cpu_specs.cpu_name}\nTotal CPUs: ${resource.cpu_specs.total_cpus}\nThreads/Core: ${resource.cpu_specs.threads_per_core}\nMax MHz: ${resource.cpu_specs.cpu_max_mhz}`; } else if ( resource.resource_type.toUpperCase() === 'GPU' && resource.gpu_specs ) { specs = `GPU Name: ${resource.gpu_specs.gpu_name}\nMemory: ${resource.gpu_specs.memory_size}\nCUDA Cores: ${resource.gpu_specs.cuda_cores}\nClock Speed: ${resource.gpu_specs.clock_speed}`; } resourceTable.push([ chalk.green(resource.id), resource.resource_type.toUpperCase(), chalk.yellow(resource.location), chalk.magenta(`$${resource.hourly_price}`), chalk.blue(resource.ram), chalk.cyan(storageInfo), chalk.white(specs), ]); }); console.log( '\n' + boxen( chalk.bold('💻 Compute Resources') + '\n\n' + resourceTable.toString(), { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'cyan', title: '🌟 Resources Overview', titleAlignment: 'center', } ) ); } catch (error) { spinner.fail(chalk.red('An error occurred while fetching miner data.')); console.log(chalk.red(`Error: ${error.message}`)); console.log( chalk.yellow('\nTip: Ensure the API server is running and accessible.') ); } } async function showNavigationMenu() { const { navChoice } = await inquirer.prompt([ { type: 'list', name: 'navChoice', message: chalk.cyan('What would you like to do next?'), choices: [ { name: gradient.passion('↩️ Back to Main Menu'), value: 'MAIN_MENU' }, { name: gradient.cristal('🚪 Exit'), value: 'EXIT' }, ], prefix: '🌟', }, ]); return navChoice; } // Display sample compute resource format function displayResourceFormat() { const table = new Table({ head: [ chalk.cyan.bold('ID'), chalk.cyan.bold('Type'), chalk.cyan.bold('Location'), chalk.cyan.bold('Price/Hr'), chalk.cyan.bold('RAM'), chalk.cyan.bold('Storage'), chalk.cyan.bold('Specs'), ], style: { head: [], border: ['gray'] }, }); table.push( [ chalk.green('plrs_01'), 'CPU', chalk.yellow('NYC'), chalk.magenta('$2.50'), chalk.blue('64GB'), chalk.cyan('2TB NVME\n7000MB/s Read'), chalk.yellow('32x Intel Xeon\n2 Threads/Core'), ], [ chalk.green('plrs_02'), 'GPU', chalk.yellow('LAX'), chalk.magenta('$3.50'), chalk.blue('128GB'), chalk.cyan('5TB NVME\n7000MB/s Read'), chalk.yellow('NVIDIA A100\n40GB VRAM'), ] ); console.log( '\n' + boxen( chalk.bold('📊 Resource Format') + '\n\n' + table.toString(), { padding: 1, margin: 1, borderStyle: 'double', borderColor: 'cyan', title: '🌟 Required Format', titleAlignment: 'center', } ) ); } // Main menu loop async function mainLoop() { while (true) { displayTitle(); const { action } = await inquirer.prompt([ { type: 'list', name: 'action', message: chalk.cyan('🚀 Select Operation:'), choices: [ { name: gradient.passion('▶️ Run Polaris Subnet'), value: 'Run' }, { name: gradient.mind('💻 Add Compute Resources'), value: 'Add' }, { name: gradient.cristal('⛏️ Register Miner'), value: 'Register' }, { name: gradient.summer('👀 View Miner Compute'), value: 'View' }, { name: gradient.cristal('🚪 Exit'), value: 'EXIT' }, ], prefix: '🌟', }, ]); if (action === 'EXIT') { console.log( boxen( chalk.yellow.bold('👋 Thanks for using Polaris!\n') + chalk.gray('See you next time!'), { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'yellow', } ) ); process.exit(0); } switch (action) { case 'Run': await runSubnet(); const runNav = await showNavigationMenu(); if (runNav === 'EXIT') { process.exit(0); } console.clear(); break; case 'Add': console.log('\n' + chalk.cyan.bold('💻 Add Compute Resources') + '\n'); const { choice } = await inquirer.prompt([ { type: 'list', name: 'choice', message: chalk.cyan('What would you like to do?'), choices: [ { name: 'Add to existing miner', value: 'existing' }, { name: 'Process new resources', value: 'new' } ] } ]); if (choice === 'existing') { await addComputeToMiner(); } else { displayResourceFormat(); const { filePath } = await inquirer.prompt([ { type: 'input', name: 'filePath', message: chalk.cyan('\n📂 Drag and drop your compute resources Excel file here (or enter path): '), validate: (input) => { const cleanPath = input.trim().replace(/^["']|["']$/g, ''); try { const normalizedPath = path.normalize(cleanPath); return fs.existsSync(normalizedPath) && normalizedPath.endsWith('.xlsx') ? true : chalk.red('Please provide a valid Excel file (.xlsx)'); } catch (error) { return chalk.red('Invalid file path'); } }, } ]); const cleanPath = filePath.trim().replace(/^["']|["']$/g, ''); const normalizedPath = path.normalize(cleanPath); await processComputeResources(normalizedPath); } const addNav = await showNavigationMenu(); if (addNav === 'EXIT') { process.exit(0); } console.clear(); break; case 'Register': await registerMiner(); const regNav = await showNavigationMenu(); if (regNav === 'EXIT') { process.exit(0); } console.clear(); break; case 'View': await viewMinerCompute(); const viewNav = await showNavigationMenu(); if (viewNav === 'EXIT') { process.exit(0); } console.clear(); break; } } } // Main command program .command('start') .description('Start Polaris with options') .action(async () => { await mainLoop(); }); // Parse command-line arguments program.parse(process.argv);