18 Commits

Author SHA1 Message Date
626cfe30ba 1.4.0
Some checks failed
CI / Type Check & Lint (push) Successful in 43s
Publish to npm / npm-publish (push) Failing after 1m1s
Release / build-and-release (push) Successful in 3m26s
CI / Build Test (Current Platform) (push) Successful in 3m42s
CI / Build All Platforms (push) Failing after 6m35s
2025-10-28 19:03:13 +00:00
45ac9af405 feat(cli): Improve CLI output and logging with colored header, grouped script listings, and ANSI-styled logger 2025-10-28 19:03:13 +00:00
d832343b38 1.3.6
Some checks failed
Publish to npm / npm-publish (push) Failing after 4s
CI / Type Check & Lint (push) Successful in 40s
Release / build-and-release (push) Successful in 2m8s
CI / Build All Platforms (push) Successful in 2m21s
CI / Build Test (Current Platform) (push) Successful in 2m26s
2025-10-28 18:39:51 +00:00
b38c99aaa0 fix(deps): Bump smartcli dependency and add local settings file 2025-10-28 18:39:51 +00:00
e379d60c65 1.3.5
Some checks failed
CI / Type Check & Lint (push) Failing after 5s
CI / Build Test (Current Platform) (push) Failing after 4s
CI / Build All Platforms (push) Failing after 4s
Publish to npm / npm-publish (push) Failing after 5s
Release / build-and-release (push) Failing after 3s
2025-10-28 18:36:25 +00:00
99b68c9bb0 fix(smartcli): Bump @push.rocks/smartcli to ^4.0.18 and add local settings file for tooling permissions 2025-10-28 18:36:24 +00:00
32d38975ec 1.3.4
Some checks failed
CI / Type Check & Lint (push) Successful in 17s
Publish to npm / npm-publish (push) Failing after 43s
CI / Build Test (Current Platform) (push) Successful in 55s
CI / Build All Platforms (push) Successful in 1m36s
Release / build-and-release (push) Successful in 1m38s
2025-10-28 15:06:39 +00:00
4ac2d99c32 fix(smartcli): Update @push.rocks/smartcli to ^4.0.16 and add local Claude settings 2025-10-28 15:06:39 +00:00
9265670e63 1.3.3
Some checks failed
CI / Type Check & Lint (push) Successful in 38s
Publish to npm / npm-publish (push) Failing after 58s
CI / Build Test (Current Platform) (push) Successful in 1m10s
CI / Build All Platforms (push) Successful in 1m57s
Release / build-and-release (push) Successful in 1m57s
2025-10-28 14:06:15 +00:00
9341b9cd16 fix(deno.json): Bump @push.rocks/smartcli to ^4.0.15 2025-10-28 14:06:15 +00:00
8e6418a574 1.3.2
Some checks failed
CI / Type Check & Lint (push) Successful in 38s
Publish to npm / npm-publish (push) Failing after 1m0s
CI / Build Test (Current Platform) (push) Successful in 1m11s
CI / Build All Platforms (push) Successful in 2m0s
Release / build-and-release (push) Successful in 2m4s
2025-10-28 12:36:50 +00:00
33609bff9a fix(cli): Correct scripts subcommand argument parsing and bump smartcli dependency 2025-10-28 12:36:50 +00:00
ef7bab3e32 1.3.1
Some checks failed
CI / Type Check & Lint (push) Successful in 18s
Publish to npm / npm-publish (push) Failing after 39s
CI / Build Test (Current Platform) (push) Successful in 46s
CI / Build All Platforms (push) Successful in 1m35s
Release / build-and-release (push) Successful in 1m35s
2025-10-27 17:38:30 +00:00
723dca735f fix(publish): Switch publish registry to internal Verdaccio instance and add local CI settings 2025-10-27 17:38:29 +00:00
dc868e3fbc 1.3.0
Some checks failed
CI / Type Check & Lint (push) Successful in 40s
Publish to npm / npm-publish (push) Failing after 1m1s
CI / Build Test (Current Platform) (push) Successful in 1m7s
CI / Build All Platforms (push) Successful in 1m52s
Release / build-and-release (push) Successful in 1m57s
2025-10-27 17:36:16 +00:00
f80a3fdb73 feat(cli): Add automatic update command and documentation updates 2025-10-27 17:36:16 +00:00
bf3e44c8c5 1.2.0
Some checks failed
CI / Type Check & Lint (push) Successful in 42s
Publish to npm / npm-publish (push) Failing after 1m4s
CI / Build Test (Current Platform) (push) Successful in 1m12s
Release / build-and-release (push) Successful in 1m52s
CI / Build All Platforms (push) Successful in 1m57s
2025-10-27 17:29:53 +00:00
fc22e1dd88 feat(scripts): Add community scripts subsystem: script index, runner, and CLI commands with background refresh; update docs and paths 2025-10-27 17:29:52 +00:00
12 changed files with 1035 additions and 39 deletions

View File

@@ -1,21 +1,101 @@
# Changelog # Changelog
## 2025-10-27 - 1.1.0 - feat(cli) ## 2025-10-28 - 1.4.0 - feat(cli)
Add initial MOXYTOOL implementation, packaging, install/uninstall scripts, CI and release workflows Improve CLI output and logging with colored header, grouped script listings, and ANSI-styled logger
- Add core CLI implementation (mod.ts and ts/): vgpu-setup command, logging, paths and plugins integration - Set smartcli instance version from deno.json to surface the package version in the CLI
- Add Deno config (deno.json) and build/test tasks - Revamp standard command output with a colored ASCII header, clearer commands list, and improved usage line
- Add compilation and packaging scripts (scripts/compile-all.sh, scripts/install-binary.js) and binary wrapper (bin/moxytool-wrapper.js) - Group script index output by type including Proxmox VE host (pve), Containers (ct), Virtual Machines (vm), and Other
- Add installer and uninstaller scripts (install.sh, uninstall.sh) for easy deployment - Enhance scripts listing formatting (slug padding and bullet points) for readability
- Add CI, build and release workflows (.gitea/workflows/) including multi-platform compilation and npm publish steps - Replace timestamped logger messages with ANSI-colored output and icons for error/warn/success/info
- Add documentation and metadata: readme.md, changelog.md, package.json and license
- Add .gitignore and dist/binaries handling, plus release checksum generation in workflows ## 2025-10-28 - 1.3.6 - fix(deps)
Bump smartcli dependency and add local settings file
- Bumped @push.rocks/smartcli from ^4.0.18 to ^4.0.19 in deno.json
- Added .claude/settings.local.json (development/local settings file)
## 2025-10-28 - 1.3.5 - fix(smartcli)
Bump @push.rocks/smartcli to ^4.0.18 and add local settings file for tooling permissions
- Updated dependency @push.rocks/smartcli from ^4.0.16 to ^4.0.18 in deno.json
- Added a local settings file (.claude/settings.local.json) to configure runtime/tooling permissions (web fetch domains, bash/deno/npm command allowances, and local read access)
- No code API changes; this is a dependency/infra update — incrementing patch version
## 2025-10-28 - 1.3.4 - fix(smartcli)
Update @push.rocks/smartcli to ^4.0.16 and add local Claude settings
- Bump dependency in deno.json: @push.rocks/smartcli from ^4.0.15 to ^4.0.16
- Add .claude/settings.local.json containing local permissions/configuration (development/local-only file)
## 2025-10-28 - 1.3.3 - fix(deno.json)
Bump @push.rocks/smartcli to ^4.0.15 and add local Claude settings
- Updated deno.json: @push.rocks/smartcli ^4.0.14 → ^4.0.15
- Added .claude/settings.local.json with local permissions for development/CI
- No runtime source changes; dependency update only — recommend a patch release
## 2025-10-28 - 1.3.2 - fix(cli)
Correct scripts subcommand argument parsing and bump smartcli dependency
- Fix scripts command argument indices so the subcommand is read from argvArg._[1] and subsequent arguments from argvArg._[2]. This resolves incorrect handling of 'scripts search', 'scripts info' and 'scripts run' inputs.
- Upgrade @push.rocks/smartcli dependency from ^4.0.11 to ^4.0.14 in deno.json for compatibility/stability improvements.
## 2025-10-27 - 1.3.1 - fix(publish)
Switch publish registry to internal Verdaccio instance and add local CI settings
- Update package.json publishConfig.registry from https://registry.npmjs.org/ to https://verdaccio.lossless.digital/ to publish packages to the internal Verdaccio registry.
- Add .claude/settings.local.json to include local CI/dev settings (local configuration only).
## 2025-10-27 - 1.3.0 - feat(cli)
Add automatic update command and documentation updates
- Add 'update' CLI command that checks the latest Gitea release and runs the install script to perform a self-update
- Implements release fetch/compare logic and executes the repository install.sh via curl
- Update README to document the one-line installer and the new 'moxytool update' usage
- Update changelog to note the new update command and installation clarifications
## 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
- 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. 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
- `update` command for automatic self-updating from Gitea releases
- `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)
- One-line installation script as primary installation method
### Features
- `moxytool update` - Update MOXYTOOL to the latest version automatically
- `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 +113,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

View File

@@ -1,6 +1,6 @@
{ {
"name": "@serve.zone/moxytool", "name": "@serve.zone/moxytool",
"version": "1.1.0", "version": "1.4.0",
"exports": "./mod.ts", "exports": "./mod.ts",
"nodeModulesDir": "auto", "nodeModulesDir": "auto",
"tasks": { "tasks": {
@@ -40,7 +40,7 @@
"@push.rocks/npmextra": "npm:@push.rocks/npmextra@^5.1.2", "@push.rocks/npmextra": "npm:@push.rocks/npmextra@^5.1.2",
"@push.rocks/projectinfo": "npm:@push.rocks/projectinfo@^5.0.1", "@push.rocks/projectinfo": "npm:@push.rocks/projectinfo@^5.0.1",
"@push.rocks/qenv": "npm:@push.rocks/qenv@^6.1.0", "@push.rocks/qenv": "npm:@push.rocks/qenv@^6.1.0",
"@push.rocks/smartcli": "npm:@push.rocks/smartcli@^4.0.11", "@push.rocks/smartcli": "npm:@push.rocks/smartcli@^4.0.19",
"@push.rocks/smartdelay": "npm:@push.rocks/smartdelay@^3.0.5", "@push.rocks/smartdelay": "npm:@push.rocks/smartdelay@^3.0.5",
"@push.rocks/smartfile": "npm:@push.rocks/smartfile@^11.0.23", "@push.rocks/smartfile": "npm:@push.rocks/smartfile@^11.0.23",
"@push.rocks/smartjson": "npm:@push.rocks/smartjson@^5.0.20", "@push.rocks/smartjson": "npm:@push.rocks/smartjson@^5.0.20",

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

@@ -1,6 +1,6 @@
{ {
"name": "@serve.zone/moxytool", "name": "@serve.zone/moxytool",
"version": "1.1.0", "version": "1.4.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",
"keywords": [ "keywords": [
"proxmox", "proxmox",
@@ -55,7 +55,7 @@
], ],
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://registry.npmjs.org/" "registry": "https://verdaccio.lossless.digital/"
}, },
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34" "packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
} }

View File

@@ -19,7 +19,22 @@ MOXYTOOL is a comprehensive command-line tool for managing Proxmox servers, with
## Installation ## Installation
### Global Installation (Recommended) ### One-Line Installation (Recommended)
```bash
# Download and install MOXYTOOL automatically
curl -sSL https://code.foss.global/serve.zone/moxytool/raw/branch/main/install.sh | sudo bash
```
This will:
- Detect your platform automatically (Linux x64/ARM64, macOS Intel/Apple Silicon, Windows)
- Download the latest binary from Gitea releases (~400-500KB)
- Install to `/usr/local/bin/moxytool`
- Make it available system-wide
### Via npm (Alternative)
Install globally using npm:
```bash ```bash
npm install -g @serve.zone/moxytool npm install -g @serve.zone/moxytool
@@ -31,14 +46,28 @@ or with pnpm:
pnpm install -g @serve.zone/moxytool pnpm install -g @serve.zone/moxytool
``` ```
### Local Installation **Benefits:**
- Automatic platform detection and binary download
```bash - Easy updates via `npm update -g @serve.zone/moxytool`
npm install @serve.zone/moxytool - Version management with npm
``` - Works with Node.js >=14
## Usage ## Usage
### Updating MOXYTOOL
Update to the latest version from the repository:
```bash
moxytool update
```
This command will:
- Check the current version
- Fetch the latest release from Gitea
- Automatically download and install the update if available
- Preserve your existing configuration
### vGPU Setup ### vGPU Setup
Install and configure NVIDIA vGPU support on your Proxmox host: Install and configure NVIDIA vGPU support on your Proxmox host:
@@ -90,12 +119,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 +172,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.4.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,9 @@
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';
import denoConfig from '../deno.json' with { type: 'json' };
export const runCli = async () => { export const runCli = async () => {
const smartshellInstance = new plugins.smartshell.Smartshell({ const smartshellInstance = new plugins.smartshell.Smartshell({
@@ -8,15 +11,38 @@ export const runCli = async () => {
}); });
const smartcliInstance = new plugins.smartcli.Smartcli(); const smartcliInstance = new plugins.smartcli.Smartcli();
smartcliInstance.version = denoConfig.version;
// Initialize script index and check if refresh is needed
const scriptIndex = new ScriptIndex();
// Silently check and refresh index in the background if needed
(async () => {
try {
await scriptIndex.loadCache();
if (await scriptIndex.needsRefresh()) {
// Don't block CLI startup, refresh in background
scriptIndex.fetchIndex().catch(() => {
// Silently fail, will use cached data
});
}
} catch {
// Silently fail on index errors
}
})();
// Standard command (no arguments) // Standard command (no arguments)
smartcliInstance.standardCommand().subscribe(async () => { smartcliInstance.standardCommand().subscribe(async () => {
logger.log('info', 'MOXYTOOL - Proxmox Administration Tool'); console.log('\x1b[1m\x1b[36m╔════════════════════════════════════════════╗\x1b[0m');
logger.log('info', ''); console.log('\x1b[1m\x1b[36m║\x1b[0m \x1b[1mMOXYTOOL\x1b[0m - Proxmox Administration \x1b[1m\x1b[36m║\x1b[0m');
logger.log('info', 'Available commands:'); console.log('\x1b[1m\x1b[36m╚════════════════════════════════════════════╝\x1b[0m');
logger.log('info', '* vgpu-setup - Install and configure Proxmox vGPU support'); console.log('');
logger.log('info', ''); console.log('\x1b[1mCommands:\x1b[0m');
logger.log('info', 'Usage: moxytool <command> [options]'); console.log(' \x1b[36m►\x1b[0m vgpu-setup Install and configure Proxmox vGPU support');
console.log(' \x1b[36m►\x1b[0m scripts Manage Proxmox community scripts (400+)');
console.log(' \x1b[36m►\x1b[0m update Update MOXYTOOL to the latest version');
console.log('');
console.log('\x1b[2mUsage: moxytool <command> [options]\x1b[0m');
}); });
// vGPU setup command // vGPU setup command
@@ -108,5 +134,312 @@ export const runCli = async () => {
} }
}); });
// Update command
smartcliInstance.addCommand('update').subscribe(async (argvArg) => {
logger.log('info', 'Checking for updates...');
logger.log('info', '');
try {
// Get current version from deno.json
const denoJsonPath = plugins.path.join(paths.packageDir, 'deno.json');
let currentVersion = '1.1.0'; // fallback
try {
const denoJsonContent = await Deno.readTextFile(denoJsonPath);
const denoJson = JSON.parse(denoJsonContent);
currentVersion = denoJson.version || currentVersion;
} catch {
// Use fallback version
}
// Fetch latest version from Gitea API
const apiUrl = 'https://code.foss.global/api/v1/repos/serve.zone/moxytool/releases/latest';
const response = await fetch(apiUrl);
if (!response.ok) {
logger.log('error', 'Failed to check for updates');
logger.log('error', `HTTP ${response.status}: ${response.statusText}`);
Deno.exit(1);
}
const release = await response.json();
const latestVersion = release.tag_name; // e.g., "v1.1.0"
// Normalize versions for comparison (ensure both have "v" prefix)
const normalizedCurrent = currentVersion.startsWith('v') ? currentVersion : `v${currentVersion}`;
const normalizedLatest = latestVersion.startsWith('v') ? latestVersion : `v${latestVersion}`;
logger.log('info', `Current version: ${normalizedCurrent}`);
logger.log('info', `Latest version: ${normalizedLatest}`);
logger.log('info', '');
// Compare normalized versions
if (normalizedCurrent === normalizedLatest) {
logger.log('success', 'Already up to date!');
logger.log('info', '');
return;
}
logger.log('ok', `New version available: ${latestVersion}`);
logger.log('info', 'Downloading and installing...');
logger.log('info', '');
// Download and run the install script
const installUrl = 'https://code.foss.global/serve.zone/moxytool/raw/branch/main/install.sh';
const updateResult = await smartshellInstance.exec(
`curl -sSL ${installUrl} | bash`
);
if (updateResult.exitCode !== 0) {
logger.log('error', 'Update failed');
logger.log('error', updateResult.stderr || 'Unknown error');
Deno.exit(1);
}
logger.log('info', '');
logger.log('success', `Updated to ${latestVersion}`);
logger.log('info', '');
} catch (error) {
logger.log('error', `Update failed: ${error instanceof Error ? error.message : String(error)}`);
Deno.exit(1);
}
});
// Scripts management commands
smartcliInstance.addCommand('scripts').subscribe(async (argvArg) => {
const subcommand = argvArg._[1]; // _[0] is 'scripts', _[1] is the subcommand
if (!subcommand) {
logger.log('info', 'MOXYTOOL Scripts - Proxmox Community Scripts Management');
logger.log('info', '');
logger.log('info', 'Available subcommands:');
logger.log('info', '* list - List all available scripts');
logger.log('info', '* search <query> - Search for scripts by name or description');
logger.log('info', '* info <slug> - Show detailed information about a script');
logger.log('info', '* run <slug> - Execute a script');
logger.log('info', '* refresh - Force refresh the script index');
logger.log('info', '');
logger.log('info', 'Usage: moxytool scripts <subcommand> [options]');
return;
}
// Ensure index is loaded
await scriptIndex.loadCache();
switch (subcommand) {
case 'list': {
const scripts = scriptIndex.getAll();
if (scripts.length === 0) {
logger.log('warn', 'No scripts found. Run "moxytool scripts refresh" to fetch the index.');
return;
}
const stats = scriptIndex.getStats();
logger.log('info', `Available Scripts (${stats.count} total, indexed ${stats.age})`);
logger.log('info', '');
// Group by type
const pveScripts = scripts.filter(s => s.type === 'pve');
const containers = scripts.filter(s => s.type === 'ct');
const vms = scripts.filter(s => s.type === 'vm');
const otherScripts = scripts.filter(s => s.type !== 'pve' && s.type !== 'ct' && s.type !== 'vm');
if (pveScripts.length > 0) {
logger.log('info', 'Proxmox VE Host Scripts:');
pveScripts.forEach(script => {
logger.log('info', `${script.slug.padEnd(25)} - ${script.name}`);
});
logger.log('info', '');
}
if (containers.length > 0) {
logger.log('info', 'Containers (LXC):');
containers.forEach(script => {
logger.log('info', `${script.slug.padEnd(25)} - ${script.name}`);
});
logger.log('info', '');
}
if (vms.length > 0) {
logger.log('info', 'Virtual Machines:');
vms.forEach(script => {
logger.log('info', `${script.slug.padEnd(25)} - ${script.name}`);
});
logger.log('info', '');
}
if (otherScripts.length > 0) {
logger.log('info', 'Other:');
otherScripts.forEach(script => {
logger.log('info', `${script.slug.padEnd(25)} - ${script.name} (${script.type})`);
});
}
logger.log('info', '');
logger.log('info', 'Use "moxytool scripts info <slug>" for more details');
logger.log('info', 'Use "moxytool scripts run <slug>" to install');
break;
}
case 'search': {
const query = argvArg._[2]; // _[0]=scripts, _[1]=search, _[2]=query
if (!query) {
logger.log('error', 'Please provide a search query');
logger.log('info', 'Usage: moxytool scripts search <query>');
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._[2]; // _[0]=scripts, _[1]=info, _[2]=slug
if (!slug) {
logger.log('error', 'Please provide a script slug');
logger.log('info', 'Usage: moxytool scripts info <slug>');
return;
}
const script = scriptIndex.getBySlug(slug as string);
if (!script) {
logger.log('error', `Script "${slug}" not found`);
logger.log('info', 'Use "moxytool scripts search <query>" to find scripts');
return;
}
logger.log('info', '═'.repeat(60));
logger.log('info', `${script.name}`);
logger.log('info', '═'.repeat(60));
logger.log('info', '');
logger.log('info', `Slug: ${script.slug}`);
logger.log('info', `Type: ${script.type === 'ct' ? 'Container (LXC)' : 'Virtual Machine'}`);
logger.log('info', '');
logger.log('info', 'Description:');
logger.log('info', script.description);
logger.log('info', '');
if (script.install_methods && script.install_methods[0]?.resources) {
const res = script.install_methods[0].resources;
logger.log('info', 'Resource Requirements:');
if (res.cpu) logger.log('info', ` CPU: ${res.cpu} cores`);
if (res.ram) logger.log('info', ` RAM: ${res.ram} MB`);
if (res.hdd) logger.log('info', ` Disk: ${res.hdd} GB`);
if (res.os) logger.log('info', ` OS: ${res.os} ${res.version || ''}`);
logger.log('info', '');
}
if (script.interface_port) {
logger.log('info', `Web Interface: http://<your-ip>:${script.interface_port}`);
logger.log('info', '');
}
if (script.default_credentials) {
logger.log('info', 'Default Credentials:');
if (script.default_credentials.username) {
logger.log('info', ` Username: ${script.default_credentials.username}`);
}
if (script.default_credentials.password) {
logger.log('info', ` Password: ${script.default_credentials.password}`);
}
logger.log('info', '');
}
if (script.notes && script.notes.length > 0) {
logger.log('info', 'Important Notes:');
script.notes.forEach(note => {
const prefix = note.type === 'warning' ? '⚠️ ' : ' ';
logger.log('warn', `${prefix}${note.content}`);
});
logger.log('info', '');
}
if (script.documentation) {
logger.log('info', `Documentation: ${script.documentation}`);
}
if (script.website) {
logger.log('info', `Website: ${script.website}`);
}
logger.log('info', '');
logger.log('info', `To install: sudo moxytool scripts run ${script.slug}`);
logger.log('info', '═'.repeat(60));
break;
}
case 'run': {
const slug = argvArg._[2]; // _[0]=scripts, _[1]=run, _[2]=slug
if (!slug) {
logger.log('error', 'Please provide a script slug');
logger.log('info', 'Usage: sudo moxytool scripts run <slug>');
return;
}
const script = scriptIndex.getBySlug(slug as string);
if (!script) {
logger.log('error', `Script "${slug}" not found`);
logger.log('info', 'Use "moxytool scripts search <query>" to find scripts');
return;
}
// Validate Proxmox host
const runner = new ScriptRunner();
const isProxmox = await runner.validateProxmoxHost();
if (!isProxmox) {
logger.log('error', 'This system does not appear to be running Proxmox');
logger.log('error', 'Community scripts can only be run on Proxmox hosts');
Deno.exit(1);
}
// Execute the script
const exitCode = await runner.execute(script);
Deno.exit(exitCode);
}
case 'refresh': {
logger.log('info', 'Refreshing script index...');
try {
await scriptIndex.fetchIndex();
const stats = scriptIndex.getStats();
logger.log('success', `Index refreshed: ${stats.count} scripts cached`);
} catch (error) {
logger.log('error', `Failed to refresh index: ${error}`);
Deno.exit(1);
}
break;
}
default:
logger.log('error', `Unknown subcommand: ${subcommand}`);
logger.log('info', 'Run "moxytool scripts" to see available subcommands');
}
});
smartcliInstance.startParse(); smartcliInstance.startParse();
}; };

View File

@@ -1,5 +1,19 @@
import * as plugins from './moxytool.plugins.ts'; import * as plugins from './moxytool.plugins.ts';
// ANSI color codes
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
dim: '\x1b[2m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m',
};
/** /**
* A simple logger class for MOXYTOOL * A simple logger class for MOXYTOOL
*/ */
@@ -14,22 +28,20 @@ class Logger {
} }
public log(level: string, message: string): void { public log(level: string, message: string): void {
const timestamp = new Date().toISOString();
switch (level) { switch (level) {
case 'error': case 'error':
console.error(`[${timestamp}] [ERROR] ${message}`); console.error(`${colors.red}${message}${colors.reset}`);
break; break;
case 'warn': case 'warn':
console.warn(`[${timestamp}] [WARN] ${message}`); console.warn(`${colors.yellow}${message}${colors.reset}`);
break; break;
case 'ok': case 'ok':
case 'success': case 'success':
console.log(`[${timestamp}] [OK] ${message}`); console.log(`${colors.green}${message}${colors.reset}`);
break; break;
case 'info': case 'info':
default: default:
console.log(`[${timestamp}] [INFO] ${message}`); console.log(message);
break; break;
} }
} }

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');