feat(scripts): Add community scripts subsystem: script index, runner, and CLI commands with background refresh; update docs and paths
This commit is contained in:
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
314
ts/moxytool.classes.scriptindex.ts
Normal file
314
ts/moxytool.classes.scriptindex.ts
Normal 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`,
|
||||
};
|
||||
}
|
||||
}
|
||||
170
ts/moxytool.classes.scriptrunner.ts
Normal file
170
ts/moxytool.classes.scriptrunner.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <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();
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user