feat(scripts): Add community scripts subsystem: script index, runner, and CLI commands with background refresh; update docs and paths

This commit is contained in:
2025-10-27 17:29:52 +00:00
parent a289bcc413
commit fc22e1dd88
9 changed files with 819 additions and 17 deletions

View File

@@ -1,21 +1,43 @@
# Changelog # Changelog
## 2025-10-27 - 1.1.0 - feat(cli) ## 2025-10-27 - 1.2.0 - feat(scripts)
Add initial MOXYTOOL implementation, packaging, install/uninstall scripts, CI and release workflows 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 - New `scripts` command with subcommands: list, search, info, run, refresh (implemented in ts/moxytool.cli.ts)
- Add Deno config (deno.json) and build/test tasks - Added ScriptIndex (ts/moxytool.classes.scriptindex.ts) to fetch and cache ~400 community scripts with a 24h TTL and background refresh
- Add compilation and packaging scripts (scripts/compile-all.sh, scripts/install-binary.js) and binary wrapper (bin/moxytool-wrapper.js) - Added ScriptRunner (ts/moxytool.classes.scriptrunner.ts) to execute community installation scripts interactively via bash/curl
- Add installer and uninstaller scripts (install.sh, uninstall.sh) for easy deployment - Background index refresh at startup and explicit refresh command; cache saved under /etc/moxytool/scripts
- Add CI, build and release workflows (.gitea/workflows/) including multi-platform compilation and npm publish steps - README and changelog updated with scripts usage and features; Proxmox support range updated to 7.4-9.x
- Add documentation and metadata: readme.md, changelog.md, package.json and license - Updated module exports in mod.ts and minor logging change in ts/index.ts
- Add .gitignore and dist/binaries handling, plus release checksum generation in workflows - Added script-related paths (scriptsCacheDir, scriptsIndexFile) to ts/moxytool.paths.ts
All notable changes to this project will be documented in this file. 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/), 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). 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 <query>` - Search scripts by keyword
- `moxytool scripts info <slug>` - View detailed script information
- `moxytool scripts run <slug>` - 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 ## [1.0.0] - 2025-01-24
### Added ### Added
@@ -33,4 +55,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Step-by-step installation process - Step-by-step installation process
- Verification of Proxmox installation before setup - 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 [1.0.0]: https://code.foss.global/serve.zone/moxytool/releases/tag/v1.0.0

5
mod.ts
View File

@@ -44,5 +44,6 @@ if (import.meta.main) {
} }
} }
// Export for library usage // Export for library usage (Deno modules only)
export * from './ts/index.ts'; export * from './ts/moxytool.cli.ts';
export { logger } from './ts/moxytool.logging.ts';

View File

@@ -90,12 +90,50 @@ After successful installation:
2. **Configure VMs**: Add vGPU devices in Proxmox web UI (VM → Hardware → Add → PCI Device) 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 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 ## Requirements
- Proxmox VE 7.4+ or 8.x - Proxmox VE 7.4-9.x
- NVIDIA GPU with vGPU support
- Root/sudo access - 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 ## Supported Platforms
@@ -105,9 +143,11 @@ After successful installation:
## Development ## Development
**Note:** Development requires Deno. End users don't need Deno - they use pre-compiled binaries.
### Prerequisites ### Prerequisites
- Deno 1.x or later - Deno 2.x or later
- Bash (for compilation scripts) - Bash (for compilation scripts)
### Building from Source ### Building from Source

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/moxytool', name: '@serve.zone/moxytool',
version: '1.1.0', version: '1.2.0',
description: 'Proxmox administration tool for vGPU setup, VM management, and cluster configuration' description: 'Proxmox administration tool for vGPU setup, VM management, and cluster configuration'
} }

View File

@@ -18,8 +18,9 @@ async function main() {
} }
// Run the main function and handle any errors // Run the main function and handle any errors
// Note: This file is only used as the Node.js entry point
main().catch((error) => { main().catch((error) => {
logger.error(`Error: ${error}`); logger.log('error', `Error: ${error}`);
process.exit(1); process.exit(1);
}); });

View File

@@ -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<boolean> {
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<void> {
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<void> {
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<string[]> {
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<IScriptMetadata | null> {
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<void> {
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`,
};
}
}

View File

@@ -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<number> {
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 <url>)"
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://<your-ip>:${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<boolean> {
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<string | null> {
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;
}
}
}

View File

@@ -1,6 +1,8 @@
import * as plugins from './moxytool.plugins.ts'; import * as plugins from './moxytool.plugins.ts';
import * as paths from './moxytool.paths.ts'; import * as paths from './moxytool.paths.ts';
import { logger } from './moxytool.logging.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 () => { export const runCli = async () => {
const smartshellInstance = new plugins.smartshell.Smartshell({ const smartshellInstance = new plugins.smartshell.Smartshell({
@@ -9,12 +11,31 @@ export const runCli = async () => {
const smartcliInstance = new plugins.smartcli.Smartcli(); 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) // Standard command (no arguments)
smartcliInstance.standardCommand().subscribe(async () => { smartcliInstance.standardCommand().subscribe(async () => {
logger.log('info', 'MOXYTOOL - Proxmox Administration Tool'); logger.log('info', 'MOXYTOOL - Proxmox Administration Tool');
logger.log('info', ''); logger.log('info', '');
logger.log('info', 'Available commands:'); logger.log('info', 'Available commands:');
logger.log('info', '* vgpu-setup - Install and configure Proxmox vGPU support'); 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', '');
logger.log('info', 'Usage: moxytool <command> [options]'); logger.log('info', 'Usage: moxytool <command> [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 <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 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 <slug>" for more details');
logger.log('info', 'Use "moxytool scripts run <slug>" 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 <query>');
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 <slug>" 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 <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._[1];
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(); smartcliInstance.startParse();
}; };

View File

@@ -19,3 +19,18 @@ export const logDir = plugins.path.join(dataDir, 'logs');
* Temporary working directory * Temporary working directory
*/ */
export const tmpDir = plugins.path.join(dataDir, 'tmp'); 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');