feat(scripts): Add community scripts subsystem: script index, runner, and CLI commands with background refresh; update docs and paths
This commit is contained in:
41
changelog.md
41
changelog.md
@@ -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
5
mod.ts
@@ -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';
|
||||||
|
|||||||
48
readme.md
48
readme.md
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
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 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();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user