diff --git a/changelog.md b/changelog.md index 90c82e4..cebadb1 100644 --- a/changelog.md +++ b/changelog.md @@ -1,21 +1,43 @@ # Changelog -## 2025-10-27 - 1.1.0 - feat(cli) -Add initial MOXYTOOL implementation, packaging, install/uninstall scripts, CI and release workflows +## 2025-10-27 - 1.2.0 - feat(scripts) +Add community scripts subsystem: script index, runner, and CLI commands with background refresh; update docs and paths -- Add core CLI implementation (mod.ts and ts/): vgpu-setup command, logging, paths and plugins integration -- Add Deno config (deno.json) and build/test tasks -- Add compilation and packaging scripts (scripts/compile-all.sh, scripts/install-binary.js) and binary wrapper (bin/moxytool-wrapper.js) -- Add installer and uninstaller scripts (install.sh, uninstall.sh) for easy deployment -- Add CI, build and release workflows (.gitea/workflows/) including multi-platform compilation and npm publish steps -- Add documentation and metadata: readme.md, changelog.md, package.json and license -- Add .gitignore and dist/binaries handling, plus release checksum generation in workflows +- New `scripts` command with subcommands: list, search, info, run, refresh (implemented in ts/moxytool.cli.ts) +- Added ScriptIndex (ts/moxytool.classes.scriptindex.ts) to fetch and cache ~400 community scripts with a 24h TTL and background refresh +- Added ScriptRunner (ts/moxytool.classes.scriptrunner.ts) to execute community installation scripts interactively via bash/curl +- Background index refresh at startup and explicit refresh command; cache saved under /etc/moxytool/scripts +- README and changelog updated with scripts usage and features; Proxmox support range updated to 7.4-9.x +- Updated module exports in mod.ts and minor logging change in ts/index.ts +- Added script-related paths (scriptsCacheDir, scriptsIndexFile) to ts/moxytool.paths.ts All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.0] - 2025-01-27 + +### Added +- `scripts` command for Proxmox community scripts management +- Access to 400+ community-maintained installation scripts +- Automatic daily index updates with local caching +- Script search and filtering capabilities +- Interactive script execution with full stdin/stdout/stderr passthrough +- Support for both LXC containers and VM templates +- Script metadata display (requirements, ports, credentials) + +### Features +- `moxytool scripts list` - List all available scripts +- `moxytool scripts search ` - Search scripts by keyword +- `moxytool scripts info ` - View detailed script information +- `moxytool scripts run ` - Execute installation scripts +- `moxytool scripts refresh` - Force update the script index + +### Changed +- Updated Proxmox version support to 7.4-9.x (from 7.4-8.x) +- Updated vGPU installer to anomixer fork with Proxmox v9 support + ## [1.0.0] - 2025-01-24 ### Added @@ -33,4 +55,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Step-by-step installation process - Verification of Proxmox installation before setup +[1.1.0]: https://code.foss.global/serve.zone/moxytool/releases/tag/v1.1.0 [1.0.0]: https://code.foss.global/serve.zone/moxytool/releases/tag/v1.0.0 diff --git a/mod.ts b/mod.ts index cf4c611..aef2d8c 100644 --- a/mod.ts +++ b/mod.ts @@ -44,5 +44,6 @@ if (import.meta.main) { } } -// Export for library usage -export * from './ts/index.ts'; +// Export for library usage (Deno modules only) +export * from './ts/moxytool.cli.ts'; +export { logger } from './ts/moxytool.logging.ts'; diff --git a/readme.md b/readme.md index ad27e3b..81d545b 100644 --- a/readme.md +++ b/readme.md @@ -90,12 +90,50 @@ After successful installation: 2. **Configure VMs**: Add vGPU devices in Proxmox web UI (VM → Hardware → Add → PCI Device) 3. **Install guest drivers**: Download and install NVIDIA vGPU guest drivers in your VMs +### Community Scripts + +Access and deploy 400+ community-maintained Proxmox installation scripts: + +```bash +# List all available scripts +moxytool scripts list + +# Search for specific applications +moxytool scripts search docker +moxytool scripts search homeassistant + +# View detailed information +moxytool scripts info docker + +# Install a script +sudo moxytool scripts run docker + +# Refresh the script index +moxytool scripts refresh +``` + +**Features:** +- Automatic daily index updates (cached locally) +- 400+ LXC containers and VM templates +- Full interactive installation support +- Applications include: Docker, Jellyfin, Home Assistant, Pi-hole, Nextcloud, and many more + +**Script Categories:** +- Containerization (Docker, Podman, Kubernetes) +- Media servers (Plex, Jellyfin, Emby) +- Home automation (Home Assistant, Node-RED) +- Development tools (GitLab, Jenkins, Gitea) +- Network tools (Pi-hole, AdGuard, WireGuard) +- Databases (PostgreSQL, MariaDB, MongoDB) +- And much more... + ## Requirements -- Proxmox VE 7.4+ or 8.x -- NVIDIA GPU with vGPU support +- Proxmox VE 7.4-9.x - Root/sudo access -- Internet connection for downloading drivers +- Internet connection for downloading scripts/drivers + +**Note:** The tool comes as a pre-compiled binary - no runtime dependencies needed! ## Supported Platforms @@ -105,9 +143,11 @@ After successful installation: ## Development +**Note:** Development requires Deno. End users don't need Deno - they use pre-compiled binaries. + ### Prerequisites -- Deno 1.x or later +- Deno 2.x or later - Bash (for compilation scripts) ### Building from Source diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 0ee3c67..0fd0aaa 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/moxytool', - version: '1.1.0', + version: '1.2.0', description: 'Proxmox administration tool for vGPU setup, VM management, and cluster configuration' } diff --git a/ts/index.ts b/ts/index.ts index 13a28dd..b0ed6db 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -18,8 +18,9 @@ async function main() { } // Run the main function and handle any errors +// Note: This file is only used as the Node.js entry point main().catch((error) => { - logger.error(`Error: ${error}`); + logger.log('error', `Error: ${error}`); process.exit(1); }); diff --git a/ts/moxytool.classes.scriptindex.ts b/ts/moxytool.classes.scriptindex.ts new file mode 100644 index 0000000..c731cca --- /dev/null +++ b/ts/moxytool.classes.scriptindex.ts @@ -0,0 +1,314 @@ +import * as plugins from './moxytool.plugins.ts'; +import * as paths from './moxytool.paths.ts'; +import { logger } from './moxytool.logging.ts'; + +/** + * Interface for script metadata from JSON files + */ +export interface IScriptMetadata { + name: string; + slug: string; + type: string; + categories: number[]; + description: string; + install_methods: Array<{ + type: string; + script: string; + resources: { + cpu?: number; + ram?: number; + hdd?: number; + os?: string; + version?: string; + }; + }>; + interface_port?: number; + config_path?: string; + documentation?: string; + website?: string; + logo?: string; + default_credentials?: { + username?: string; + password?: string; + }; + notes?: Array<{ + type: string; + content: string; + }>; + updateable?: boolean; + privileged?: boolean; + date_created?: string; +} + +/** + * Interface for the cached index + */ +export interface IScriptIndexCache { + lastUpdated: number; + scripts: IScriptMetadata[]; +} + +/** + * ScriptIndex class manages the Proxmox community scripts index + * - Fetches JSON metadata from Gitea + * - Caches locally with 24-hour TTL + * - Provides search and filtering capabilities + */ +export class ScriptIndex { + private static readonly BASE_URL = + 'https://code.foss.global/asset_backups/ProxmoxVE/raw/branch/main/frontend/public/json'; + private static readonly INDEX_LIST_URL = + 'https://code.foss.global/asset_backups/ProxmoxVE/src/branch/main/frontend/public/json'; + private static readonly CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours in ms + + private cache: IScriptIndexCache | null = null; + + /** + * Check if the index needs to be refreshed (>24 hours old) + */ + public async needsRefresh(): Promise { + try { + // Try to load from cache + await this.loadCache(); + + if (!this.cache) { + return true; // No cache, need refresh + } + + const age = Date.now() - this.cache.lastUpdated; + return age > ScriptIndex.CACHE_TTL; + } catch (error) { + logger.log('warn', `Error checking cache age: ${error}`); + return true; // On error, refresh + } + } + + /** + * Load the index from local cache + */ + public async loadCache(): Promise { + try { + if (!Deno) { + throw new Error('Deno runtime not available'); + } + + const cacheFile = paths.scriptsIndexFile; + + // Check if cache file exists + try { + await Deno.stat(cacheFile); + } catch { + // File doesn't exist + this.cache = null; + return; + } + + // Read and parse cache + const content = await Deno.readTextFile(cacheFile); + this.cache = JSON.parse(content) as IScriptIndexCache; + + logger.log('info', `Loaded ${this.cache.scripts.length} scripts from cache`); + } catch (error) { + logger.log('warn', `Error loading cache: ${error}`); + this.cache = null; + } + } + + /** + * Fetch the index from Gitea and update cache + */ + public async fetchIndex(): Promise { + try { + logger.log('info', 'Fetching script index from Gitea...'); + + // First, get the list of all JSON files + const fileList = await this.fetchFileList(); + + if (fileList.length === 0) { + throw new Error('No JSON files found in repository'); + } + + logger.log('info', `Found ${fileList.length} script definitions`); + + // Fetch all JSON files + const scripts: IScriptMetadata[] = []; + let successCount = 0; + let errorCount = 0; + + for (const filename of fileList) { + try { + const script = await this.fetchScript(filename); + if (script) { + scripts.push(script); + successCount++; + } + } catch (error) { + errorCount++; + logger.log('warn', `Failed to fetch ${filename}: ${error}`); + } + } + + logger.log('info', `Successfully fetched ${successCount} scripts (${errorCount} errors)`); + + // Update cache + this.cache = { + lastUpdated: Date.now(), + scripts, + }; + + // Save to disk + await this.saveCache(); + + logger.log('success', `Index refreshed: ${scripts.length} scripts cached`); + } catch (error) { + logger.log('error', `Failed to fetch index: ${error}`); + throw error; + } + } + + /** + * Fetch the list of JSON files from the repository + */ + private async fetchFileList(): Promise { + try { + // Simple approach: fetch a known comprehensive list + // In production, you'd parse the directory listing or use the API + // For now, we'll use a hardcoded list of common scripts + // TODO: Implement proper directory listing scraping or use Gitea API + + // Fetch the directory listing page + const response = await fetch(ScriptIndex.INDEX_LIST_URL); + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const html = await response.text(); + + // Extract .json filenames from HTML + const jsonFileRegex = /href="[^"]*\/([^"\/]+\.json)"/g; + const matches = [...html.matchAll(jsonFileRegex)]; + const files = matches.map((match) => match[1]).filter((file) => file.endsWith('.json')); + + // Remove duplicates + return [...new Set(files)]; + } catch (error) { + logger.log('error', `Failed to fetch file list: ${error}`); + throw error; + } + } + + /** + * Fetch a single script JSON file + */ + private async fetchScript(filename: string): Promise { + try { + const url = `${ScriptIndex.BASE_URL}/${filename}`; + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + const data = await response.json(); + return data as IScriptMetadata; + } catch (error) { + logger.log('warn', `Failed to fetch ${filename}: ${error}`); + return null; + } + } + + /** + * Save the cache to disk + */ + private async saveCache(): Promise { + try { + if (!this.cache) { + return; + } + + // Ensure cache directory exists + await Deno.mkdir(paths.scriptsCacheDir, { recursive: true }); + + // Write cache file + const content = JSON.stringify(this.cache, null, 2); + await Deno.writeTextFile(paths.scriptsIndexFile, content); + + logger.log('info', 'Cache saved successfully'); + } catch (error) { + logger.log('error', `Failed to save cache: ${error}`); + throw error; + } + } + + /** + * Get all scripts from cache + */ + public getAll(): IScriptMetadata[] { + if (!this.cache) { + return []; + } + return this.cache.scripts; + } + + /** + * Search scripts by query string + */ + public search(query: string): IScriptMetadata[] { + if (!this.cache) { + return []; + } + + const lowerQuery = query.toLowerCase(); + + return this.cache.scripts.filter((script) => { + // Search in name, description, and slug + return ( + script.name.toLowerCase().includes(lowerQuery) || + script.slug.toLowerCase().includes(lowerQuery) || + script.description.toLowerCase().includes(lowerQuery) + ); + }); + } + + /** + * Get a script by slug + */ + public getBySlug(slug: string): IScriptMetadata | null { + if (!this.cache) { + return null; + } + + return this.cache.scripts.find((script) => script.slug === slug) || null; + } + + /** + * Filter scripts by type (ct/vm) + */ + public filterByType(type: string): IScriptMetadata[] { + if (!this.cache) { + return []; + } + + return this.cache.scripts.filter((script) => script.type === type); + } + + /** + * Get cache statistics + */ + public getStats(): { count: number; lastUpdated: Date | null; age: string } { + if (!this.cache) { + return { count: 0, lastUpdated: null, age: 'never' }; + } + + const now = Date.now(); + const age = now - this.cache.lastUpdated; + const hours = Math.floor(age / (60 * 60 * 1000)); + const minutes = Math.floor((age % (60 * 60 * 1000)) / (60 * 1000)); + + return { + count: this.cache.scripts.length, + lastUpdated: new Date(this.cache.lastUpdated), + age: hours > 0 ? `${hours}h ${minutes}m ago` : `${minutes}m ago`, + }; + } +} diff --git a/ts/moxytool.classes.scriptrunner.ts b/ts/moxytool.classes.scriptrunner.ts new file mode 100644 index 0000000..86edd5b --- /dev/null +++ b/ts/moxytool.classes.scriptrunner.ts @@ -0,0 +1,170 @@ +import * as plugins from './moxytool.plugins.ts'; +import { logger } from './moxytool.logging.ts'; +import type { IScriptMetadata } from './moxytool.classes.scriptindex.ts'; + +/** + * ScriptRunner class handles the execution of Proxmox community scripts + * - Executes scripts via bash with curl + * - Ensures proper stdin/stdout/stderr passthrough for interactive prompts + * - Handles script exit codes + */ +export class ScriptRunner { + private static readonly SCRIPT_BASE_URL = + 'https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main'; + + /** + * Execute a community script + * @param script The script metadata + * @returns The exit code of the script + */ + public async execute(script: IScriptMetadata): Promise { + try { + // Get the script URL from install_methods + if (!script.install_methods || script.install_methods.length === 0) { + logger.log('error', 'Script has no install methods defined'); + return 1; + } + + const installMethod = script.install_methods[0]; + const scriptPath = installMethod.script; + + if (!scriptPath) { + logger.log('error', 'Script path is not defined'); + return 1; + } + + // Construct the full script URL + const scriptUrl = `${ScriptRunner.SCRIPT_BASE_URL}${scriptPath}`; + + logger.log('info', `Executing script: ${script.name}`); + logger.log('info', `URL: ${scriptUrl}`); + logger.log('info', ''); + + // Show script details + if (script.description) { + logger.log('info', `Description: ${script.description}`); + } + + if (script.notes && script.notes.length > 0) { + logger.log('info', ''); + logger.log('info', 'Important Notes:'); + for (const note of script.notes) { + const prefix = note.type === 'warning' ? '⚠️ ' : 'ℹ️ '; + logger.log('warn', `${prefix}${note.content}`); + } + } + + if (installMethod.resources) { + logger.log('info', ''); + logger.log('info', 'Resource Requirements:'); + if (installMethod.resources.cpu) { + logger.log('info', ` CPU: ${installMethod.resources.cpu} cores`); + } + if (installMethod.resources.ram) { + logger.log('info', ` RAM: ${installMethod.resources.ram} MB`); + } + if (installMethod.resources.hdd) { + logger.log('info', ` Disk: ${installMethod.resources.hdd} GB`); + } + if (installMethod.resources.os) { + logger.log( + 'info', + ` OS: ${installMethod.resources.os} ${installMethod.resources.version || ''}`, + ); + } + } + + logger.log('info', ''); + logger.log('info', 'Starting installation...'); + logger.log('info', '═'.repeat(60)); + logger.log('info', ''); + + // Execute the script using smartshell + // The command structure: bash -c "$(curl -fsSL )" + const smartshellInstance = new plugins.smartshell.Smartshell({ + executor: 'bash', + }); + + // Construct the command that will be executed + const command = `bash -c "$(curl -fsSL ${scriptUrl})"`; + + // Execute with inherited stdio for full interactivity + const result = await smartshellInstance.exec(command); + + logger.log('info', ''); + logger.log('info', '═'.repeat(60)); + + if (result.exitCode === 0) { + logger.log('success', `✓ Script completed successfully`); + + if (script.interface_port) { + logger.log('info', ''); + logger.log('info', `Access the service at: http://:${script.interface_port}`); + } + + if (script.default_credentials) { + logger.log('info', ''); + 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}`); + } + } + + if (script.documentation) { + logger.log('info', ''); + logger.log('info', `Documentation: ${script.documentation}`); + } + } else { + logger.log('error', `✗ Script failed with exit code: ${result.exitCode}`); + if (result.stderr) { + logger.log('error', `Error output: ${result.stderr}`); + } + } + + return result.exitCode; + } catch (error) { + logger.log('error', `Failed to execute script: ${error}`); + return 1; + } + } + + /** + * Validate that we're running on a Proxmox host + */ + public async validateProxmoxHost(): Promise { + try { + const smartshellInstance = new plugins.smartshell.Smartshell({ + executor: 'bash', + }); + + const result = await smartshellInstance.exec('which pveversion'); + return result.exitCode === 0; + } catch { + return false; + } + } + + /** + * Get Proxmox version information + */ + public async getProxmoxVersion(): Promise { + try { + const smartshellInstance = new plugins.smartshell.Smartshell({ + executor: 'bash', + }); + + const result = await smartshellInstance.exec('pveversion'); + + if (result.exitCode === 0 && result.stdout) { + return result.stdout.trim(); + } + + return null; + } catch { + return null; + } + } +} diff --git a/ts/moxytool.cli.ts b/ts/moxytool.cli.ts index 5ab222f..8c17f86 100644 --- a/ts/moxytool.cli.ts +++ b/ts/moxytool.cli.ts @@ -1,6 +1,8 @@ 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'; export const runCli = async () => { const smartshellInstance = new plugins.smartshell.Smartshell({ @@ -9,12 +11,31 @@ export const runCli = async () => { const smartcliInstance = new plugins.smartcli.Smartcli(); + // 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 () => { logger.log('info', 'MOXYTOOL - Proxmox Administration Tool'); logger.log('info', ''); logger.log('info', 'Available commands:'); logger.log('info', '* vgpu-setup - Install and configure Proxmox vGPU support'); + logger.log('info', '* scripts - Manage Proxmox community scripts'); logger.log('info', ''); logger.log('info', 'Usage: moxytool [options]'); }); @@ -108,5 +129,222 @@ export const runCli = async () => { } }); + // Scripts management commands + smartcliInstance.addCommand('scripts').subscribe(async (argvArg) => { + const subcommand = argvArg._[0]; + + 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 - Search for scripts by name or description'); + logger.log('info', '* info - Show detailed information about a script'); + logger.log('info', '* run - Execute a script'); + logger.log('info', '* refresh - Force refresh the script index'); + logger.log('info', ''); + logger.log('info', 'Usage: moxytool scripts [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 containers = scripts.filter(s => s.type === 'ct'); + const vms = scripts.filter(s => s.type === 'vm'); + + 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', ''); + logger.log('info', 'Use "moxytool scripts info " for more details'); + logger.log('info', 'Use "moxytool scripts run " to install'); + break; + } + + case 'search': { + const query = argvArg._[1]; + + if (!query) { + logger.log('error', 'Please provide a search query'); + logger.log('info', 'Usage: moxytool scripts search '); + return; + } + + const results = scriptIndex.search(query as string); + + if (results.length === 0) { + logger.log('warn', `No scripts found matching "${query}"`); + return; + } + + logger.log('info', `Found ${results.length} script(s) matching "${query}":`); + logger.log('info', ''); + + results.forEach(script => { + logger.log('info', `${script.slug} (${script.type})`); + logger.log('info', ` ${script.name}`); + logger.log('info', ` ${script.description.substring(0, 80)}...`); + logger.log('info', ''); + }); + + logger.log('info', 'Use "moxytool scripts info " for more details'); + break; + } + + case 'info': { + const slug = argvArg._[1]; + + if (!slug) { + logger.log('error', 'Please provide a script slug'); + logger.log('info', 'Usage: moxytool scripts info '); + return; + } + + const script = scriptIndex.getBySlug(slug as string); + + if (!script) { + logger.log('error', `Script "${slug}" not found`); + logger.log('info', 'Use "moxytool scripts search " 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://:${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._[1]; + + if (!slug) { + logger.log('error', 'Please provide a script slug'); + logger.log('info', 'Usage: sudo moxytool scripts run '); + return; + } + + const script = scriptIndex.getBySlug(slug as string); + + if (!script) { + logger.log('error', `Script "${slug}" not found`); + logger.log('info', 'Use "moxytool scripts search " 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(); }; diff --git a/ts/moxytool.paths.ts b/ts/moxytool.paths.ts index 76d145a..023abf7 100644 --- a/ts/moxytool.paths.ts +++ b/ts/moxytool.paths.ts @@ -19,3 +19,18 @@ export const logDir = plugins.path.join(dataDir, 'logs'); * Temporary working directory */ export const tmpDir = plugins.path.join(dataDir, 'tmp'); + +/** + * Scripts cache directory + */ +export const scriptsCacheDir = plugins.path.join(dataDir, 'scripts'); + +/** + * Scripts index cache file + */ +export const scriptsIndexFile = plugins.path.join(scriptsCacheDir, 'index.json'); + +/** + * Last index time tracker file + */ +export const scriptsLastIndexFile = plugins.path.join(scriptsCacheDir, 'last-index-time');