Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 976c4d0980 | |||
| eda652994d | |||
| 46f82d2b65 | |||
| 90c5f07be4 | |||
| 0e7d416048 | |||
| 23f41cc152 | |||
| ad7e9b0b46 | |||
| 8cf016f0d8 | |||
| 626cfe30ba | |||
| 45ac9af405 | |||
| d832343b38 | |||
| b38c99aaa0 | |||
| e379d60c65 | |||
| 99b68c9bb0 | |||
| 32d38975ec | |||
| 4ac2d99c32 | |||
| 9265670e63 | |||
| 9341b9cd16 | |||
| 8e6418a574 | |||
| 33609bff9a | |||
| ef7bab3e32 | |||
| 723dca735f | |||
| dc868e3fbc | |||
| f80a3fdb73 | |||
| bf3e44c8c5 | |||
| fc22e1dd88 |
129
changelog.md
129
changelog.md
@@ -1,21 +1,131 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 2025-10-27 - 1.1.0 - feat(cli)
|
## 2025-10-29 - 1.5.1 - fix(scriptindex)
|
||||||
Add initial MOXYTOOL implementation, packaging, install/uninstall scripts, CI and release workflows
|
Improve script search: use ObjectSorter with weighted results prioritizing slug and name
|
||||||
|
|
||||||
- Add core CLI implementation (mod.ts and ts/): vgpu-setup command, logging, paths and plugins integration
|
- Replaced smartfuzzy.FuzzyMatcher with smartfuzzy.ObjectSorter for multi-field fuzzy searching
|
||||||
- Add Deno config (deno.json) and build/test tasks
|
- Search now runs across slug, name, and description fields
|
||||||
- Add compilation and packaging scripts (scripts/compile-all.sh, scripts/install-binary.js) and binary wrapper (bin/moxytool-wrapper.js)
|
- Added weighting so slug matches are prioritized, name matches receive a medium boost
|
||||||
- Add installer and uninstaller scripts (install.sh, uninstall.sh) for easy deployment
|
- Search returns ordered script objects based on adjusted score
|
||||||
- Add CI, build and release workflows (.gitea/workflows/) including multi-platform compilation and npm publish steps
|
|
||||||
- Add documentation and metadata: readme.md, changelog.md, package.json and license
|
## 2025-10-29 - 1.5.0 - feat(scripts)
|
||||||
- Add .gitignore and dist/binaries handling, plus release checksum generation in workflows
|
Add fuzzy search and type filtering for community scripts; improve scripts CLI output and cache handling
|
||||||
|
|
||||||
|
- Integrate @push.rocks/smartfuzzy and use FuzzyMatcher to provide ranked, fuzzy search results for scripts
|
||||||
|
- Add optional type filtering to scripts search (e.g. type:vm, type:ct, type:pve) and parse a --filter option in the CLI
|
||||||
|
- Improve scripts CLI output: colored type badges, truncated descriptions, clearer usage/help text and filtered result messaging
|
||||||
|
- Optimize ScriptIndex.loadCache to avoid reloading when cache is already present
|
||||||
|
- Update deno.json to include the smartfuzzy dependency and export/import smartfuzzy in plugins
|
||||||
|
|
||||||
|
## 2025-10-28 - 1.4.2 - fix(scriptindex)
|
||||||
|
Handle missing script metadata fields in ScriptIndex.search to prevent crashes
|
||||||
|
|
||||||
|
- Add null/undefined checks for name, slug, and description in ScriptIndex.search to avoid runtime exceptions when script metadata is incomplete
|
||||||
|
- Improves robustness of scripts search against partially populated or malformed cached metadata
|
||||||
|
|
||||||
|
## 2025-10-28 - 1.4.1 - fix(cli)
|
||||||
|
Fallback to 'unknown' when script.slug is missing in scripts list
|
||||||
|
|
||||||
|
- Fixes a potential runtime error when listing scripts if a script entry lacks a slug
|
||||||
|
- Uses a safe fallback ('unknown') before calling padEnd to ensure stable output
|
||||||
|
- Modified file: ts/moxytool.cli.ts
|
||||||
|
|
||||||
|
## 2025-10-28 - 1.4.0 - feat(cli)
|
||||||
|
Improve CLI output and logging with colored header, grouped script listings, and ANSI-styled logger
|
||||||
|
|
||||||
|
- Set smartcli instance version from deno.json to surface the package version in the CLI
|
||||||
|
- Revamp standard command output with a colored ASCII header, clearer commands list, and improved usage line
|
||||||
|
- Group script index output by type including Proxmox VE host (pve), Containers (ct), Virtual Machines (vm), and Other
|
||||||
|
- Enhance scripts listing formatting (slug padding and bullet points) for readability
|
||||||
|
- Replace timestamped logger messages with ANSI-colored output and icons for error/warn/success/info
|
||||||
|
|
||||||
|
## 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 +143,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
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/moxytool",
|
"name": "@serve.zone/moxytool",
|
||||||
"version": "1.1.0",
|
"version": "1.5.1",
|
||||||
"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",
|
||||||
@@ -49,6 +49,7 @@
|
|||||||
"@push.rocks/smartpath": "npm:@push.rocks/smartpath@^5.0.5",
|
"@push.rocks/smartpath": "npm:@push.rocks/smartpath@^5.0.5",
|
||||||
"@push.rocks/smartshell": "npm:@push.rocks/smartshell@^3.2.2",
|
"@push.rocks/smartshell": "npm:@push.rocks/smartshell@^3.2.2",
|
||||||
"@push.rocks/smartexpect": "npm:@push.rocks/smartexpect@^1.0.15",
|
"@push.rocks/smartexpect": "npm:@push.rocks/smartexpect@^1.0.15",
|
||||||
|
"@push.rocks/smartfuzzy": "npm:@push.rocks/smartfuzzy@^2.0.0",
|
||||||
"@push.rocks/smartrx": "npm:@push.rocks/smartrx@^3.0.10",
|
"@push.rocks/smartrx": "npm:@push.rocks/smartrx@^3.0.10",
|
||||||
"@push.rocks/smartpromise": "npm:@push.rocks/smartpromise@^4.0.0",
|
"@push.rocks/smartpromise": "npm:@push.rocks/smartpromise@^4.0.0",
|
||||||
"@push.rocks/smartstring": "npm:@push.rocks/smartstring@^4.0.0",
|
"@push.rocks/smartstring": "npm:@push.rocks/smartstring@^4.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';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/moxytool",
|
"name": "@serve.zone/moxytool",
|
||||||
"version": "1.1.0",
|
"version": "1.5.1",
|
||||||
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
89
readme.md
89
readme.md
@@ -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
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/moxytool',
|
name: '@serve.zone/moxytool',
|
||||||
version: '1.1.0',
|
version: '1.5.1',
|
||||||
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
347
ts/moxytool.classes.scriptindex.ts
Normal file
347
ts/moxytool.classes.scriptindex.ts
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
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 {
|
||||||
|
// Don't reload if already cached
|
||||||
|
if (this.cache) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 with optional type filter
|
||||||
|
* @param query - Search query
|
||||||
|
* @param typeFilter - Optional type filter (e.g., 'vm', 'ct', 'pve')
|
||||||
|
*/
|
||||||
|
public search(query: string, typeFilter?: string): IScriptMetadata[] {
|
||||||
|
if (!this.cache) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let scripts = this.cache.scripts;
|
||||||
|
|
||||||
|
// Apply type filter if provided
|
||||||
|
if (typeFilter) {
|
||||||
|
scripts = scripts.filter((script) => script.type === typeFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use ObjectSorter for fuzzy searching across multiple fields
|
||||||
|
const sorter = new plugins.smartfuzzy.ObjectSorter(scripts);
|
||||||
|
|
||||||
|
// Search across slug, name, and description
|
||||||
|
const results = sorter.sort(query, ['slug', 'name', 'description']);
|
||||||
|
|
||||||
|
// Post-process to weight results by which field matched
|
||||||
|
const weightedResults = results.map((result) => {
|
||||||
|
let weight = 1;
|
||||||
|
|
||||||
|
// Boost score if match was in slug (highest priority)
|
||||||
|
if (result.matches?.some((m) => m.key === 'slug')) {
|
||||||
|
weight = 3;
|
||||||
|
}
|
||||||
|
// Boost if match was in name (medium priority)
|
||||||
|
else if (result.matches?.some((m) => m.key === 'name')) {
|
||||||
|
weight = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
adjustedScore: (result.score || 0) / weight,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort by adjusted score and return just the script objects
|
||||||
|
return weightedResults
|
||||||
|
.sort((a, b) => (a.adjustedScore || 0) - (b.adjustedScore || 0))
|
||||||
|
.map((result) => result.item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,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,326 @@ 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 compile-time imported deno.json
|
||||||
|
const currentVersion = denoConfig.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 => {
|
||||||
|
const slug = script.slug || 'unknown';
|
||||||
|
logger.log('info', ` • ${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> [--filter type:vm]');
|
||||||
|
logger.log('info', 'Filters: type:vm, type:ct, type:pve, type:addon');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse filter option
|
||||||
|
let typeFilter: string | undefined;
|
||||||
|
if (argvArg.filter) {
|
||||||
|
const filterString = argvArg.filter as string;
|
||||||
|
if (filterString.startsWith('type:')) {
|
||||||
|
typeFilter = filterString.substring(5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = scriptIndex.search(query as string, typeFilter);
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
const filterMsg = typeFilter ? ` (filtered by type:${typeFilter})` : '';
|
||||||
|
logger.log('warn', `No scripts found matching "${query}"${filterMsg}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterMsg = typeFilter ? ` \x1b[2m(filtered by type:${typeFilter})\x1b[0m` : '';
|
||||||
|
logger.log('info', `Found ${results.length} script(s) matching "\x1b[1m${query}\x1b[0m"${filterMsg}:`);
|
||||||
|
logger.log('info', '');
|
||||||
|
|
||||||
|
results.forEach(script => {
|
||||||
|
const slug = script.slug || 'unknown';
|
||||||
|
const description = script.description ? script.description.substring(0, 100) : 'No description available';
|
||||||
|
|
||||||
|
// Type badge with colors
|
||||||
|
let typeBadge = '';
|
||||||
|
if (script.type === 'ct') typeBadge = '\x1b[36m[LXC]\x1b[0m';
|
||||||
|
else if (script.type === 'vm') typeBadge = '\x1b[35m[VM]\x1b[0m';
|
||||||
|
else if (script.type === 'pve') typeBadge = '\x1b[33m[PVE]\x1b[0m';
|
||||||
|
else typeBadge = `\x1b[2m[${script.type}]\x1b[0m`;
|
||||||
|
|
||||||
|
logger.log('info', `\x1b[1m\x1b[36m►\x1b[0m \x1b[1m${slug}\x1b[0m ${typeBadge}`);
|
||||||
|
logger.log('info', ` \x1b[2m${script.name}\x1b[0m`);
|
||||||
|
logger.log('info', ` ${description}${script.description && script.description.length > 100 ? '...' : ''}`);
|
||||||
|
logger.log('info', '');
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.log('info', '\x1b[2mUse "moxytool scripts info <slug>" for more details\x1b[0m');
|
||||||
|
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();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import * as projectinfo from '@push.rocks/projectinfo';
|
|||||||
import * as smartcli from '@push.rocks/smartcli';
|
import * as smartcli from '@push.rocks/smartcli';
|
||||||
import * as smartdelay from '@push.rocks/smartdelay';
|
import * as smartdelay from '@push.rocks/smartdelay';
|
||||||
import * as smartfile from '@push.rocks/smartfile';
|
import * as smartfile from '@push.rocks/smartfile';
|
||||||
|
import * as smartfuzzy from '@push.rocks/smartfuzzy';
|
||||||
import * as smartjson from '@push.rocks/smartjson';
|
import * as smartjson from '@push.rocks/smartjson';
|
||||||
import * as smartlog from '@push.rocks/smartlog';
|
import * as smartlog from '@push.rocks/smartlog';
|
||||||
import * as smartlogDestinationLocal from '@push.rocks/smartlog-destination-local';
|
import * as smartlogDestinationLocal from '@push.rocks/smartlog-destination-local';
|
||||||
@@ -21,6 +22,7 @@ export {
|
|||||||
smartcli,
|
smartcli,
|
||||||
smartdelay,
|
smartdelay,
|
||||||
smartfile,
|
smartfile,
|
||||||
|
smartfuzzy,
|
||||||
smartjson,
|
smartjson,
|
||||||
smartlog,
|
smartlog,
|
||||||
smartlogDestinationLocal,
|
smartlogDestinationLocal,
|
||||||
|
|||||||
Reference in New Issue
Block a user