460 lines
17 KiB
TypeScript
460 lines
17 KiB
TypeScript
import * as plugins from './moxytool.plugins.ts';
|
||
import * as paths from './moxytool.paths.ts';
|
||
import { logger } from './moxytool.logging.ts';
|
||
import { ScriptIndex } from './moxytool.classes.scriptindex.ts';
|
||
import { ScriptRunner } from './moxytool.classes.scriptrunner.ts';
|
||
import denoConfig from '../deno.json' with { type: 'json' };
|
||
|
||
export const runCli = async () => {
|
||
const smartshellInstance = new plugins.smartshell.Smartshell({
|
||
executor: 'bash',
|
||
});
|
||
|
||
const smartcliInstance = new plugins.smartcli.Smartcli();
|
||
smartcliInstance.version = denoConfig.version;
|
||
|
||
// Initialize script index and check if refresh is needed
|
||
const scriptIndex = new ScriptIndex();
|
||
|
||
// Silently check and refresh index in the background if needed
|
||
(async () => {
|
||
try {
|
||
await scriptIndex.loadCache();
|
||
if (await scriptIndex.needsRefresh()) {
|
||
// Don't block CLI startup, refresh in background
|
||
scriptIndex.fetchIndex().catch(() => {
|
||
// Silently fail, will use cached data
|
||
});
|
||
}
|
||
} catch {
|
||
// Silently fail on index errors
|
||
}
|
||
})();
|
||
|
||
// Standard command (no arguments)
|
||
smartcliInstance.standardCommand().subscribe(async () => {
|
||
console.log('\x1b[1m\x1b[36m╔════════════════════════════════════════════╗\x1b[0m');
|
||
console.log('\x1b[1m\x1b[36m║\x1b[0m \x1b[1mMOXYTOOL\x1b[0m - Proxmox Administration \x1b[1m\x1b[36m║\x1b[0m');
|
||
console.log('\x1b[1m\x1b[36m╚════════════════════════════════════════════╝\x1b[0m');
|
||
console.log('');
|
||
console.log('\x1b[1mCommands:\x1b[0m');
|
||
console.log(' \x1b[36m►\x1b[0m vgpu-setup Install and configure Proxmox vGPU support');
|
||
console.log(' \x1b[36m►\x1b[0m scripts Manage Proxmox community scripts (400+)');
|
||
console.log(' \x1b[36m►\x1b[0m update Update MOXYTOOL to the latest version');
|
||
console.log('');
|
||
console.log('\x1b[2mUsage: moxytool <command> [options]\x1b[0m');
|
||
});
|
||
|
||
// vGPU setup command
|
||
smartcliInstance.addCommand('vgpu-setup').subscribe(async (argvArg) => {
|
||
logger.log('ok', 'Starting Proxmox vGPU setup process');
|
||
logger.log('info', 'This will install vGPU support for NVIDIA GPUs on Proxmox');
|
||
logger.log('info', '');
|
||
|
||
// Check for arguments
|
||
const step = argvArg.step;
|
||
const url = argvArg.url;
|
||
const file = argvArg.file;
|
||
const debug = argvArg.debug;
|
||
|
||
// Check if running on Proxmox
|
||
try {
|
||
const result = await smartshellInstance.exec('which pveversion');
|
||
if (result.exitCode !== 0) {
|
||
logger.log('error', 'This system does not appear to be running Proxmox');
|
||
logger.log('error', 'Please run this command on a Proxmox host');
|
||
Deno.exit(1);
|
||
}
|
||
} catch (e) {
|
||
logger.log('error', 'Failed to verify Proxmox installation');
|
||
logger.log('error', 'Please ensure you are running this on a Proxmox host');
|
||
Deno.exit(1);
|
||
}
|
||
|
||
// Create temporary directory
|
||
const tmpDir = '/tmp/moxytool-vgpu-setup';
|
||
await smartshellInstance.exec(`mkdir -p ${tmpDir}`);
|
||
|
||
try {
|
||
// Step 1: Clone and run the installer script
|
||
logger.log('info', 'Step 1: Downloading Proxmox vGPU installer (supports v9)...');
|
||
const cloneResult = await smartshellInstance.exec(
|
||
`cd ${tmpDir} && git clone https://github.com/anomixer/proxmox-vgpu-installer.git`,
|
||
);
|
||
|
||
if (cloneResult.exitCode !== 0) {
|
||
logger.log('error', 'Failed to clone the installer repository');
|
||
logger.log('error', cloneResult.stderr || 'Unknown error');
|
||
Deno.exit(1);
|
||
}
|
||
|
||
logger.log('ok', 'Installer downloaded successfully');
|
||
logger.log('info', '');
|
||
|
||
// Build command with arguments
|
||
let command = 'bash proxmox-installer.sh';
|
||
if (step) {
|
||
command += ` --step ${step}`;
|
||
}
|
||
if (url) {
|
||
command += ` --url ${url}`;
|
||
}
|
||
if (file) {
|
||
command += ` --file ${file}`;
|
||
}
|
||
if (debug) {
|
||
command += ' --debug';
|
||
}
|
||
|
||
logger.log('info', 'Running installer script...');
|
||
logger.log('info', 'Please follow the interactive prompts');
|
||
logger.log('info', '');
|
||
|
||
// Run the installer script interactively
|
||
const installResult = await smartshellInstance.exec(
|
||
`cd ${tmpDir}/proxmox-vgpu-installer && ${command}`,
|
||
);
|
||
|
||
if (installResult.exitCode !== 0) {
|
||
logger.log('error', 'Installer script failed');
|
||
logger.log('error', installResult.stderr || 'Unknown error');
|
||
Deno.exit(1);
|
||
}
|
||
|
||
logger.log('ok', 'vGPU setup process completed');
|
||
logger.log('info', '');
|
||
logger.log('info', 'Next steps:');
|
||
logger.log('info', '1. If prompted, reboot your system');
|
||
logger.log('info', '2. After reboot, run this command again to continue setup');
|
||
logger.log('info', '3. Verify installation with: mdevctl types');
|
||
logger.log('info', '4. Configure your VMs with vGPU in the Proxmox web UI');
|
||
} catch (error) {
|
||
logger.log('error', `Setup failed: ${error instanceof Error ? error.message : String(error)}`);
|
||
Deno.exit(1);
|
||
}
|
||
});
|
||
|
||
// Update command
|
||
smartcliInstance.addCommand('update').subscribe(async (argvArg) => {
|
||
logger.log('info', 'Checking for updates...');
|
||
logger.log('info', '');
|
||
|
||
try {
|
||
// Get current version from compile-time imported deno.json
|
||
const currentVersion = denoConfig.version;
|
||
|
||
// Fetch latest version from Gitea API
|
||
const apiUrl = 'https://code.foss.global/api/v1/repos/serve.zone/moxytool/releases/latest';
|
||
const response = await fetch(apiUrl);
|
||
|
||
if (!response.ok) {
|
||
logger.log('error', 'Failed to check for updates');
|
||
logger.log('error', `HTTP ${response.status}: ${response.statusText}`);
|
||
Deno.exit(1);
|
||
}
|
||
|
||
const release = await response.json();
|
||
const latestVersion = release.tag_name; // e.g., "v1.1.0"
|
||
|
||
// Normalize versions for comparison (ensure both have "v" prefix)
|
||
const normalizedCurrent = currentVersion.startsWith('v') ? currentVersion : `v${currentVersion}`;
|
||
const normalizedLatest = latestVersion.startsWith('v') ? latestVersion : `v${latestVersion}`;
|
||
|
||
logger.log('info', `Current version: ${normalizedCurrent}`);
|
||
logger.log('info', `Latest version: ${normalizedLatest}`);
|
||
logger.log('info', '');
|
||
|
||
// Compare normalized versions
|
||
if (normalizedCurrent === normalizedLatest) {
|
||
logger.log('success', 'Already up to date!');
|
||
logger.log('info', '');
|
||
return;
|
||
}
|
||
|
||
logger.log('ok', `New version available: ${latestVersion}`);
|
||
logger.log('info', 'Downloading and installing...');
|
||
logger.log('info', '');
|
||
|
||
// Download and run the install script
|
||
const installUrl = 'https://code.foss.global/serve.zone/moxytool/raw/branch/main/install.sh';
|
||
|
||
const updateResult = await smartshellInstance.exec(
|
||
`curl -sSL ${installUrl} | bash`
|
||
);
|
||
|
||
if (updateResult.exitCode !== 0) {
|
||
logger.log('error', 'Update failed');
|
||
logger.log('error', updateResult.stderr || 'Unknown error');
|
||
Deno.exit(1);
|
||
}
|
||
|
||
logger.log('info', '');
|
||
logger.log('success', `Updated to ${latestVersion}`);
|
||
logger.log('info', '');
|
||
} catch (error) {
|
||
logger.log('error', `Update failed: ${error instanceof Error ? error.message : String(error)}`);
|
||
Deno.exit(1);
|
||
}
|
||
});
|
||
|
||
// Scripts management commands
|
||
smartcliInstance.addCommand('scripts').subscribe(async (argvArg) => {
|
||
const subcommand = argvArg._[1]; // _[0] is 'scripts', _[1] is the subcommand
|
||
|
||
if (!subcommand) {
|
||
logger.log('info', 'MOXYTOOL Scripts - Proxmox Community Scripts Management');
|
||
logger.log('info', '');
|
||
logger.log('info', 'Available subcommands:');
|
||
logger.log('info', '* list - List all available scripts');
|
||
logger.log('info', '* search <query> - Search for scripts by name or description');
|
||
logger.log('info', '* info <slug> - Show detailed information about a script');
|
||
logger.log('info', '* run <slug> - Execute a script');
|
||
logger.log('info', '* refresh - Force refresh the script index');
|
||
logger.log('info', '');
|
||
logger.log('info', 'Usage: moxytool scripts <subcommand> [options]');
|
||
return;
|
||
}
|
||
|
||
// Ensure index is loaded
|
||
await scriptIndex.loadCache();
|
||
|
||
switch (subcommand) {
|
||
case 'list': {
|
||
const scripts = scriptIndex.getAll();
|
||
|
||
if (scripts.length === 0) {
|
||
logger.log('warn', 'No scripts found. Run "moxytool scripts refresh" to fetch the index.');
|
||
return;
|
||
}
|
||
|
||
const stats = scriptIndex.getStats();
|
||
logger.log('info', `Available Scripts (${stats.count} total, indexed ${stats.age})`);
|
||
logger.log('info', '');
|
||
|
||
// Group by type
|
||
const pveScripts = scripts.filter(s => s.type === 'pve');
|
||
const containers = scripts.filter(s => s.type === 'ct');
|
||
const vms = scripts.filter(s => s.type === 'vm');
|
||
const otherScripts = scripts.filter(s => s.type !== 'pve' && s.type !== 'ct' && s.type !== 'vm');
|
||
|
||
if (pveScripts.length > 0) {
|
||
logger.log('info', 'Proxmox VE Host Scripts:');
|
||
pveScripts.forEach(script => {
|
||
logger.log('info', ` • ${script.slug.padEnd(25)} - ${script.name}`);
|
||
});
|
||
logger.log('info', '');
|
||
}
|
||
|
||
if (containers.length > 0) {
|
||
logger.log('info', 'Containers (LXC):');
|
||
containers.forEach(script => {
|
||
logger.log('info', ` • ${script.slug.padEnd(25)} - ${script.name}`);
|
||
});
|
||
logger.log('info', '');
|
||
}
|
||
|
||
if (vms.length > 0) {
|
||
logger.log('info', 'Virtual Machines:');
|
||
vms.forEach(script => {
|
||
logger.log('info', ` • ${script.slug.padEnd(25)} - ${script.name}`);
|
||
});
|
||
logger.log('info', '');
|
||
}
|
||
|
||
if (otherScripts.length > 0) {
|
||
logger.log('info', 'Other:');
|
||
otherScripts.forEach(script => {
|
||
const slug = script.slug || 'unknown';
|
||
logger.log('info', ` • ${slug.padEnd(25)} - ${script.name} (${script.type})`);
|
||
});
|
||
}
|
||
|
||
logger.log('info', '');
|
||
logger.log('info', 'Use "moxytool scripts info <slug>" for more details');
|
||
logger.log('info', 'Use "moxytool scripts run <slug>" to install');
|
||
break;
|
||
}
|
||
|
||
case 'search': {
|
||
const query = argvArg._[2]; // _[0]=scripts, _[1]=search, _[2]=query
|
||
|
||
if (!query) {
|
||
logger.log('error', 'Please provide a search query');
|
||
logger.log('info', 'Usage: moxytool scripts search <query> [--filter type:vm]');
|
||
logger.log('info', 'Filters: type:vm, type:ct, type:pve, type:addon');
|
||
return;
|
||
}
|
||
|
||
// Parse filter option
|
||
let typeFilter: string | undefined;
|
||
if (argvArg.filter) {
|
||
const filterString = argvArg.filter as string;
|
||
if (filterString.startsWith('type:')) {
|
||
typeFilter = filterString.substring(5);
|
||
}
|
||
}
|
||
|
||
const results = scriptIndex.search(query as string, typeFilter);
|
||
|
||
if (results.length === 0) {
|
||
const filterMsg = typeFilter ? ` (filtered by type:${typeFilter})` : '';
|
||
logger.log('warn', `No scripts found matching "${query}"${filterMsg}`);
|
||
return;
|
||
}
|
||
|
||
const filterMsg = typeFilter ? ` \x1b[2m(filtered by type:${typeFilter})\x1b[0m` : '';
|
||
logger.log('info', `Found ${results.length} script(s) matching "\x1b[1m${query}\x1b[0m"${filterMsg}:`);
|
||
logger.log('info', '');
|
||
|
||
results.forEach(script => {
|
||
const slug = script.slug || 'unknown';
|
||
const description = script.description ? script.description.substring(0, 100) : 'No description available';
|
||
|
||
// Type badge with colors
|
||
let typeBadge = '';
|
||
if (script.type === 'ct') typeBadge = '\x1b[36m[LXC]\x1b[0m';
|
||
else if (script.type === 'vm') typeBadge = '\x1b[35m[VM]\x1b[0m';
|
||
else if (script.type === 'pve') typeBadge = '\x1b[33m[PVE]\x1b[0m';
|
||
else typeBadge = `\x1b[2m[${script.type}]\x1b[0m`;
|
||
|
||
logger.log('info', `\x1b[1m\x1b[36m►\x1b[0m \x1b[1m${slug}\x1b[0m ${typeBadge}`);
|
||
logger.log('info', ` \x1b[2m${script.name}\x1b[0m`);
|
||
logger.log('info', ` ${description}${script.description && script.description.length > 100 ? '...' : ''}`);
|
||
logger.log('info', '');
|
||
});
|
||
|
||
logger.log('info', '\x1b[2mUse "moxytool scripts info <slug>" for more details\x1b[0m');
|
||
break;
|
||
}
|
||
|
||
case 'info': {
|
||
const slug = argvArg._[2]; // _[0]=scripts, _[1]=info, _[2]=slug
|
||
|
||
if (!slug) {
|
||
logger.log('error', 'Please provide a script slug');
|
||
logger.log('info', 'Usage: moxytool scripts info <slug>');
|
||
return;
|
||
}
|
||
|
||
const script = scriptIndex.getBySlug(slug as string);
|
||
|
||
if (!script) {
|
||
logger.log('error', `Script "${slug}" not found`);
|
||
logger.log('info', 'Use "moxytool scripts search <query>" to find scripts');
|
||
return;
|
||
}
|
||
|
||
logger.log('info', '═'.repeat(60));
|
||
logger.log('info', `${script.name}`);
|
||
logger.log('info', '═'.repeat(60));
|
||
logger.log('info', '');
|
||
logger.log('info', `Slug: ${script.slug}`);
|
||
logger.log('info', `Type: ${script.type === 'ct' ? 'Container (LXC)' : 'Virtual Machine'}`);
|
||
logger.log('info', '');
|
||
logger.log('info', 'Description:');
|
||
logger.log('info', script.description);
|
||
logger.log('info', '');
|
||
|
||
if (script.install_methods && script.install_methods[0]?.resources) {
|
||
const res = script.install_methods[0].resources;
|
||
logger.log('info', 'Resource Requirements:');
|
||
if (res.cpu) logger.log('info', ` CPU: ${res.cpu} cores`);
|
||
if (res.ram) logger.log('info', ` RAM: ${res.ram} MB`);
|
||
if (res.hdd) logger.log('info', ` Disk: ${res.hdd} GB`);
|
||
if (res.os) logger.log('info', ` OS: ${res.os} ${res.version || ''}`);
|
||
logger.log('info', '');
|
||
}
|
||
|
||
if (script.interface_port) {
|
||
logger.log('info', `Web Interface: http://<your-ip>:${script.interface_port}`);
|
||
logger.log('info', '');
|
||
}
|
||
|
||
if (script.default_credentials) {
|
||
logger.log('info', 'Default Credentials:');
|
||
if (script.default_credentials.username) {
|
||
logger.log('info', ` Username: ${script.default_credentials.username}`);
|
||
}
|
||
if (script.default_credentials.password) {
|
||
logger.log('info', ` Password: ${script.default_credentials.password}`);
|
||
}
|
||
logger.log('info', '');
|
||
}
|
||
|
||
if (script.notes && script.notes.length > 0) {
|
||
logger.log('info', 'Important Notes:');
|
||
script.notes.forEach(note => {
|
||
const prefix = note.type === 'warning' ? '⚠️ ' : 'ℹ️ ';
|
||
logger.log('warn', `${prefix}${note.content}`);
|
||
});
|
||
logger.log('info', '');
|
||
}
|
||
|
||
if (script.documentation) {
|
||
logger.log('info', `Documentation: ${script.documentation}`);
|
||
}
|
||
if (script.website) {
|
||
logger.log('info', `Website: ${script.website}`);
|
||
}
|
||
|
||
logger.log('info', '');
|
||
logger.log('info', `To install: sudo moxytool scripts run ${script.slug}`);
|
||
logger.log('info', '═'.repeat(60));
|
||
break;
|
||
}
|
||
|
||
case 'run': {
|
||
const slug = argvArg._[2]; // _[0]=scripts, _[1]=run, _[2]=slug
|
||
|
||
if (!slug) {
|
||
logger.log('error', 'Please provide a script slug');
|
||
logger.log('info', 'Usage: sudo moxytool scripts run <slug>');
|
||
return;
|
||
}
|
||
|
||
const script = scriptIndex.getBySlug(slug as string);
|
||
|
||
if (!script) {
|
||
logger.log('error', `Script "${slug}" not found`);
|
||
logger.log('info', 'Use "moxytool scripts search <query>" to find scripts');
|
||
return;
|
||
}
|
||
|
||
// Validate Proxmox host
|
||
const runner = new ScriptRunner();
|
||
const isProxmox = await runner.validateProxmoxHost();
|
||
|
||
if (!isProxmox) {
|
||
logger.log('error', 'This system does not appear to be running Proxmox');
|
||
logger.log('error', 'Community scripts can only be run on Proxmox hosts');
|
||
Deno.exit(1);
|
||
}
|
||
|
||
// Execute the script
|
||
const exitCode = await runner.execute(script);
|
||
Deno.exit(exitCode);
|
||
}
|
||
|
||
case 'refresh': {
|
||
logger.log('info', 'Refreshing script index...');
|
||
|
||
try {
|
||
await scriptIndex.fetchIndex();
|
||
const stats = scriptIndex.getStats();
|
||
logger.log('success', `Index refreshed: ${stats.count} scripts cached`);
|
||
} catch (error) {
|
||
logger.log('error', `Failed to refresh index: ${error}`);
|
||
Deno.exit(1);
|
||
}
|
||
break;
|
||
}
|
||
|
||
default:
|
||
logger.log('error', `Unknown subcommand: ${subcommand}`);
|
||
logger.log('info', 'Run "moxytool scripts" to see available subcommands');
|
||
}
|
||
});
|
||
|
||
smartcliInstance.startParse();
|
||
};
|