Fix script execution issues and improve container creation
- Fixed syntax errors in build.func (duplicate export, unmatched quotes) - Fixed color variable initialization by calling load_functions in core.func - Replaced undefined function calls (post_to_api, post_update_to_api) with echo statements - Fixed install script execution by copying scripts into container first - Made create_lxc.sh executable - Improved error handling and script sourcing - Added missing core functions and tools - Enhanced script downloader and local script management
This commit is contained in:
12
.env.example
12
.env.example
@@ -4,14 +4,18 @@
|
|||||||
# Prisma
|
# Prisma
|
||||||
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
|
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
|
||||||
DATABASE_URL="postgresql://postgres:password@localhost:5432/pve-scripts-local"
|
DATABASE_URL="postgresql://postgres:password@localhost:5432/pve-scripts-local"
|
||||||
REPO_URL="https://github.com/michelroegl-brunner/PVESciptslocal"
|
REPO_URL="https://github.com/community-scripts/ProxmoxVE"
|
||||||
REPO_BRANCH="main"
|
REPO_BRANCH="main"
|
||||||
SCRIPTS_DIRECTORY="scripts/ct"
|
SCRIPTS_DIRECTORY="scripts"
|
||||||
ALLOWED_SCRIPT_EXTENSIONS=".sh"
|
ALLOWED_SCRIPT_EXTENSIONS=".sh"
|
||||||
|
|
||||||
|
CT_SCRIPT_FOLDER="ct"
|
||||||
|
INSTALL_SCRIPT_FOLDER="install"
|
||||||
|
JSON_FOLDER="frontend/public/json"
|
||||||
|
|
||||||
# Security
|
# Security
|
||||||
MAX_SCRIPT_EXECUTION_TIME="300000"
|
MAX_SCRIPT_EXECUTION_TIME="900000"
|
||||||
ALLOWED_SCRIPT_PATHS="scripts/"
|
ALLOWED_SCRIPT_PATHS="scripts/"
|
||||||
|
|
||||||
# WebSocket Configuration
|
# WebSocket Configuration
|
||||||
WEBSOCKET_PORT="3000"
|
WEBSOCKET_PORT="3001"
|
||||||
|
|||||||
249
README.md
249
README.md
@@ -1,12 +1,249 @@
|
|||||||
# How to run this Alpha Software
|
# PVE Scripts Local 🚀
|
||||||
|
|
||||||
You need npm and git installed on your PVE host
|
A modern web-based management interface for Proxmox VE (PVE) helper scripts. This tool provides a user-friendly way to discover, download, and execute community-sourced Proxmox scripts locally with real-time terminal output streaming.
|
||||||
|
|
||||||
```git clone git@github.com:michelroegl-brunner/PVESciptslocal.git```
|
## 🌟 Features
|
||||||
|
|
||||||
Then you need to run ```npm install```
|
- **Web-based Interface**: Modern React/Next.js frontend with real-time terminal emulation
|
||||||
|
- **Script Discovery**: Browse and search through community Proxmox scripts from GitHub
|
||||||
|
- **One-Click Execution**: Run scripts directly from the web interface with live output
|
||||||
|
- **Real-time Terminal**: Full terminal emulation with xterm.js for interactive script execution
|
||||||
|
- **Script Management**: Download, update, and manage local script collections
|
||||||
|
- **Security**: Sandboxed script execution with path validation and time limits
|
||||||
|
- **Database Integration**: PostgreSQL backend for script metadata and execution history
|
||||||
|
- **WebSocket Communication**: Real-time bidirectional communication for script execution
|
||||||
|
|
||||||
And to run the dev server on IP:3000
|
## 🏗️ Architecture
|
||||||
|
|
||||||
```npm run dev```
|
### Frontend
|
||||||
|
- **Next.js 15** with React 19
|
||||||
|
- **TypeScript** for type safety
|
||||||
|
- **Tailwind CSS** for styling
|
||||||
|
- **xterm.js** for terminal emulation
|
||||||
|
- **tRPC** for type-safe API communication
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- **Node.js** server with WebSocket support
|
||||||
|
- **PostgreSQL** database with Prisma ORM
|
||||||
|
- **WebSocket Server** for real-time script execution
|
||||||
|
- **Script Downloader Service** for GitHub integration
|
||||||
|
|
||||||
|
### Scripts
|
||||||
|
- **Core Functions**: Shared utilities and build functions
|
||||||
|
- **Container Scripts**: Pre-configured LXC container setups
|
||||||
|
- **Installation Scripts**: System setup and configuration tools
|
||||||
|
|
||||||
|
## 📋 Prerequisites
|
||||||
|
|
||||||
|
- **Node.js** 18+ and npm
|
||||||
|
- **PostgreSQL** database (or Docker/Podman for local development)
|
||||||
|
- **Git** for cloning the repository
|
||||||
|
- **Linux/Unix environment** (tested on Proxmox VE hosts)
|
||||||
|
|
||||||
|
## 🚀 Installation
|
||||||
|
|
||||||
|
### 1. Clone the Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/michelroegl-brunner/PVESciptslocal.git
|
||||||
|
cd PVESciptslocal
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Environment Configuration
|
||||||
|
|
||||||
|
Copy the example environment file and configure your settings:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `.env` with your configuration:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Database Configuration
|
||||||
|
DATABASE_URL="postgresql://postgres:password@localhost:5432/pve-scripts-local"
|
||||||
|
|
||||||
|
# GitHub Repository Configuration
|
||||||
|
REPO_URL="https://github.com/community-scripts/ProxmoxVE"
|
||||||
|
REPO_BRANCH="main"
|
||||||
|
SCRIPTS_DIRECTORY="scripts/ct"
|
||||||
|
|
||||||
|
# Security Settings
|
||||||
|
MAX_SCRIPT_EXECUTION_TIME="300000"
|
||||||
|
ALLOWED_SCRIPT_PATHS="scripts/"
|
||||||
|
|
||||||
|
# WebSocket Configuration
|
||||||
|
WEBSOCKET_PORT="3000"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### 4. Start the Application
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### Production Mode
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
The application will be available at `http://IP:3000`
|
||||||
|
|
||||||
|
## 🎯 Usage
|
||||||
|
|
||||||
|
### 1. Access the Web Interface
|
||||||
|
|
||||||
|
Open your browser and navigate to `http://IP:3000` (or your configured host/port).
|
||||||
|
|
||||||
|
### 2. Browse Available Scripts
|
||||||
|
|
||||||
|
- The main page displays a grid of available Proxmox scripts
|
||||||
|
- Use the search functionality to find specific scripts
|
||||||
|
- Scripts are categorized by type (containers, installations, etc.)
|
||||||
|
|
||||||
|
### 3. Download Scripts
|
||||||
|
|
||||||
|
- Click on any script card to view details
|
||||||
|
- Use the "Download" button to fetch scripts from GitHub
|
||||||
|
- Downloaded scripts are stored locally in the `scripts/` directory
|
||||||
|
|
||||||
|
### 4. Execute Scripts
|
||||||
|
|
||||||
|
- Click "Run Script" on any downloaded script
|
||||||
|
- A terminal window will open with real-time output
|
||||||
|
- Interact with the script through the web terminal
|
||||||
|
- Use the close button to stop execution
|
||||||
|
|
||||||
|
### 5. Script Management
|
||||||
|
|
||||||
|
- View script execution history
|
||||||
|
- Update scripts to latest versions
|
||||||
|
- Manage local script collections
|
||||||
|
|
||||||
|
## 📁 Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
PVESciptslocal/
|
||||||
|
├── scripts/ # Script collection
|
||||||
|
│ ├── core/ # Core utility functions
|
||||||
|
│ │ ├── build.func # Build system functions
|
||||||
|
│ │ ├── tools.func # Tool installation functions
|
||||||
|
│ │ └── create_lxc.sh # LXC container creation
|
||||||
|
│ ├── ct/ # Container templates
|
||||||
|
│ │ ├── 2fauth.sh # 2FA authentication app
|
||||||
|
│ │ ├── adguard.sh # AdGuard Home
|
||||||
|
│ │ └── debian.sh # Debian base container
|
||||||
|
│ └── install/ # Installation scripts
|
||||||
|
├── src/ # Source code
|
||||||
|
│ ├── app/ # Next.js app directory
|
||||||
|
│ │ ├── _components/ # React components
|
||||||
|
│ │ └── page.tsx # Main page
|
||||||
|
│ └── server/ # Server-side code
|
||||||
|
│ └── services/ # Business logic services
|
||||||
|
├── prisma/ # Database schema
|
||||||
|
├── public/ # Static assets
|
||||||
|
├── server.js # Main server file
|
||||||
|
└── package.json # Dependencies and scripts
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 🔧 Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `DATABASE_URL` | PostgreSQL connection string | Required |
|
||||||
|
| `REPO_URL` | GitHub repository URL | Required |
|
||||||
|
| `REPO_BRANCH` | Git branch to use | `main` |
|
||||||
|
| `SCRIPTS_DIRECTORY` | Local scripts directory | `scripts/ct` |
|
||||||
|
| `MAX_SCRIPT_EXECUTION_TIME` | Max execution time (ms) | `300000` |
|
||||||
|
| `ALLOWED_SCRIPT_PATHS` | Allowed script paths | `scripts/` |
|
||||||
|
|
||||||
|
### Database Configuration
|
||||||
|
|
||||||
|
The application uses PostgreSQL with Prisma ORM. The database stores:
|
||||||
|
- Script metadata and descriptions
|
||||||
|
- Execution history and logs
|
||||||
|
- User preferences and settings
|
||||||
|
|
||||||
|
## 🚀 Development
|
||||||
|
|
||||||
|
### Prerequisites for Development
|
||||||
|
- Node.js 18+
|
||||||
|
- PostgreSQL or Docker
|
||||||
|
- Git
|
||||||
|
|
||||||
|
### Development Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Start development server
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Start Next.js in development mode
|
||||||
|
npm run dev:next
|
||||||
|
|
||||||
|
# Type checking
|
||||||
|
npm run typecheck
|
||||||
|
|
||||||
|
# Linting
|
||||||
|
npm run lint
|
||||||
|
npm run lint:fix
|
||||||
|
|
||||||
|
# Formatting
|
||||||
|
npm run format:write
|
||||||
|
npm run format:check
|
||||||
|
|
||||||
|
# Database operations
|
||||||
|
npm run db:generate # Generate Prisma client
|
||||||
|
npm run db:migrate # Run migrations
|
||||||
|
npm run db:push # Push schema changes
|
||||||
|
npm run db:studio # Open Prisma Studio
|
||||||
|
```
|
||||||
|
|
||||||
|
### Project Structure for Developers
|
||||||
|
|
||||||
|
- **Frontend**: React components in `src/app/_components/`
|
||||||
|
- **Backend**: Server logic in `src/server/`
|
||||||
|
- **API**: tRPC routers for type-safe API communication
|
||||||
|
- **Database**: Prisma schema in `prisma/schema.prisma`
|
||||||
|
- **Scripts**: Bash scripts in `scripts/` directory
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||||
|
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
||||||
|
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||||
|
5. Open a Pull Request
|
||||||
|
|
||||||
|
### Adding New Scripts
|
||||||
|
|
||||||
|
1. Create a new `.sh` file in the appropriate directory (`scripts/ct/` for containers)
|
||||||
|
2. Follow the existing script structure and include proper headers
|
||||||
|
3. Test the script thoroughly
|
||||||
|
4. Submit a pull request with the new script
|
||||||
|
|
||||||
|
## 📝 License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
- Server logs: Check console output or `server.log`
|
||||||
|
- Database logs: Check PostgreSQL logs
|
||||||
|
- Script execution: View in web terminal
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Note**: This is alpha software. Use with caution in production environments and always backup your Proxmox configuration before running scripts.
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
# Copyright (c) 2021-2025 michelroegl-brunner
|
# Copyright (c) 2021-2025 tteck
|
||||||
# Author: michelroegl-brunner
|
# Author: tteck (tteckster)
|
||||||
|
# Co-Author: MickLesk
|
||||||
|
# Co-Author: michelroegl-brunner
|
||||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||||
|
|
||||||
variables() {
|
variables() {
|
||||||
@@ -7,14 +9,15 @@ variables() {
|
|||||||
var_install="${NSAPP}-install" # sets the var_install variable by appending "-install" to the value of NSAPP.
|
var_install="${NSAPP}-install" # sets the var_install variable by appending "-install" to the value of NSAPP.
|
||||||
INTEGER='^[0-9]+([.][0-9]+)?$' # it defines the INTEGER regular expression pattern.
|
INTEGER='^[0-9]+([.][0-9]+)?$' # it defines the INTEGER regular expression pattern.
|
||||||
PVEHOST_NAME=$(hostname) # gets the Proxmox Hostname and sets it to Uppercase
|
PVEHOST_NAME=$(hostname) # gets the Proxmox Hostname and sets it to Uppercase
|
||||||
|
DIAGNOSTICS="yes" # sets the DIAGNOSTICS variable to "yes", used for the API call.
|
||||||
METHOD="default" # sets the METHOD variable to "default", used for the API call.
|
METHOD="default" # sets the METHOD variable to "default", used for the API call.
|
||||||
|
RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" # generates a random UUID and sets it to the RANDOM_UUID variable.
|
||||||
CT_TYPE=${var_unprivileged:-$CT_TYPE}
|
CT_TYPE=${var_unprivileged:-$CT_TYPE}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/core.func"
|
source "$(dirname "${BASH_SOURCE[0]}")/core.func"
|
||||||
|
|
||||||
|
|
||||||
# This function enables error handling in the script by setting options and defining a trap for the ERR signal.
|
# This function enables error handling in the script by setting options and defining a trap for the ERR signal.
|
||||||
catch_errors() {
|
catch_errors() {
|
||||||
set -Eeo pipefail
|
set -Eeo pipefail
|
||||||
@@ -23,7 +26,6 @@ catch_errors() {
|
|||||||
|
|
||||||
# This function is called when an error occurs. It receives the exit code, line number, and command that caused the error, and displays an error message.
|
# This function is called when an error occurs. It receives the exit code, line number, and command that caused the error, and displays an error message.
|
||||||
error_handler() {
|
error_handler() {
|
||||||
|
|
||||||
printf "\e[?25h"
|
printf "\e[?25h"
|
||||||
local exit_code="$?"
|
local exit_code="$?"
|
||||||
local line_number="$1"
|
local line_number="$1"
|
||||||
@@ -351,25 +353,574 @@ exit_script() {
|
|||||||
exit
|
exit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# This function allows the user to configure advanced settings for the script.
|
||||||
|
advanced_settings() {
|
||||||
|
whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox --title "Here is an instructional tip:" "To make a selection, use the Spacebar." 8 58
|
||||||
|
# Setting Default Tag for Advanced Settings
|
||||||
|
TAGS="community-script;${var_tags:-}"
|
||||||
|
CT_DEFAULT_TYPE="${CT_TYPE}"
|
||||||
|
CT_TYPE=""
|
||||||
|
while [ -z "$CT_TYPE" ]; do
|
||||||
|
if [ "$CT_DEFAULT_TYPE" == "1" ]; then
|
||||||
|
if CT_TYPE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \
|
||||||
|
"1" "Unprivileged" ON \
|
||||||
|
"0" "Privileged" OFF \
|
||||||
|
3>&1 1>&2 2>&3); then
|
||||||
|
if [ -n "$CT_TYPE" ]; then
|
||||||
|
CT_TYPE_DESC="Unprivileged"
|
||||||
|
if [ "$CT_TYPE" -eq 0 ]; then
|
||||||
|
CT_TYPE_DESC="Privileged"
|
||||||
|
fi
|
||||||
|
echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}"
|
||||||
|
echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}"
|
||||||
|
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
exit_script
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ "$CT_DEFAULT_TYPE" == "0" ]; then
|
||||||
|
if CT_TYPE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \
|
||||||
|
"1" "Unprivileged" OFF \
|
||||||
|
"0" "Privileged" ON \
|
||||||
|
3>&1 1>&2 2>&3); then
|
||||||
|
if [ -n "$CT_TYPE" ]; then
|
||||||
|
CT_TYPE_DESC="Unprivileged"
|
||||||
|
if [ "$CT_TYPE" -eq 0 ]; then
|
||||||
|
CT_TYPE_DESC="Privileged"
|
||||||
|
fi
|
||||||
|
echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}"
|
||||||
|
echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}"
|
||||||
|
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
exit_script
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
if PW1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --passwordbox "\nSet Root Password (needed for root ssh access)" 9 58 --title "PASSWORD (leave blank for automatic login)" 3>&1 1>&2 2>&3); then
|
||||||
|
# Empty = Autologin
|
||||||
|
if [[ -z "$PW1" ]]; then
|
||||||
|
PW=""
|
||||||
|
PW1="Automatic Login"
|
||||||
|
echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}$PW1${CL}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Invalid: contains spaces
|
||||||
|
if [[ "$PW1" == *" "* ]]; then
|
||||||
|
whiptail --msgbox "Password cannot contain spaces." 8 58
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Invalid: too short
|
||||||
|
if ((${#PW1} < 5)); then
|
||||||
|
whiptail --msgbox "Password must be at least 5 characters." 8 58
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Confirm password
|
||||||
|
if PW2=$(whiptail --backtitle "Proxmox VE Helper Scripts" --passwordbox "\nVerify Root Password" 9 58 --title "PASSWORD VERIFICATION" 3>&1 1>&2 2>&3); then
|
||||||
|
if [[ "$PW1" == "$PW2" ]]; then
|
||||||
|
PW="-password $PW1"
|
||||||
|
echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}********${CL}"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
whiptail --msgbox "Passwords do not match. Please try again." 8 58
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
exit_script
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
exit_script
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if CT_ID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Container ID" 8 58 "$NEXTID" --title "CONTAINER ID" 3>&1 1>&2 2>&3); then
|
||||||
|
if [ -z "$CT_ID" ]; then
|
||||||
|
CT_ID="$NEXTID"
|
||||||
|
echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}"
|
||||||
|
else
|
||||||
|
echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
exit_script
|
||||||
|
fi
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
if CT_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$NSAPP" --title "HOSTNAME" 3>&1 1>&2 2>&3); then
|
||||||
|
if [ -z "$CT_NAME" ]; then
|
||||||
|
HN="$NSAPP"
|
||||||
|
else
|
||||||
|
HN=$(echo "${CT_NAME,,}" | tr -d ' ')
|
||||||
|
fi
|
||||||
|
# Hostname validate (RFC 1123)
|
||||||
|
if [[ "$HN" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then
|
||||||
|
echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||||||
|
--msgbox "❌ Invalid hostname: '$HN'\n\nOnly lowercase letters, digits and hyphens (-) are allowed.\nUnderscores (_) or other characters are not permitted!" 10 70
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
exit_script
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GB" 8 58 "$var_disk" --title "DISK SIZE" 3>&1 1>&2 2>&3) || exit_script
|
||||||
|
|
||||||
|
if [ -z "$DISK_SIZE" ]; then
|
||||||
|
DISK_SIZE="$var_disk"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$DISK_SIZE" =~ ^[1-9][0-9]*$ ]]; then
|
||||||
|
echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
whiptail --msgbox "Disk size must be a positive integer!" 8 58
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||||||
|
--inputbox "Allocate CPU Cores" 8 58 "$var_cpu" --title "CORE COUNT" 3>&1 1>&2 2>&3) || exit_script
|
||||||
|
|
||||||
|
if [ -z "$CORE_COUNT" ]; then
|
||||||
|
CORE_COUNT="$var_cpu"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$CORE_COUNT" =~ ^[1-9][0-9]*$ ]]; then
|
||||||
|
echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
whiptail --msgbox "CPU core count must be a positive integer!" 8 58
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||||||
|
--inputbox "Allocate RAM in MiB" 8 58 "$var_ram" --title "RAM" 3>&1 1>&2 2>&3) || exit_script
|
||||||
|
|
||||||
|
if [ -z "$RAM_SIZE" ]; then
|
||||||
|
RAM_SIZE="$var_ram"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$RAM_SIZE" =~ ^[1-9][0-9]*$ ]]; then
|
||||||
|
echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
whiptail --msgbox "RAM size must be a positive integer!" 8 58
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
BRIDGES=""
|
||||||
|
IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f)
|
||||||
|
OLD_IFS=$IFS
|
||||||
|
IFS=$'\n'
|
||||||
|
|
||||||
|
for iface_filepath in ${IFACE_FILEPATH_LIST}; do
|
||||||
|
iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX')
|
||||||
|
|
||||||
|
(grep -Pn '^\s*iface' "${iface_filepath}" | cut -d':' -f1 && wc -l "${iface_filepath}" | cut -d' ' -f1) |
|
||||||
|
awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" || true
|
||||||
|
|
||||||
|
if [ -f "${iface_indexes_tmpfile}" ]; then
|
||||||
|
while read -r pair; do
|
||||||
|
start=$(echo "${pair}" | cut -d':' -f1)
|
||||||
|
end=$(echo "${pair}" | cut -d':' -f2)
|
||||||
|
|
||||||
|
if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then
|
||||||
|
iface_name=$(sed "${start}q;d" "${iface_filepath}" | awk '{print $2}')
|
||||||
|
BRIDGES="${iface_name}"$'\n'"${BRIDGES}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
done <"${iface_indexes_tmpfile}"
|
||||||
|
rm -f "${iface_indexes_tmpfile}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
IFS=$OLD_IFS
|
||||||
|
|
||||||
|
BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq)
|
||||||
|
|
||||||
|
if [[ -z "$BRIDGES" ]]; then
|
||||||
|
BRG="vmbr0"
|
||||||
|
echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
|
||||||
|
else
|
||||||
|
BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --menu "Select network bridge:" 15 40 6 $(echo "$BRIDGES" | awk '{print $0, "Bridge"}') 3>&1 1>&2 2>&3)
|
||||||
|
if [ -z "$BRG" ]; then
|
||||||
|
exit_script
|
||||||
|
else
|
||||||
|
echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# IPv4 methods: dhcp, static, none
|
||||||
|
while true; do
|
||||||
|
IPV4_METHOD=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||||||
|
--title "IPv4 Address Management" \
|
||||||
|
--menu "Select IPv4 Address Assignment Method:" 12 60 2 \
|
||||||
|
"dhcp" "Automatic (DHCP, recommended)" \
|
||||||
|
"static" "Static (manual entry)" \
|
||||||
|
3>&1 1>&2 2>&3)
|
||||||
|
|
||||||
|
exit_status=$?
|
||||||
|
if [ $exit_status -ne 0 ]; then
|
||||||
|
exit_script
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$IPV4_METHOD" in
|
||||||
|
dhcp)
|
||||||
|
NET="dhcp"
|
||||||
|
GATE=""
|
||||||
|
echo -e "${NETWORK}${BOLD}${DGN}IPv4: DHCP${CL}"
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
static)
|
||||||
|
# Static: call and validate CIDR address
|
||||||
|
while true; do
|
||||||
|
NET=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||||||
|
--inputbox "Enter Static IPv4 CIDR Address (e.g. 192.168.100.50/24)" 8 58 "" \
|
||||||
|
--title "IPv4 ADDRESS" 3>&1 1>&2 2>&3)
|
||||||
|
if [ -z "$NET" ]; then
|
||||||
|
whiptail --msgbox "IPv4 address must not be empty." 8 58
|
||||||
|
continue
|
||||||
|
elif [[ "$NET" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then
|
||||||
|
echo -e "${NETWORK}${BOLD}${DGN}IPv4 Address: ${BGN}$NET${CL}"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
whiptail --msgbox "$NET is not a valid IPv4 CIDR address. Please enter a correct value!" 8 58
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# call and validate Gateway
|
||||||
|
while true; do
|
||||||
|
GATE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||||||
|
--inputbox "Enter Gateway IP address for static IPv4" 8 58 "" \
|
||||||
|
--title "Gateway IP" 3>&1 1>&2 2>&3)
|
||||||
|
if [ -z "$GATE1" ]; then
|
||||||
|
whiptail --msgbox "Gateway IP address cannot be empty." 8 58
|
||||||
|
elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
|
||||||
|
whiptail --msgbox "Invalid Gateway IP address format." 8 58
|
||||||
|
else
|
||||||
|
GATE=",gw=$GATE1"
|
||||||
|
echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# IPv6 Address Management selection
|
||||||
|
while true; do
|
||||||
|
IPV6_METHOD=$(whiptail --backtitle "Proxmox VE Helper Scripts" --menu \
|
||||||
|
"Select IPv6 Address Management Type:" 15 58 4 \
|
||||||
|
"auto" "SLAAC/AUTO (recommended, default)" \
|
||||||
|
"dhcp" "DHCPv6" \
|
||||||
|
"static" "Static (manual entry)" \
|
||||||
|
"none" "Disabled" \
|
||||||
|
--default-item "auto" 3>&1 1>&2 2>&3)
|
||||||
|
[ $? -ne 0 ] && exit_script
|
||||||
|
|
||||||
|
case "$IPV6_METHOD" in
|
||||||
|
auto)
|
||||||
|
echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}SLAAC/AUTO${CL}"
|
||||||
|
IPV6_ADDR=""
|
||||||
|
IPV6_GATE=""
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
dhcp)
|
||||||
|
echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}DHCPv6${CL}"
|
||||||
|
IPV6_ADDR="dhcp"
|
||||||
|
IPV6_GATE=""
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
static)
|
||||||
|
# Ask for static IPv6 address (CIDR notation, e.g., 2001:db8::1234/64)
|
||||||
|
while true; do
|
||||||
|
IPV6_ADDR=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox \
|
||||||
|
"Set a static IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 "" \
|
||||||
|
--title "IPv6 STATIC ADDRESS" 3>&1 1>&2 2>&3) || exit_script
|
||||||
|
if [[ "$IPV6_ADDR" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+(/[0-9]{1,3})$ ]]; then
|
||||||
|
echo -e "${NETWORK}${BOLD}${DGN}IPv6 Address: ${BGN}$IPV6_ADDR${CL}"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox \
|
||||||
|
"$IPV6_ADDR is an invalid IPv6 CIDR address. Please enter a valid IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
# Optional: ask for IPv6 gateway for static config
|
||||||
|
while true; do
|
||||||
|
IPV6_GATE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox \
|
||||||
|
"Enter IPv6 gateway address (optional, leave blank for none)" 8 58 "" --title "IPv6 GATEWAY" 3>&1 1>&2 2>&3)
|
||||||
|
if [ -z "$IPV6_GATE" ]; then
|
||||||
|
IPV6_GATE=""
|
||||||
|
break
|
||||||
|
elif [[ "$IPV6_GATE" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+$ ]]; then
|
||||||
|
break
|
||||||
|
else
|
||||||
|
whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox \
|
||||||
|
"Invalid IPv6 gateway format." 8 58
|
||||||
|
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
none)
|
||||||
|
echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}Disabled${CL}"
|
||||||
|
IPV6_ADDR="none"
|
||||||
|
IPV6_GATE=""
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
exit_script
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$var_os" == "alpine" ]; then
|
||||||
|
APT_CACHER=""
|
||||||
|
APT_CACHER_IP=""
|
||||||
|
else
|
||||||
|
if APT_CACHER_IP=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set APT-Cacher IP (leave blank for none)" 8 58 --title "APT-Cacher IP" 3>&1 1>&2 2>&3); then
|
||||||
|
APT_CACHER="${APT_CACHER_IP:+yes}"
|
||||||
|
echo -e "${NETWORK}${BOLD}${DGN}APT-Cacher IP Address: ${BGN}${APT_CACHER_IP:-Default}${CL}"
|
||||||
|
else
|
||||||
|
exit_script
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default [The MTU of your selected vmbr, default is 1500])" 8 58 --title "MTU SIZE" 3>&1 1>&2 2>&3); then
|
||||||
|
if [ -z "$MTU1" ]; then
|
||||||
|
MTU1="Default"
|
||||||
|
MTU=""
|
||||||
|
else
|
||||||
|
MTU=",mtu=$MTU1"
|
||||||
|
fi
|
||||||
|
echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
|
||||||
|
else
|
||||||
|
exit_script
|
||||||
|
fi
|
||||||
|
|
||||||
|
if SD=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a DNS Search Domain (leave blank for HOST)" 8 58 --title "DNS Search Domain" 3>&1 1>&2 2>&3); then
|
||||||
|
if [ -z "$SD" ]; then
|
||||||
|
SX=Host
|
||||||
|
SD=""
|
||||||
|
else
|
||||||
|
SX=$SD
|
||||||
|
SD="-searchdomain=$SD"
|
||||||
|
fi
|
||||||
|
echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}$SX${CL}"
|
||||||
|
else
|
||||||
|
exit_script
|
||||||
|
fi
|
||||||
|
|
||||||
|
if NX=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a DNS Server IP (leave blank for HOST)" 8 58 --title "DNS SERVER IP" 3>&1 1>&2 2>&3); then
|
||||||
|
if [ -z "$NX" ]; then
|
||||||
|
NX=Host
|
||||||
|
NS=""
|
||||||
|
else
|
||||||
|
NS="-nameserver=$NX"
|
||||||
|
fi
|
||||||
|
echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}$NX${CL}"
|
||||||
|
else
|
||||||
|
exit_script
|
||||||
|
fi
|
||||||
|
|
||||||
|
if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address(leave blank for generated MAC)" 8 58 --title "MAC ADDRESS" 3>&1 1>&2 2>&3); then
|
||||||
|
if [ -z "$MAC1" ]; then
|
||||||
|
MAC1="Default"
|
||||||
|
MAC=""
|
||||||
|
else
|
||||||
|
MAC=",hwaddr=$MAC1"
|
||||||
|
echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
exit_script
|
||||||
|
fi
|
||||||
|
|
||||||
|
if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for no VLAN)" 8 58 --title "VLAN" 3>&1 1>&2 2>&3); then
|
||||||
|
if [ -z "$VLAN1" ]; then
|
||||||
|
VLAN1="Default"
|
||||||
|
VLAN=""
|
||||||
|
else
|
||||||
|
VLAN=",tag=$VLAN1"
|
||||||
|
fi
|
||||||
|
echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}$VLAN1${CL}"
|
||||||
|
else
|
||||||
|
exit_script
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ADV_TAGS=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Custom Tags?[If you remove all, there will be no tags!]" 8 58 "${TAGS}" --title "Advanced Tags" 3>&1 1>&2 2>&3); then
|
||||||
|
if [ -n "${ADV_TAGS}" ]; then
|
||||||
|
ADV_TAGS=$(echo "$ADV_TAGS" | tr -d '[:space:]')
|
||||||
|
TAGS="${ADV_TAGS}"
|
||||||
|
else
|
||||||
|
TAGS=";"
|
||||||
|
fi
|
||||||
|
echo -e "${NETWORK}${BOLD}${DGN}Tags: ${BGN}$TAGS${CL}"
|
||||||
|
else
|
||||||
|
exit_script
|
||||||
|
fi
|
||||||
|
|
||||||
|
SSH_AUTHORIZED_KEY="$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "SSH Authorized key for root (leave empty for none)" 8 58 --title "SSH Key" 3>&1 1>&2 2>&3)"
|
||||||
|
|
||||||
|
if [[ -z "${SSH_AUTHORIZED_KEY}" ]]; then
|
||||||
|
SSH_AUTHORIZED_KEY=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$PW" == -password* || -n "$SSH_AUTHORIZED_KEY" ]]; then
|
||||||
|
if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH ACCESS" --yesno "Enable Root SSH Access?" 10 58); then
|
||||||
|
SSH="yes"
|
||||||
|
else
|
||||||
|
SSH="no"
|
||||||
|
fi
|
||||||
|
echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}"
|
||||||
|
else
|
||||||
|
SSH="no"
|
||||||
|
echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "FUSE Support" --yesno "Enable FUSE support?\nRequired for tools like rclone, mergerfs, AppImage, etc." 10 58); then
|
||||||
|
ENABLE_FUSE="yes"
|
||||||
|
else
|
||||||
|
ENABLE_FUSE="no"
|
||||||
|
fi
|
||||||
|
echo -e "${FUSE}${BOLD}${DGN}Enable FUSE Support: ${BGN}$ENABLE_FUSE${CL}"
|
||||||
|
|
||||||
|
if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "VERBOSE MODE" --yesno "Enable Verbose Mode?" 10 58); then
|
||||||
|
VERBOSE="yes"
|
||||||
|
else
|
||||||
|
VERBOSE="no"
|
||||||
|
fi
|
||||||
|
echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}"
|
||||||
|
|
||||||
|
if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create ${APP} LXC?" 10 58); then
|
||||||
|
echo -e "${CREATING}${BOLD}${RD}Creating a ${APP} LXC using the above advanced settings${CL}"
|
||||||
|
|
||||||
|
# Strip prefixes from DNS parameters for config file storage
|
||||||
|
local SD_VALUE="$SD"
|
||||||
|
local NS_VALUE="$NS"
|
||||||
|
local MAC_VALUE="$MAC"
|
||||||
|
local VLAN_VALUE="$VLAN"
|
||||||
|
[[ "$SD" =~ ^-searchdomain= ]] && SD_VALUE="${SD#-searchdomain=}"
|
||||||
|
[[ "$NS" =~ ^-nameserver= ]] && NS_VALUE="${NS#-nameserver=}"
|
||||||
|
[[ "$MAC" =~ ^,hwaddr= ]] && MAC_VALUE="${MAC#,hwaddr=}"
|
||||||
|
[[ "$VLAN" =~ ^,tag= ]] && VLAN_VALUE="${VLAN#,tag=}"
|
||||||
|
|
||||||
|
# Temporarily store original values
|
||||||
|
local SD_ORIG="$SD"
|
||||||
|
local NS_ORIG="$NS"
|
||||||
|
local MAC_ORIG="$MAC"
|
||||||
|
local VLAN_ORIG="$VLAN"
|
||||||
|
|
||||||
|
# Set clean values for config file writing
|
||||||
|
SD="$SD_VALUE"
|
||||||
|
NS="$NS_VALUE"
|
||||||
|
MAC="$MAC_VALUE"
|
||||||
|
VLAN="$VLAN_VALUE"
|
||||||
|
|
||||||
|
write_config
|
||||||
|
|
||||||
|
# Restore original formatted values for container creation
|
||||||
|
SD="$SD_ORIG"
|
||||||
|
NS="$NS_ORIG"
|
||||||
|
MAC="$MAC_ORIG"
|
||||||
|
VLAN="$VLAN_ORIG"
|
||||||
|
else
|
||||||
|
clear
|
||||||
|
header_info
|
||||||
|
echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings on node $PVEHOST_NAME${CL}"
|
||||||
|
advanced_settings
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
diagnostics_check() {
|
||||||
|
if ! [ -d "/usr/local/community-scripts" ]; then
|
||||||
|
mkdir -p /usr/local/community-scripts
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! [ -f "/usr/local/community-scripts/diagnostics" ]; then
|
||||||
|
if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "DIAGNOSTICS" --yesno "Send Diagnostics of LXC Installation?\n\n(This only transmits data without user data, just RAM, CPU, LXC name, ...)" 10 58); then
|
||||||
|
cat <<EOF >/usr/local/community-scripts/diagnostics
|
||||||
|
DIAGNOSTICS=yes
|
||||||
|
|
||||||
|
#This file is used to store the diagnostics settings for the Community-Scripts API.
|
||||||
|
#https://github.com/community-scripts/ProxmoxVE/discussions/1836
|
||||||
|
#Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes.
|
||||||
|
#You can review the data at https://community-scripts.github.io/ProxmoxVE/data
|
||||||
|
#If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue.
|
||||||
|
#This will disable the diagnostics feature.
|
||||||
|
#To send diagnostics, set the variable 'DIAGNOSTICS' to "yes" in /usr/local/community-scripts/diagnostics, or use the menue.
|
||||||
|
#This will enable the diagnostics feature.
|
||||||
|
#The following information will be sent:
|
||||||
|
#"ct_type"
|
||||||
|
#"disk_size"
|
||||||
|
#"core_count"
|
||||||
|
#"ram_size"
|
||||||
|
#"os_type"
|
||||||
|
#"os_version"
|
||||||
|
#"nsapp"
|
||||||
|
#"method"
|
||||||
|
#"pve_version"
|
||||||
|
#"status"
|
||||||
|
#If you have any concerns, please review the source code at /misc/build.func
|
||||||
|
EOF
|
||||||
|
DIAGNOSTICS="yes"
|
||||||
|
else
|
||||||
|
cat <<EOF >/usr/local/community-scripts/diagnostics
|
||||||
|
DIAGNOSTICS=no
|
||||||
|
|
||||||
|
#This file is used to store the diagnostics settings for the Community-Scripts API.
|
||||||
|
#https://github.com/community-scripts/ProxmoxVE/discussions/1836
|
||||||
|
#Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes.
|
||||||
|
#You can review the data at https://community-scripts.github.io/ProxmoxVE/data
|
||||||
|
#If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue.
|
||||||
|
#This will disable the diagnostics feature.
|
||||||
|
#To send diagnostics, set the variable 'DIAGNOSTICS' to "yes" in /usr/local/community-scripts/diagnostics, or use the menue.
|
||||||
|
#This will enable the diagnostics feature.
|
||||||
|
#The following information will be sent:
|
||||||
|
#"ct_type"
|
||||||
|
#"disk_size"
|
||||||
|
#"core_count"
|
||||||
|
#"ram_size"
|
||||||
|
#"os_type"
|
||||||
|
#"os_version"
|
||||||
|
#"nsapp"
|
||||||
|
#"method"
|
||||||
|
#"pve_version"
|
||||||
|
#"status"
|
||||||
|
#If you have any concerns, please review the source code at /misc/build.func
|
||||||
|
EOF
|
||||||
|
DIAGNOSTICS="no"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
DIAGNOSTICS=$(awk -F '=' '/^DIAGNOSTICS/ {print $2}' /usr/local/community-scripts/diagnostics)
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
install_script() {
|
install_script() {
|
||||||
pve_check
|
pve_check
|
||||||
shell_check
|
shell_check
|
||||||
root_check
|
root_check
|
||||||
arch_check
|
arch_check
|
||||||
#ssh_check
|
|
||||||
maxkeys_check
|
maxkeys_check
|
||||||
|
diagnostics_check
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if systemctl is-active -q ping-instances.service; then
|
if systemctl is-active -q ping-instances.service; then
|
||||||
systemctl -q stop ping-instances.service
|
systemctl -q stop ping-instances.service
|
||||||
fi
|
fi
|
||||||
NEXTID=$(pvesh get /cluster/nextid)
|
NEXTID=$(pvesh get /cluster/nextid)
|
||||||
timezone=$(cat /etc/timezone)
|
timezone=$(cat /etc/timezone)
|
||||||
#header_info
|
header_info
|
||||||
echo "TEST"
|
|
||||||
while true; do
|
while true; do
|
||||||
|
|
||||||
TMP_CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
|
TMP_CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||||||
@@ -378,7 +929,9 @@ install_script() {
|
|||||||
"1" "Default Settings" \
|
"1" "Default Settings" \
|
||||||
"2" "Default Settings (with verbose)" \
|
"2" "Default Settings (with verbose)" \
|
||||||
"3" "Advanced Settings" \
|
"3" "Advanced Settings" \
|
||||||
"4" "Exit" \
|
"4" "Use Config File" \
|
||||||
|
"5" "Diagnostic Settings" \
|
||||||
|
"6" "Exit" \
|
||||||
--default-item "1" 3>&1 1>&2 2>&3) || true
|
--default-item "1" 3>&1 1>&2 2>&3) || true
|
||||||
|
|
||||||
if [ -z "$TMP_CHOICE" ]; then
|
if [ -z "$TMP_CHOICE" ]; then
|
||||||
@@ -416,6 +969,32 @@ install_script() {
|
|||||||
break
|
break
|
||||||
;;
|
;;
|
||||||
4)
|
4)
|
||||||
|
header_info
|
||||||
|
echo -e "${INFO}${HOLD} ${GN}Using Config File on node $PVEHOST_NAME${CL}"
|
||||||
|
METHOD="config_file"
|
||||||
|
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/config-file.func)
|
||||||
|
config_file
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
5)
|
||||||
|
if [[ $DIAGNOSTICS == "yes" ]]; then
|
||||||
|
if whiptail --backtitle "Proxmox VE Helper Scripts" --title "DIAGNOSTICS SETTINGS" --yesno "Send Diagnostics of LXC Installation?\n\nCurrent setting: ${DIAGNOSTICS}" 10 58 \
|
||||||
|
--yes-button "No" --no-button "Back"; then
|
||||||
|
DIAGNOSTICS="no"
|
||||||
|
sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=no/' /usr/local/community-scripts/diagnostics
|
||||||
|
whiptail --backtitle "Proxmox VE Helper Scripts" --title "DIAGNOSTICS SETTINGS" --msgbox "Diagnostics settings changed to ${DIAGNOSTICS}." 8 58
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if whiptail --backtitle "Proxmox VE Helper Scripts" --title "DIAGNOSTICS SETTINGS" --yesno "Send Diagnostics of LXC Installation?\n\nCurrent setting: ${DIAGNOSTICS}" 10 58 \
|
||||||
|
--yes-button "Yes" --no-button "Back"; then
|
||||||
|
DIAGNOSTICS="yes"
|
||||||
|
sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=yes/' /usr/local/community-scripts/diagnostics
|
||||||
|
whiptail --backtitle "Proxmox VE Helper Scripts" --title "DIAGNOSTICS SETTINGS" --msgbox "Diagnostics settings changed to ${DIAGNOSTICS}." 8 58
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
;;
|
||||||
|
6)
|
||||||
echo -e "\n${CROSS}${RD}Script terminated. Have a great day!${CL}\n"
|
echo -e "\n${CROSS}${RD}Script terminated. Have a great day!${CL}\n"
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
@@ -464,9 +1043,10 @@ check_container_storage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func)
|
source "$(dirname "${BASH_SOURCE[0]}")/tools.func"
|
||||||
if command -v pveversion >/dev/null 2>&1; then
|
if command -v pveversion >/dev/null 2>&1; then
|
||||||
install_script
|
install_script
|
||||||
|
echo "TEST!!!"
|
||||||
else
|
else
|
||||||
CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \
|
CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \
|
||||||
"Support/Update functions for ${APP} LXC. Choose an option:" \
|
"Support/Update functions for ${APP} LXC. Choose an option:" \
|
||||||
@@ -496,6 +1076,7 @@ start() {
|
|||||||
|
|
||||||
# This function collects user settings and integrates all the collected information.
|
# This function collects user settings and integrates all the collected information.
|
||||||
build_container() {
|
build_container() {
|
||||||
|
echo "TEST"
|
||||||
# if [ "$VERBOSE" == "yes" ]; then set -x; fi
|
# if [ "$VERBOSE" == "yes" ]; then set -x; fi
|
||||||
|
|
||||||
NET_STRING="-net0 name=eth0,bridge=$BRG$MAC,ip=$NET$GATE$VLAN$MTU"
|
NET_STRING="-net0 name=eth0,bridge=$BRG$MAC,ip=$NET$GATE$VLAN$MTU"
|
||||||
@@ -518,13 +1099,16 @@ build_container() {
|
|||||||
FEATURES="$FEATURES,fuse=1"
|
FEATURES="$FEATURES,fuse=1"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ $DIAGNOSTICS == "yes" ]]; then
|
||||||
|
echo "Diagnostics enabled (post_to_api function not available)"
|
||||||
|
fi
|
||||||
|
|
||||||
TEMP_DIR=$(mktemp -d)
|
TEMP_DIR=$(mktemp -d)
|
||||||
pushd "$TEMP_DIR" >/dev/null
|
pushd "$TEMP_DIR" >/dev/null
|
||||||
if [ "$var_os" == "alpine" ]; then
|
if [ "$var_os" == "alpine" ]; then
|
||||||
export FUNCTIONS_FILE_PATH="$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/alpine-install.func)"
|
export FUNCTIONS_FILE_PATH="$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/alpine-install.func)"
|
||||||
else
|
else
|
||||||
export FUNCTIONS_FILE_PATH="$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/install.func)"
|
export FUNCTIONS_FILE_PATH="$(dirname "${BASH_SOURCE[0]}")/install.func"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export DIAGNOSTICS="$DIAGNOSTICS"
|
export DIAGNOSTICS="$DIAGNOSTICS"
|
||||||
@@ -559,7 +1143,7 @@ build_container() {
|
|||||||
$PW
|
$PW
|
||||||
"
|
"
|
||||||
# This executes create_lxc.sh and creates the container and .conf file
|
# This executes create_lxc.sh and creates the container and .conf file
|
||||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/create_lxc.sh)" $?
|
bash "$(dirname "${BASH_SOURCE[0]}")/create_lxc.sh" $?
|
||||||
|
|
||||||
LXC_CONFIG="/etc/pve/lxc/${CTID}.conf"
|
LXC_CONFIG="/etc/pve/lxc/${CTID}.conf"
|
||||||
|
|
||||||
@@ -752,7 +1336,9 @@ EOF'
|
|||||||
fi
|
fi
|
||||||
msg_ok "Customized LXC Container"
|
msg_ok "Customized LXC Container"
|
||||||
|
|
||||||
lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/install/${var_install}.sh)"
|
# Copy the install script into the container and execute it
|
||||||
|
pct push "$CTID" "$(dirname "${BASH_SOURCE[0]}")/../install/${var_install}.sh" "/tmp/${var_install}.sh"
|
||||||
|
lxc-attach -n "$CTID" -- bash "/tmp/${var_install}.sh"
|
||||||
}
|
}
|
||||||
|
|
||||||
# This function sets the description of the container.
|
# This function sets the description of the container.
|
||||||
@@ -797,5 +1383,6 @@ EOF
|
|||||||
if [[ -f /etc/systemd/system/ping-instances.service ]]; then
|
if [[ -f /etc/systemd/system/ping-instances.service ]]; then
|
||||||
systemctl start ping-instances.service
|
systemctl start ping-instances.service
|
||||||
fi
|
fi
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -406,4 +406,7 @@ check_or_create_swap() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
trap 'stop_spinner' EXIT INT TERM
|
trap 'stop_spinner' EXIT INT TERM
|
||||||
|
|
||||||
|
# Initialize functions when core.func is sourced
|
||||||
|
load_functions
|
||||||
380
scripts/core/create_lxc.sh
Executable file
380
scripts/core/create_lxc.sh
Executable file
@@ -0,0 +1,380 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Copyright (c) 2021-2025 tteck
|
||||||
|
# Author: tteck (tteckster)
|
||||||
|
# Co-Author: MickLesk
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||||
|
|
||||||
|
# This sets verbose mode if the global variable is set to "yes"
|
||||||
|
# if [ "$VERBOSE" == "yes" ]; then set -x; fi
|
||||||
|
|
||||||
|
source "$(dirname "$0")/core.func"
|
||||||
|
|
||||||
|
|
||||||
|
# This sets error handling options and defines the error_handler function to handle errors
|
||||||
|
set -Eeuo pipefail
|
||||||
|
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
|
||||||
|
trap on_exit EXIT
|
||||||
|
trap on_interrupt INT
|
||||||
|
trap on_terminate TERM
|
||||||
|
|
||||||
|
function on_exit() {
|
||||||
|
local exit_code="$?"
|
||||||
|
[[ -n "${lockfile:-}" && -e "$lockfile" ]] && rm -f "$lockfile"
|
||||||
|
exit "$exit_code"
|
||||||
|
}
|
||||||
|
|
||||||
|
function error_handler() {
|
||||||
|
local exit_code="$?"
|
||||||
|
local line_number="$1"
|
||||||
|
local command="$2"
|
||||||
|
printf "\e[?25h"
|
||||||
|
echo -e "\n${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}\n"
|
||||||
|
exit "$exit_code"
|
||||||
|
}
|
||||||
|
|
||||||
|
function on_interrupt() {
|
||||||
|
echo -e "\n${RD}Interrupted by user (SIGINT)${CL}"
|
||||||
|
exit 130
|
||||||
|
}
|
||||||
|
|
||||||
|
function on_terminate() {
|
||||||
|
echo -e "\n${RD}Terminated by signal (SIGTERM)${CL}"
|
||||||
|
exit 143
|
||||||
|
}
|
||||||
|
|
||||||
|
function exit_script() {
|
||||||
|
clear
|
||||||
|
printf "\e[?25h"
|
||||||
|
echo -e "\n${CROSS}${RD}User exited script${CL}\n"
|
||||||
|
kill 0
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function check_storage_support() {
|
||||||
|
local CONTENT="$1"
|
||||||
|
local -a VALID_STORAGES=()
|
||||||
|
while IFS= read -r line; do
|
||||||
|
local STORAGE_NAME
|
||||||
|
STORAGE_NAME=$(awk '{print $1}' <<<"$line")
|
||||||
|
[[ -z "$STORAGE_NAME" ]] && continue
|
||||||
|
VALID_STORAGES+=("$STORAGE_NAME")
|
||||||
|
done < <(pvesm status -content "$CONTENT" 2>/dev/null | awk 'NR>1')
|
||||||
|
|
||||||
|
[[ ${#VALID_STORAGES[@]} -gt 0 ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# This function selects a storage pool for a given content type (e.g., rootdir, vztmpl).
|
||||||
|
function select_storage() {
|
||||||
|
local CLASS=$1 CONTENT CONTENT_LABEL
|
||||||
|
|
||||||
|
case $CLASS in
|
||||||
|
container)
|
||||||
|
CONTENT='rootdir'
|
||||||
|
CONTENT_LABEL='Container'
|
||||||
|
;;
|
||||||
|
template)
|
||||||
|
CONTENT='vztmpl'
|
||||||
|
CONTENT_LABEL='Container template'
|
||||||
|
;;
|
||||||
|
iso)
|
||||||
|
CONTENT='iso'
|
||||||
|
CONTENT_LABEL='ISO image'
|
||||||
|
;;
|
||||||
|
images)
|
||||||
|
CONTENT='images'
|
||||||
|
CONTENT_LABEL='VM Disk image'
|
||||||
|
;;
|
||||||
|
backup)
|
||||||
|
CONTENT='backup'
|
||||||
|
CONTENT_LABEL='Backup'
|
||||||
|
;;
|
||||||
|
snippets)
|
||||||
|
CONTENT='snippets'
|
||||||
|
CONTENT_LABEL='Snippets'
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
msg_error "Invalid storage class '$CLASS'"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Check for preset STORAGE variable
|
||||||
|
if [ "$CONTENT" = "rootdir" ] && [ -n "${STORAGE:-}" ]; then
|
||||||
|
if pvesm status -content "$CONTENT" | awk 'NR>1 {print $1}' | grep -qx "$STORAGE"; then
|
||||||
|
STORAGE_RESULT="$STORAGE"
|
||||||
|
msg_info "Using preset storage: $STORAGE_RESULT for $CONTENT_LABEL"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
msg_error "Preset storage '$STORAGE' is not valid for content type '$CONTENT'."
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
local -A STORAGE_MAP
|
||||||
|
local -a MENU
|
||||||
|
local COL_WIDTH=0
|
||||||
|
|
||||||
|
while read -r TAG TYPE _ TOTAL USED FREE _; do
|
||||||
|
[[ -n "$TAG" && -n "$TYPE" ]] || continue
|
||||||
|
local STORAGE_NAME="$TAG"
|
||||||
|
local DISPLAY="${STORAGE_NAME} (${TYPE})"
|
||||||
|
local USED_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$USED")
|
||||||
|
local FREE_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$FREE")
|
||||||
|
local INFO="Free: ${FREE_FMT}B Used: ${USED_FMT}B"
|
||||||
|
STORAGE_MAP["$DISPLAY"]="$STORAGE_NAME"
|
||||||
|
MENU+=("$DISPLAY" "$INFO" "OFF")
|
||||||
|
((${#DISPLAY} > COL_WIDTH)) && COL_WIDTH=${#DISPLAY}
|
||||||
|
done < <(pvesm status -content "$CONTENT" | awk 'NR>1')
|
||||||
|
|
||||||
|
if [ ${#MENU[@]} -eq 0 ]; then
|
||||||
|
msg_error "No storage found for content type '$CONTENT'."
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $((${#MENU[@]} / 3)) -eq 1 ]; then
|
||||||
|
STORAGE_RESULT="${STORAGE_MAP[${MENU[0]}]}"
|
||||||
|
STORAGE_INFO="${MENU[1]}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local WIDTH=$((COL_WIDTH + 42))
|
||||||
|
while true; do
|
||||||
|
local DISPLAY_SELECTED
|
||||||
|
DISPLAY_SELECTED=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||||||
|
--title "Storage Pools" \
|
||||||
|
--radiolist "Which storage pool for ${CONTENT_LABEL,,}?\n(Spacebar to select)" \
|
||||||
|
16 "$WIDTH" 6 "${MENU[@]}" 3>&1 1>&2 2>&3)
|
||||||
|
|
||||||
|
# Cancel or ESC
|
||||||
|
[[ $? -ne 0 ]] && exit_script
|
||||||
|
|
||||||
|
# Strip trailing whitespace or newline (important for storages like "storage (dir)")
|
||||||
|
DISPLAY_SELECTED=$(sed 's/[[:space:]]*$//' <<<"$DISPLAY_SELECTED")
|
||||||
|
|
||||||
|
if [[ -z "$DISPLAY_SELECTED" || -z "${STORAGE_MAP[$DISPLAY_SELECTED]+_}" ]]; then
|
||||||
|
whiptail --msgbox "No valid storage selected. Please try again." 8 58
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
STORAGE_RESULT="${STORAGE_MAP[$DISPLAY_SELECTED]}"
|
||||||
|
for ((i = 0; i < ${#MENU[@]}; i += 3)); do
|
||||||
|
if [[ "${MENU[$i]}" == "$DISPLAY_SELECTED" ]]; then
|
||||||
|
STORAGE_INFO="${MENU[$i + 1]}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test if required variables are set
|
||||||
|
[[ "${CTID:-}" ]] || {
|
||||||
|
msg_error "You need to set 'CTID' variable."
|
||||||
|
exit 203
|
||||||
|
}
|
||||||
|
[[ "${PCT_OSTYPE:-}" ]] || {
|
||||||
|
msg_error "You need to set 'PCT_OSTYPE' variable."
|
||||||
|
exit 204
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test if ID is valid
|
||||||
|
[ "$CTID" -ge "100" ] || {
|
||||||
|
msg_error "ID cannot be less than 100."
|
||||||
|
exit 205
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test if ID is in use
|
||||||
|
if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then
|
||||||
|
echo -e "ID '$CTID' is already in use."
|
||||||
|
unset CTID
|
||||||
|
msg_error "Cannot use ID that is already in use."
|
||||||
|
exit 206
|
||||||
|
fi
|
||||||
|
|
||||||
|
# This checks for the presence of valid Container Storage and Template Storage locations
|
||||||
|
msg_info "Validating storage"
|
||||||
|
if ! check_storage_support "rootdir"; then
|
||||||
|
msg_error "No valid storage found for 'rootdir' [Container]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! check_storage_support "vztmpl"; then
|
||||||
|
msg_error "No valid storage found for 'vztmpl' [Template]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
#msg_info "Checking template storage"
|
||||||
|
while true; do
|
||||||
|
if select_storage template; then
|
||||||
|
TEMPLATE_STORAGE="$STORAGE_RESULT"
|
||||||
|
TEMPLATE_STORAGE_INFO="$STORAGE_INFO"
|
||||||
|
msg_ok "Storage ${BL}$TEMPLATE_STORAGE${CL} ($TEMPLATE_STORAGE_INFO) [Template]"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
if select_storage container; then
|
||||||
|
CONTAINER_STORAGE="$STORAGE_RESULT"
|
||||||
|
CONTAINER_STORAGE_INFO="$STORAGE_INFO"
|
||||||
|
msg_ok "Storage ${BL}$CONTAINER_STORAGE${CL} ($CONTAINER_STORAGE_INFO) [Container]"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check free space on selected container storage
|
||||||
|
STORAGE_FREE=$(pvesm status | awk -v s="$CONTAINER_STORAGE" '$1 == s { print $6 }')
|
||||||
|
REQUIRED_KB=$((${PCT_DISK_SIZE:-8} * 1024 * 1024))
|
||||||
|
if [ "$STORAGE_FREE" -lt "$REQUIRED_KB" ]; then
|
||||||
|
msg_error "Not enough space on '$CONTAINER_STORAGE'. Needed: ${PCT_DISK_SIZE:-8}G."
|
||||||
|
exit 214
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check Cluster Quorum if in Cluster
|
||||||
|
if [ -f /etc/pve/corosync.conf ]; then
|
||||||
|
msg_info "Checking cluster quorum"
|
||||||
|
if ! pvecm status | awk -F':' '/^Quorate/ { exit ($2 ~ /Yes/) ? 0 : 1 }'; then
|
||||||
|
|
||||||
|
msg_error "Cluster is not quorate. Start all nodes or configure quorum device (QDevice)."
|
||||||
|
exit 210
|
||||||
|
fi
|
||||||
|
msg_ok "Cluster is quorate"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update LXC template list
|
||||||
|
TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}"
|
||||||
|
case "$PCT_OSTYPE" in
|
||||||
|
debian | ubuntu)
|
||||||
|
TEMPLATE_PATTERN="-standard_"
|
||||||
|
;;
|
||||||
|
alpine | fedora | rocky | centos)
|
||||||
|
TEMPLATE_PATTERN="-default_"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
TEMPLATE_PATTERN=""
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# 1. Check local templates first
|
||||||
|
msg_info "Searching for template '$TEMPLATE_SEARCH'"
|
||||||
|
mapfile -t TEMPLATES < <(
|
||||||
|
pveam list "$TEMPLATE_STORAGE" |
|
||||||
|
awk -v s="$TEMPLATE_SEARCH" -v p="$TEMPLATE_PATTERN" '$1 ~ s && $1 ~ p {print $1}' |
|
||||||
|
sed 's/.*\///' | sort -t - -k 2 -V
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ ${#TEMPLATES[@]} -gt 0 ]; then
|
||||||
|
TEMPLATE_SOURCE="local"
|
||||||
|
else
|
||||||
|
msg_info "No local template found, checking online repository"
|
||||||
|
pveam update >/dev/null 2>&1
|
||||||
|
mapfile -t TEMPLATES < <(
|
||||||
|
pveam update >/dev/null 2>&1 &&
|
||||||
|
pveam available -section system |
|
||||||
|
sed -n "s/.*\($TEMPLATE_SEARCH.*$TEMPLATE_PATTERN.*\)/\1/p" |
|
||||||
|
sort -t - -k 2 -V
|
||||||
|
)
|
||||||
|
TEMPLATE_SOURCE="online"
|
||||||
|
fi
|
||||||
|
|
||||||
|
TEMPLATE="${TEMPLATES[-1]}"
|
||||||
|
TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null ||
|
||||||
|
echo "/var/lib/vz/template/cache/$TEMPLATE")"
|
||||||
|
msg_ok "Template ${BL}$TEMPLATE${CL} [$TEMPLATE_SOURCE]"
|
||||||
|
|
||||||
|
# 4. Validate template (exists & not corrupted)
|
||||||
|
TEMPLATE_VALID=1
|
||||||
|
|
||||||
|
if [ ! -s "$TEMPLATE_PATH" ]; then
|
||||||
|
TEMPLATE_VALID=0
|
||||||
|
elif ! tar --use-compress-program=zstdcat -tf "$TEMPLATE_PATH" >/dev/null 2>&1; then
|
||||||
|
TEMPLATE_VALID=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$TEMPLATE_VALID" -eq 0 ]; then
|
||||||
|
msg_warn "Template $TEMPLATE is missing or corrupted. Re-downloading."
|
||||||
|
[[ -f "$TEMPLATE_PATH" ]] && rm -f "$TEMPLATE_PATH"
|
||||||
|
for attempt in {1..3}; do
|
||||||
|
msg_info "Attempt $attempt: Downloading LXC template..."
|
||||||
|
if pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1; then
|
||||||
|
msg_ok "Template download successful."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ $attempt -eq 3 ]; then
|
||||||
|
msg_error "Failed after 3 attempts. Please check network access or manually run:\n pveam download $TEMPLATE_STORAGE $TEMPLATE"
|
||||||
|
exit 208
|
||||||
|
fi
|
||||||
|
sleep $((attempt * 5))
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
msg_info "Creating LXC Container"
|
||||||
|
# Check and fix subuid/subgid
|
||||||
|
grep -q "root:100000:65536" /etc/subuid || echo "root:100000:65536" >>/etc/subuid
|
||||||
|
grep -q "root:100000:65536" /etc/subgid || echo "root:100000:65536" >>/etc/subgid
|
||||||
|
|
||||||
|
# Combine all options
|
||||||
|
PCT_OPTIONS=(${PCT_OPTIONS[@]:-${DEFAULT_PCT_OPTIONS[@]}})
|
||||||
|
[[ " ${PCT_OPTIONS[@]} " =~ " -rootfs " ]] || PCT_OPTIONS+=(-rootfs "$CONTAINER_STORAGE:${PCT_DISK_SIZE:-8}")
|
||||||
|
|
||||||
|
# Secure creation of the LXC container with lock and template check
|
||||||
|
lockfile="/tmp/template.${TEMPLATE}.lock"
|
||||||
|
exec 9>"$lockfile" || {
|
||||||
|
msg_error "Failed to create lock file '$lockfile'."
|
||||||
|
exit 200
|
||||||
|
}
|
||||||
|
flock -w 60 9 || {
|
||||||
|
msg_error "Timeout while waiting for template lock"
|
||||||
|
exit 211
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" &>/dev/null; then
|
||||||
|
msg_error "Container creation failed. Checking if template is corrupted or incomplete."
|
||||||
|
|
||||||
|
if [[ ! -s "$TEMPLATE_PATH" || "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then
|
||||||
|
msg_error "Template file too small or missing – re-downloading."
|
||||||
|
rm -f "$TEMPLATE_PATH"
|
||||||
|
elif ! zstdcat "$TEMPLATE_PATH" | tar -tf - &>/dev/null; then
|
||||||
|
msg_error "Template appears to be corrupted – re-downloading."
|
||||||
|
rm -f "$TEMPLATE_PATH"
|
||||||
|
else
|
||||||
|
msg_error "Template is valid, but container creation still failed."
|
||||||
|
exit 209
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Retry download
|
||||||
|
for attempt in {1..3}; do
|
||||||
|
msg_info "Attempt $attempt: Re-downloading template..."
|
||||||
|
if timeout 120 pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null; then
|
||||||
|
msg_ok "Template re-download successful."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ "$attempt" -eq 3 ]; then
|
||||||
|
msg_error "Three failed attempts. Aborting."
|
||||||
|
exit 208
|
||||||
|
fi
|
||||||
|
sleep $((attempt * 5))
|
||||||
|
done
|
||||||
|
|
||||||
|
sleep 1 # I/O-Sync-Delay
|
||||||
|
msg_ok "Re-downloaded LXC Template"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! pct list | awk '{print $1}' | grep -qx "$CTID"; then
|
||||||
|
msg_error "Container ID $CTID not listed in 'pct list' – unexpected failure."
|
||||||
|
exit 215
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -q '^rootfs:' "/etc/pve/lxc/$CTID.conf"; then
|
||||||
|
msg_error "RootFS entry missing in container config – storage not correctly assigned."
|
||||||
|
exit 216
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q '^hostname:' "/etc/pve/lxc/$CTID.conf"; then
|
||||||
|
CT_HOSTNAME=$(grep '^hostname:' "/etc/pve/lxc/$CTID.conf" | awk '{print $2}')
|
||||||
|
if [[ ! "$CT_HOSTNAME" =~ ^[a-z0-9-]+$ ]]; then
|
||||||
|
msg_warn "Hostname '$CT_HOSTNAME' contains invalid characters – may cause issues with networking or DNS."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
msg_ok "LXC Container ${BL}$CTID${CL} ${GN}was successfully created."
|
||||||
2047
scripts/core/tools.func
Normal file
2047
scripts/core/tools.func
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,81 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
SCRIPT_DIR="$(dirname "$0")"
|
|
||||||
source "$SCRIPT_DIR/../core/build.func"
|
|
||||||
# Copyright (c) 2021-2025 community-scripts ORG
|
|
||||||
# Author: jkrgr0
|
|
||||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
|
||||||
# Source: https://docs.2fauth.app/
|
|
||||||
|
|
||||||
APP="2FAuth"
|
|
||||||
var_tags="${var_tags:-2fa;authenticator}"
|
|
||||||
var_cpu="${var_cpu:-1}"
|
|
||||||
var_ram="${var_ram:-512}"
|
|
||||||
var_disk="${var_disk:-2}"
|
|
||||||
var_os="${var_os:-debian}"
|
|
||||||
var_version="${var_version:-12}"
|
|
||||||
var_unprivileged="${var_unprivileged:-1}"
|
|
||||||
|
|
||||||
header_info "$APP"
|
|
||||||
variables
|
|
||||||
color
|
|
||||||
catch_errors
|
|
||||||
|
|
||||||
function update_script() {
|
|
||||||
header_info
|
|
||||||
check_container_storage
|
|
||||||
check_container_resources
|
|
||||||
|
|
||||||
if [[ ! -d "/opt/2fauth" ]]; then
|
|
||||||
msg_error "No ${APP} Installation Found!"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
if check_for_gh_release "2fauth" "Bubka/2FAuth"; then
|
|
||||||
$STD apt-get update
|
|
||||||
$STD apt-get -y upgrade
|
|
||||||
|
|
||||||
msg_info "Creating Backup"
|
|
||||||
mv "/opt/2fauth" "/opt/2fauth-backup"
|
|
||||||
if ! dpkg -l | grep -q 'php8.3'; then
|
|
||||||
cp /etc/nginx/conf.d/2fauth.conf /etc/nginx/conf.d/2fauth.conf.bak
|
|
||||||
fi
|
|
||||||
msg_ok "Backup Created"
|
|
||||||
|
|
||||||
if ! dpkg -l | grep -q 'php8.3'; then
|
|
||||||
$STD apt-get install -y \
|
|
||||||
lsb-release \
|
|
||||||
gnupg2
|
|
||||||
PHP_VERSION="8.3" PHP_MODULE="common,ctype,fileinfo,mysql,cli" PHP_FPM="YES" setup_php
|
|
||||||
sed -i 's/php8.2/php8.3/g' /etc/nginx/conf.d/2fauth.conf
|
|
||||||
fi
|
|
||||||
fetch_and_deploy_gh_release "2fauth" "Bubka/2FAuth"
|
|
||||||
setup_composer
|
|
||||||
mv "/opt/2fauth-backup/.env" "/opt/2fauth/.env"
|
|
||||||
mv "/opt/2fauth-backup/storage" "/opt/2fauth/storage"
|
|
||||||
cd "/opt/2fauth" || return
|
|
||||||
chown -R www-data: "/opt/2fauth"
|
|
||||||
chmod -R 755 "/opt/2fauth"
|
|
||||||
export COMPOSER_ALLOW_SUPERUSER=1
|
|
||||||
$STD composer install --no-dev --prefer-source
|
|
||||||
php artisan 2fauth:install
|
|
||||||
$STD systemctl restart nginx
|
|
||||||
|
|
||||||
msg_info "Cleaning Up"
|
|
||||||
if dpkg -l | grep -q 'php8.2'; then
|
|
||||||
$STD apt-get remove --purge -y php8.2*
|
|
||||||
fi
|
|
||||||
$STD apt-get -y autoremove
|
|
||||||
$STD apt-get -y autoclean
|
|
||||||
msg_ok "Cleanup Completed"
|
|
||||||
msg_ok "Updated Successfully"
|
|
||||||
fi
|
|
||||||
exit
|
|
||||||
}
|
|
||||||
|
|
||||||
start
|
|
||||||
build_container
|
|
||||||
description
|
|
||||||
|
|
||||||
msg_ok "Completed Successfully!\n"
|
|
||||||
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
|
||||||
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
|
|
||||||
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:80${CL}"
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
SCRIPT_DIR="$(dirname "$0")"
|
|
||||||
source "$SCRIPT_DIR/../core/build.func"
|
|
||||||
# Copyright (c) 2021-2025 community-scripts ORG
|
|
||||||
# Author: MickLesk (CanbiZ)
|
|
||||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
|
||||||
# Source: https://actualbudget.org/
|
|
||||||
|
|
||||||
APP="Actual Budget"
|
|
||||||
var_tags="${var_tags:-finance}"
|
|
||||||
var_cpu="${var_cpu:-2}"
|
|
||||||
var_ram="${var_ram:-2048}"
|
|
||||||
var_disk="${var_disk:-4}"
|
|
||||||
var_os="${var_os:-debian}"
|
|
||||||
var_version="${var_version:-12}"
|
|
||||||
var_unprivileged="${var_unprivileged:-1}"
|
|
||||||
|
|
||||||
header_info "$APP"
|
|
||||||
variables
|
|
||||||
color
|
|
||||||
catch_errors
|
|
||||||
|
|
||||||
function update_script() {
|
|
||||||
header_info
|
|
||||||
check_container_storage
|
|
||||||
check_container_resources
|
|
||||||
|
|
||||||
if [[ ! -f /opt/actualbudget_version.txt ]]; then
|
|
||||||
msg_error "No ${APP} Installation Found!"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
NODE_VERSION="22"
|
|
||||||
setup_nodejs
|
|
||||||
RELEASE=$(curl -fsSL https://api.github.com/repos/actualbudget/actual/releases/latest | grep "tag_name" | awk '{print substr($2, 3, length($2)-4) }')
|
|
||||||
if [[ -f /opt/actualbudget-data/config.json ]]; then
|
|
||||||
if [[ ! -f /opt/actualbudget_version.txt ]] || [[ "${RELEASE}" != "$(cat /opt/actualbudget_version.txt)" ]]; then
|
|
||||||
msg_info "Stopping ${APP}"
|
|
||||||
systemctl stop actualbudget
|
|
||||||
msg_ok "${APP} Stopped"
|
|
||||||
|
|
||||||
msg_info "Updating ${APP} to ${RELEASE}"
|
|
||||||
$STD npm update -g @actual-app/sync-server
|
|
||||||
echo "${RELEASE}" >/opt/actualbudget_version.txt
|
|
||||||
msg_ok "Updated ${APP} to ${RELEASE}"
|
|
||||||
|
|
||||||
msg_info "Starting ${APP}"
|
|
||||||
systemctl start actualbudget
|
|
||||||
msg_ok "Restarted ${APP}"
|
|
||||||
else
|
|
||||||
msg_info "${APP} is already up to date"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
msg_info "Old Installation Found, you need to migrate your data and recreate to a new container"
|
|
||||||
msg_info "Please follow the instructions on the ${APP} website to migrate your data"
|
|
||||||
msg_info "https://actualbudget.org/docs/backup-restore/backup"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
exit
|
|
||||||
}
|
|
||||||
|
|
||||||
start
|
|
||||||
build_container
|
|
||||||
description
|
|
||||||
|
|
||||||
msg_ok "Completed Successfully!\n"
|
|
||||||
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
|
||||||
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
|
|
||||||
echo -e "${TAB}${GATEWAY}${BGN}https://${IP}:5006${CL}"
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
SCRIPT_DIR="$(dirname "$0")"
|
|
||||||
source "$SCRIPT_DIR/../core/build.func"
|
|
||||||
# Copyright (c) 2021-2025 tteck
|
|
||||||
# Author: tteck (tteckster)
|
|
||||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
|
||||||
# Source: https://adguard.com/
|
|
||||||
|
|
||||||
APP="Adguard"
|
|
||||||
var_tags="${var_tags:-adblock}"
|
|
||||||
var_cpu="${var_cpu:-1}"
|
|
||||||
var_ram="${var_ram:-512}"
|
|
||||||
var_disk="${var_disk:-2}"
|
|
||||||
var_os="${var_os:-debian}"
|
|
||||||
var_version="${var_version:-12}"
|
|
||||||
var_unprivileged="${var_unprivileged:-1}"
|
|
||||||
|
|
||||||
header_info "$APP"
|
|
||||||
variables
|
|
||||||
color
|
|
||||||
catch_errors
|
|
||||||
|
|
||||||
function update_script() {
|
|
||||||
header_info
|
|
||||||
check_container_storage
|
|
||||||
check_container_resources
|
|
||||||
if [[ ! -d /opt/AdGuardHome ]]; then
|
|
||||||
msg_error "No ${APP} Installation Found!"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
msg_error "Adguard Home should be updated via the user interface."
|
|
||||||
exit
|
|
||||||
}
|
|
||||||
|
|
||||||
start
|
|
||||||
build_container
|
|
||||||
description
|
|
||||||
|
|
||||||
msg_ok "Completed Successfully!\n"
|
|
||||||
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
|
||||||
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
|
|
||||||
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:3000${CL}"
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
SCRIPT_DIR="$(dirname "$0")"
|
|
||||||
source "$SCRIPT_DIR/../core/build.func"
|
|
||||||
# Copyright (c) 2021-2025 community-scripts ORG
|
|
||||||
# Author: MickLesk (CanbiZ)
|
|
||||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
|
||||||
# Source: https://adguardhome.com/
|
|
||||||
|
|
||||||
APP="Alpine-AdGuard"
|
|
||||||
var_tags="${var_tags:-alpine;adblock}"
|
|
||||||
var_cpu="${var_cpu:-1}"
|
|
||||||
var_ram="${var_ram:-256}"
|
|
||||||
var_disk="${var_disk:-1}"
|
|
||||||
var_os="${var_os:-alpine}"
|
|
||||||
var_version="${var_version:-3.22}"
|
|
||||||
var_unprivileged="${var_unprivileged:-1}"
|
|
||||||
|
|
||||||
header_info "$APP"
|
|
||||||
variables
|
|
||||||
color
|
|
||||||
catch_errors
|
|
||||||
|
|
||||||
function update_script() {
|
|
||||||
header_info
|
|
||||||
msg_info "Updating Alpine Packages"
|
|
||||||
$STD apk -U upgrade
|
|
||||||
msg_ok "Updated Alpine Packages"
|
|
||||||
|
|
||||||
msg_info "Updating AdGuard Home"
|
|
||||||
$STD /opt/AdGuardHome/AdGuardHome --update
|
|
||||||
msg_ok "Updated AdGuard Home"
|
|
||||||
|
|
||||||
msg_info "Restarting AdGuard Home"
|
|
||||||
$STD rc-service adguardhome restart
|
|
||||||
msg_ok "Restarted AdGuard Home"
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
start
|
|
||||||
build_container
|
|
||||||
description
|
|
||||||
|
|
||||||
msg_ok "Completed Successfully!\n"
|
|
||||||
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
|
||||||
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
|
|
||||||
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:3000${CL}"
|
|
||||||
6
scripts/ct/debian.sh
Executable file → Normal file
6
scripts/ct/debian.sh
Executable file → Normal file
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
SCRIPT_DIR="$(dirname "$0")"
|
SCRIPT_DIR="$(dirname "$0")"
|
||||||
source "$SCRIPT_DIR/../core/build.func"
|
source "$SCRIPT_DIR/../core/build.func"
|
||||||
# Copyright (c) 2021-2025 tteck
|
# Copyright (c) 2021-2025 tteck
|
||||||
# Author: tteck (tteckster)
|
# Author: tteck (tteckster)
|
||||||
@@ -37,7 +37,7 @@ function update_script() {
|
|||||||
|
|
||||||
start
|
start
|
||||||
build_container
|
build_container
|
||||||
description
|
|
||||||
|
|
||||||
msg_ok "Completed Successfully!\n"
|
msg_ok "Completed Successfully!\n"
|
||||||
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# Copyright (c) 2021-2025 community-scripts ORG
|
|
||||||
# Author: jkrgr0
|
|
||||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
|
||||||
# Source: https://docs.2fauth.app/
|
|
||||||
|
|
||||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
|
||||||
color
|
|
||||||
verb_ip6
|
|
||||||
catch_errors
|
|
||||||
setting_up_container
|
|
||||||
network_check
|
|
||||||
update_os
|
|
||||||
|
|
||||||
msg_info "Installing Dependencies"
|
|
||||||
$STD apt-get install -y \
|
|
||||||
lsb-release \
|
|
||||||
nginx
|
|
||||||
msg_ok "Installed Dependencies"
|
|
||||||
|
|
||||||
PHP_VERSION="8.3" PHP_MODULE="common,ctype,fileinfo,mysql,cli" PHP_FPM="YES" setup_php
|
|
||||||
setup_composer
|
|
||||||
setup_mariadb
|
|
||||||
|
|
||||||
msg_info "Setting up Database"
|
|
||||||
DB_NAME=2fauth_db
|
|
||||||
DB_USER=2fauth
|
|
||||||
DB_PASS=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c13)
|
|
||||||
$STD mariadb -u root -e "CREATE DATABASE $DB_NAME;"
|
|
||||||
$STD mariadb -u root -e "CREATE USER '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASS';"
|
|
||||||
$STD mariadb -u root -e "GRANT ALL ON $DB_NAME.* TO '$DB_USER'@'localhost'; FLUSH PRIVILEGES;"
|
|
||||||
{
|
|
||||||
echo "2FAuth Credentials"
|
|
||||||
echo "Database User: $DB_USER"
|
|
||||||
echo "Database Password: $DB_PASS"
|
|
||||||
echo "Database Name: $DB_NAME"
|
|
||||||
} >>~/2FAuth.creds
|
|
||||||
msg_ok "Set up Database"
|
|
||||||
|
|
||||||
fetch_and_deploy_gh_release "2fauth" "Bubka/2FAuth"
|
|
||||||
|
|
||||||
msg_info "Setup 2FAuth"
|
|
||||||
cd /opt/2fauth
|
|
||||||
cp .env.example .env
|
|
||||||
IPADDRESS=$(hostname -I | awk '{print $1}')
|
|
||||||
sed -i -e "s|^APP_URL=.*|APP_URL=http://$IPADDRESS|" \
|
|
||||||
-e "s|^DB_CONNECTION=$|DB_CONNECTION=mysql|" \
|
|
||||||
-e "s|^DB_DATABASE=$|DB_DATABASE=$DB_NAME|" \
|
|
||||||
-e "s|^DB_HOST=$|DB_HOST=127.0.0.1|" \
|
|
||||||
-e "s|^DB_PORT=$|DB_PORT=3306|" \
|
|
||||||
-e "s|^DB_USERNAME=$|DB_USERNAME=$DB_USER|" \
|
|
||||||
-e "s|^DB_PASSWORD=$|DB_PASSWORD=$DB_PASS|" .env
|
|
||||||
export COMPOSER_ALLOW_SUPERUSER=1
|
|
||||||
$STD composer update --no-plugins --no-scripts
|
|
||||||
$STD composer install --no-dev --prefer-source --no-plugins --no-scripts
|
|
||||||
$STD php artisan key:generate --force
|
|
||||||
$STD php artisan migrate:refresh
|
|
||||||
$STD php artisan passport:install -q -n
|
|
||||||
$STD php artisan storage:link
|
|
||||||
$STD php artisan config:cache
|
|
||||||
chown -R www-data: /opt/2fauth
|
|
||||||
chmod -R 755 /opt/2fauth
|
|
||||||
msg_ok "Setup 2fauth"
|
|
||||||
|
|
||||||
msg_info "Configure Service"
|
|
||||||
cat <<EOF >/etc/nginx/conf.d/2fauth.conf
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
root /opt/2fauth/public;
|
|
||||||
server_name $IPADDRESS;
|
|
||||||
index index.php;
|
|
||||||
charset utf-8;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files \$uri \$uri/ /index.php?\$query_string;
|
|
||||||
}
|
|
||||||
|
|
||||||
location = /favicon.ico { access_log off; log_not_found off; }
|
|
||||||
location = /robots.txt { access_log off; log_not_found off; }
|
|
||||||
|
|
||||||
error_page 404 /index.php;
|
|
||||||
|
|
||||||
location ~ \.php\$ {
|
|
||||||
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
|
|
||||||
fastcgi_param SCRIPT_FILENAME \$realpath_root\$fastcgi_script_name;
|
|
||||||
include fastcgi_params;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ /\.(?!well-known).* {
|
|
||||||
deny all;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
systemctl reload nginx
|
|
||||||
msg_ok "Configured Service"
|
|
||||||
|
|
||||||
motd_ssh
|
|
||||||
customize
|
|
||||||
|
|
||||||
msg_info "Cleaning up"
|
|
||||||
$STD apt-get -y autoremove
|
|
||||||
$STD apt-get -y autoclean
|
|
||||||
msg_ok "Cleaned"
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# Copyright (c) 2021-2025 community-scripts ORG
|
|
||||||
# Author: MickLesk (CanbiZ)
|
|
||||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
|
||||||
# Source: https://actualbudget.org/
|
|
||||||
|
|
||||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
|
||||||
color
|
|
||||||
verb_ip6
|
|
||||||
catch_errors
|
|
||||||
setting_up_container
|
|
||||||
network_check
|
|
||||||
update_os
|
|
||||||
|
|
||||||
msg_info "Installing Dependencies"
|
|
||||||
$STD apt-get install -y \
|
|
||||||
make \
|
|
||||||
g++
|
|
||||||
msg_ok "Installed Dependencies"
|
|
||||||
|
|
||||||
msg_info "Installing Actual Budget"
|
|
||||||
cd /opt
|
|
||||||
RELEASE=$(curl -fsSL https://api.github.com/repos/actualbudget/actual/releases/latest | grep "tag_name" | awk '{print substr($2, 3, length($2)-4) }')
|
|
||||||
NODE_VERSION="22"
|
|
||||||
setup_nodejs
|
|
||||||
mkdir -p /opt/actualbudget-data/{server-files,upload,migrate,user-files,migrations,config}
|
|
||||||
chown -R root:root /opt/actualbudget-data
|
|
||||||
chmod -R 755 /opt/actualbudget-data
|
|
||||||
|
|
||||||
cat <<EOF >/opt/actualbudget-data/config.json
|
|
||||||
{
|
|
||||||
"port": 5006,
|
|
||||||
"hostname": "::",
|
|
||||||
"serverFiles": "/opt/actualbudget-data/server-files",
|
|
||||||
"userFiles": "/opt/actualbudget-data/user-files",
|
|
||||||
"trustedProxies": [
|
|
||||||
"10.0.0.0/8",
|
|
||||||
"172.16.0.0/12",
|
|
||||||
"192.168.0.0/16",
|
|
||||||
"127.0.0.0/8",
|
|
||||||
"::1/128",
|
|
||||||
"fc00::/7"
|
|
||||||
],
|
|
||||||
"https": {
|
|
||||||
"key": "/opt/actualbudget/selfhost.key",
|
|
||||||
"cert": "/opt/actualbudget/selfhost.crt"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
mkdir -p /opt/actualbudget
|
|
||||||
cd /opt/actualbudget
|
|
||||||
$STD npm install --location=global @actual-app/sync-server
|
|
||||||
$STD openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout selfhost.key -out selfhost.crt <<EOF
|
|
||||||
US
|
|
||||||
California
|
|
||||||
San Francisco
|
|
||||||
My Organization
|
|
||||||
My Unit
|
|
||||||
localhost
|
|
||||||
myemail@example.com
|
|
||||||
EOF
|
|
||||||
echo "${RELEASE}" >"/opt/actualbudget_version.txt"
|
|
||||||
msg_ok "Installed Actual Budget"
|
|
||||||
|
|
||||||
msg_info "Creating Service"
|
|
||||||
cat <<EOF >/etc/systemd/system/actualbudget.service
|
|
||||||
[Unit]
|
|
||||||
Description=Actual Budget Service
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=root
|
|
||||||
Group=root
|
|
||||||
WorkingDirectory=/opt/actualbudget
|
|
||||||
Environment=ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB=20
|
|
||||||
Environment=ACTUAL_UPLOAD_SYNC_ENCRYPTED_FILE_SYNC_SIZE_LIMIT_MB=50
|
|
||||||
Environment=ACTUAL_UPLOAD_FILE_SYNC_SIZE_LIMIT_MB=20
|
|
||||||
ExecStart=/usr/bin/actual-server --config /opt/actualbudget-data/config.json
|
|
||||||
Restart=always
|
|
||||||
RestartSec=10
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
systemctl enable -q --now actualbudget
|
|
||||||
msg_ok "Created Service"
|
|
||||||
|
|
||||||
motd_ssh
|
|
||||||
customize
|
|
||||||
|
|
||||||
msg_info "Cleaning up"
|
|
||||||
$STD apt-get -y autoremove
|
|
||||||
$STD apt-get -y autoclean
|
|
||||||
msg_ok "Cleaned"
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# Copyright (c) 2021-2025 tteck
|
|
||||||
# Author: tteck (tteckster)
|
|
||||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
|
||||||
# Source: https://adguard.com/
|
|
||||||
|
|
||||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
|
||||||
color
|
|
||||||
verb_ip6
|
|
||||||
catch_errors
|
|
||||||
setting_up_container
|
|
||||||
network_check
|
|
||||||
update_os
|
|
||||||
|
|
||||||
msg_info "Installing AdGuard Home"
|
|
||||||
$STD tar zxvf <(curl -fsSL https://static.adtidy.org/adguardhome/release/AdGuardHome_linux_amd64.tar.gz) -C /opt
|
|
||||||
msg_ok "Installed AdGuard Home"
|
|
||||||
|
|
||||||
msg_info "Creating Service"
|
|
||||||
cat <<EOF >/etc/systemd/system/AdGuardHome.service
|
|
||||||
[Unit]
|
|
||||||
Description=AdGuard Home: Network-level blocker
|
|
||||||
ConditionFileIsExecutable=/opt/AdGuardHome/AdGuardHome
|
|
||||||
After=syslog.target network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
StartLimitInterval=5
|
|
||||||
StartLimitBurst=10
|
|
||||||
ExecStart=/opt/AdGuardHome/AdGuardHome "-s" "run"
|
|
||||||
WorkingDirectory=/opt/AdGuardHome
|
|
||||||
StandardOutput=file:/var/log/AdGuardHome.out
|
|
||||||
StandardError=file:/var/log/AdGuardHome.err
|
|
||||||
Restart=always
|
|
||||||
RestartSec=10
|
|
||||||
EnvironmentFile=-/etc/sysconfig/AdGuardHome
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
systemctl enable -q --now AdGuardHome
|
|
||||||
msg_ok "Created Service"
|
|
||||||
|
|
||||||
motd_ssh
|
|
||||||
customize
|
|
||||||
|
|
||||||
msg_info "Cleaning up"
|
|
||||||
$STD apt-get -y autoremove
|
|
||||||
$STD apt-get -y autoclean
|
|
||||||
msg_ok "Cleaned"
|
|
||||||
@@ -19,4 +19,4 @@ customize
|
|||||||
msg_info "Cleaning up"
|
msg_info "Cleaning up"
|
||||||
$STD apt-get -y autoremove
|
$STD apt-get -y autoremove
|
||||||
$STD apt-get -y autoclean
|
$STD apt-get -y autoclean
|
||||||
msg_ok "Cleaned"
|
msg_ok "Cleaned"
|
||||||
|
|||||||
@@ -66,15 +66,12 @@ class ScriptExecutionHandler {
|
|||||||
|
|
||||||
async handleMessage(ws, message) {
|
async handleMessage(ws, message) {
|
||||||
const { action, scriptPath, executionId, input } = message;
|
const { action, scriptPath, executionId, input } = message;
|
||||||
console.log('Handling message:', { action, scriptPath, executionId });
|
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'start':
|
case 'start':
|
||||||
if (scriptPath && executionId) {
|
if (scriptPath && executionId) {
|
||||||
console.log('Starting script execution for:', scriptPath);
|
|
||||||
await this.startScriptExecution(ws, scriptPath, executionId);
|
await this.startScriptExecution(ws, scriptPath, executionId);
|
||||||
} else {
|
} else {
|
||||||
console.log('Missing scriptPath or executionId');
|
|
||||||
this.sendMessage(ws, {
|
this.sendMessage(ws, {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
data: 'Missing scriptPath or executionId',
|
data: 'Missing scriptPath or executionId',
|
||||||
|
|||||||
152
src/app/_components/DiffViewer.tsx
Normal file
152
src/app/_components/DiffViewer.tsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { api } from '~/trpc/react';
|
||||||
|
|
||||||
|
interface DiffViewerProps {
|
||||||
|
scriptSlug: string;
|
||||||
|
filePath: string;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DiffViewer({ scriptSlug, filePath, isOpen, onClose }: DiffViewerProps) {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
// Get diff content
|
||||||
|
const { data: diffData, refetch } = api.scripts.getScriptDiff.useQuery(
|
||||||
|
{ slug: scriptSlug, filePath },
|
||||||
|
{ enabled: isOpen && !!scriptSlug && !!filePath }
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleBackdropClick = (e: React.MouseEvent) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
await refetch();
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const renderDiffLine = (line: string, index: number) => {
|
||||||
|
const lineNumber = line.match(/^([+-]?\d+):/)?.[1];
|
||||||
|
const content = line.replace(/^[+-]?\d+:\s*/, '');
|
||||||
|
const isAdded = line.startsWith('+');
|
||||||
|
const isRemoved = line.startsWith('-');
|
||||||
|
const isContext = line.startsWith(' ');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`flex font-mono text-sm ${
|
||||||
|
isAdded
|
||||||
|
? 'bg-green-50 text-green-800 border-l-4 border-green-400'
|
||||||
|
: isRemoved
|
||||||
|
? 'bg-red-50 text-red-800 border-l-4 border-red-400'
|
||||||
|
: 'bg-gray-50 text-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="w-16 text-right pr-2 text-gray-500 select-none">
|
||||||
|
{lineNumber}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 pl-2">
|
||||||
|
<span className={isAdded ? 'text-green-600' : isRemoved ? 'text-red-600' : ''}>
|
||||||
|
{isAdded ? '+' : isRemoved ? '-' : ' '}
|
||||||
|
</span>
|
||||||
|
<span className="whitespace-pre-wrap">{content}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||||
|
onClick={handleBackdropClick}
|
||||||
|
>
|
||||||
|
<div className="bg-white rounded-lg shadow-xl max-w-6xl w-full max-h-[90vh] overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold text-gray-900">Script Diff</h2>
|
||||||
|
<p className="text-sm text-gray-600">{filePath}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={handleRefresh}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="px-3 py-1 text-sm bg-blue-100 text-blue-700 rounded hover:bg-blue-200 transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isLoading ? 'Refreshing...' : 'Refresh'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Legend */}
|
||||||
|
<div className="px-4 py-2 bg-gray-50 border-b border-gray-200">
|
||||||
|
<div className="flex items-center space-x-4 text-sm">
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<div className="w-3 h-3 bg-green-100 border border-green-300"></div>
|
||||||
|
<span className="text-green-700">Added (Remote)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<div className="w-3 h-3 bg-red-100 border border-red-300"></div>
|
||||||
|
<span className="text-red-700">Removed (Local)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<div className="w-3 h-3 bg-gray-100 border border-gray-300"></div>
|
||||||
|
<span className="text-gray-700">Unchanged</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Diff Content */}
|
||||||
|
<div className="overflow-y-auto max-h-[calc(90vh-120px)]">
|
||||||
|
{diffData?.success ? (
|
||||||
|
diffData.diff ? (
|
||||||
|
<div className="divide-y divide-gray-200">
|
||||||
|
{diffData.diff.split('\n').map((line, index) =>
|
||||||
|
line.trim() ? renderDiffLine(line, index) : null
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="p-8 text-center text-gray-500">
|
||||||
|
<svg className="w-12 h-12 mx-auto mb-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<p>No differences found</p>
|
||||||
|
<p className="text-sm">The local and remote files are identical</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : diffData?.error ? (
|
||||||
|
<div className="p-8 text-center text-red-500">
|
||||||
|
<svg className="w-12 h-12 mx-auto mb-4 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<p>Error loading diff</p>
|
||||||
|
<p className="text-sm">{diffData.error}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="p-8 text-center text-gray-500">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||||
|
<p>Loading diff...</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { api } from '~/trpc/react';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
export function RepoStatus() {
|
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
|
||||||
|
|
||||||
const { data: repoStatus, isLoading: statusLoading, refetch: refetchStatus } = api.scripts.getRepoStatus.useQuery();
|
|
||||||
const updateRepoMutation = api.scripts.updateRepo.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
setIsUpdating(false);
|
|
||||||
refetchStatus();
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
setIsUpdating(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleUpdate = async () => {
|
|
||||||
setIsUpdating(true);
|
|
||||||
updateRepoMutation.mutate();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (statusLoading) {
|
|
||||||
return (
|
|
||||||
<div className="bg-white rounded-lg border border-gray-200 p-4 shadow-sm">
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<div className="text-gray-600">Loading repository status...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!repoStatus) {
|
|
||||||
return (
|
|
||||||
<div className="bg-white rounded-lg border border-gray-200 p-4 shadow-sm">
|
|
||||||
<div className="text-red-600">Failed to load repository status</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStatusColor = () => {
|
|
||||||
if (!repoStatus.isRepo) return 'text-gray-500';
|
|
||||||
if (repoStatus.isBehind) return 'text-yellow-600';
|
|
||||||
return 'text-green-600';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusText = () => {
|
|
||||||
if (!repoStatus.isRepo) return 'No repository found';
|
|
||||||
if (repoStatus.isBehind) return 'Behind remote';
|
|
||||||
return 'Up to date';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusIcon = () => {
|
|
||||||
if (!repoStatus.isRepo) return '❓';
|
|
||||||
if (repoStatus.isBehind) return '⚠️';
|
|
||||||
return '✅';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white rounded-lg border border-gray-200 p-4 shadow-sm">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<span className="text-2xl">{getStatusIcon()}</span>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-gray-800">Repository Status</h3>
|
|
||||||
<div className={`text-sm font-medium ${getStatusColor()}`}>
|
|
||||||
{getStatusText()}
|
|
||||||
</div>
|
|
||||||
{repoStatus.isRepo && (
|
|
||||||
<div className="text-xs text-gray-500 mt-1">
|
|
||||||
<p>Branch: {repoStatus.branch || 'Unknown'}</p>
|
|
||||||
{repoStatus.lastCommit && (
|
|
||||||
<p>Last commit: {repoStatus.lastCommit.substring(0, 8)}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<button
|
|
||||||
onClick={() => refetchStatus()}
|
|
||||||
className="px-3 py-1 text-sm bg-blue-100 text-blue-700 rounded hover:bg-blue-200 transition-colors"
|
|
||||||
>
|
|
||||||
🔄 Refresh
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={handleUpdate}
|
|
||||||
disabled={isUpdating || !repoStatus.isRepo}
|
|
||||||
className={`px-4 py-2 text-sm font-medium rounded transition-colors ${
|
|
||||||
isUpdating || !repoStatus.isRepo
|
|
||||||
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
|
||||||
: 'bg-green-600 text-white hover:bg-green-700'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isUpdating ? '⏳ Updating...' : '⬇️ Update Repo'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{updateRepoMutation.isSuccess && (
|
|
||||||
<div className="mt-3 p-2 bg-green-50 border border-green-200 rounded text-sm text-green-700">
|
|
||||||
✅ {updateRepoMutation.data?.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{updateRepoMutation.isError && (
|
|
||||||
<div className="mt-3 p-2 bg-red-50 border border-red-200 rounded text-sm text-red-700">
|
|
||||||
❌ {updateRepoMutation.error?.message || 'Failed to update repository'}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -17,10 +17,10 @@ export function ScriptCard({ script, onClick }: ScriptCardProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200 cursor-pointer border border-gray-200 hover:border-blue-300"
|
className="bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200 cursor-pointer border border-gray-200 hover:border-blue-300 h-full flex flex-col"
|
||||||
onClick={() => onClick(script)}
|
onClick={() => onClick(script)}
|
||||||
>
|
>
|
||||||
<div className="p-6">
|
<div className="p-6 flex-1 flex flex-col">
|
||||||
{/* Header with logo and name */}
|
{/* Header with logo and name */}
|
||||||
<div className="flex items-start space-x-4 mb-4">
|
<div className="flex items-start space-x-4 mb-4">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
@@ -43,31 +43,48 @@ export function ScriptCard({ script, onClick }: ScriptCardProps) {
|
|||||||
<h3 className="text-lg font-semibold text-gray-900 truncate">
|
<h3 className="text-lg font-semibold text-gray-900 truncate">
|
||||||
{script.name || 'Unnamed Script'}
|
{script.name || 'Unnamed Script'}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex items-center space-x-2 mt-1">
|
<div className="mt-2 space-y-2">
|
||||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
{/* Type and Updateable status on first row */}
|
||||||
script.type === 'ct'
|
<div className="flex items-center space-x-2">
|
||||||
? 'bg-blue-100 text-blue-800'
|
<span className={`inline-flex items-center px-2 py-1 rounded text-xs font-medium ${
|
||||||
: 'bg-gray-100 text-gray-800'
|
script.type === 'ct'
|
||||||
}`}>
|
? 'bg-blue-100 text-blue-800'
|
||||||
{script.type?.toUpperCase() || 'UNKNOWN'}
|
: script.type === 'addon'
|
||||||
</span>
|
? 'bg-purple-100 text-purple-800'
|
||||||
{script.updateable && (
|
: 'bg-gray-100 text-gray-800'
|
||||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
}`}>
|
||||||
Updateable
|
{script.type?.toUpperCase() || 'UNKNOWN'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
{script.updateable && (
|
||||||
|
<span className="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-amber-100 text-amber-800">
|
||||||
|
Updateable
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Download Status */}
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<div className={`w-2 h-2 rounded-full ${
|
||||||
|
script.isDownloaded ? 'bg-green-500' : 'bg-red-500'
|
||||||
|
}`}></div>
|
||||||
|
<span className={`text-xs font-medium ${
|
||||||
|
script.isDownloaded ? 'text-green-700' : 'text-red-700'
|
||||||
|
}`}>
|
||||||
|
{script.isDownloaded ? 'Downloaded' : 'Not Downloaded'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<p className="text-gray-600 text-sm line-clamp-3 mb-4">
|
<p className="text-gray-600 text-sm line-clamp-3 mb-4 flex-1">
|
||||||
{script.description || 'No description available'}
|
{script.description || 'No description available'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Footer with website link */}
|
{/* Footer with website link */}
|
||||||
{script.website && (
|
{script.website && (
|
||||||
<div className="flex items-center justify-between">
|
<div className="mt-auto">
|
||||||
<a
|
<a
|
||||||
href={script.website}
|
href={script.website}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { api } from '~/trpc/react';
|
import { api } from '~/trpc/react';
|
||||||
import type { Script } from '~/types/script';
|
import type { Script } from '~/types/script';
|
||||||
|
import { DiffViewer } from './DiffViewer';
|
||||||
|
import { TextViewer } from './TextViewer';
|
||||||
|
|
||||||
interface ScriptDetailModalProps {
|
interface ScriptDetailModalProps {
|
||||||
script: Script | null;
|
script: Script | null;
|
||||||
@@ -15,6 +17,9 @@ export function ScriptDetailModal({ script, isOpen, onClose, onInstallScript }:
|
|||||||
const [imageError, setImageError] = useState(false);
|
const [imageError, setImageError] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [loadMessage, setLoadMessage] = useState<string | null>(null);
|
const [loadMessage, setLoadMessage] = useState<string | null>(null);
|
||||||
|
const [diffViewerOpen, setDiffViewerOpen] = useState(false);
|
||||||
|
const [selectedDiffFile, setSelectedDiffFile] = useState<string | null>(null);
|
||||||
|
const [textViewerOpen, setTextViewerOpen] = useState(false);
|
||||||
|
|
||||||
// Check if script files exist locally
|
// Check if script files exist locally
|
||||||
const { data: scriptFilesData, refetch: refetchScriptFiles } = api.scripts.checkScriptFiles.useQuery(
|
const { data: scriptFilesData, refetch: refetchScriptFiles } = api.scripts.checkScriptFiles.useQuery(
|
||||||
@@ -22,6 +27,12 @@ export function ScriptDetailModal({ script, isOpen, onClose, onInstallScript }:
|
|||||||
{ enabled: !!script && isOpen }
|
{ enabled: !!script && isOpen }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Compare local and remote script content
|
||||||
|
const { data: comparisonData, refetch: refetchComparison } = api.scripts.compareScriptContent.useQuery(
|
||||||
|
{ slug: script?.slug ?? '' },
|
||||||
|
{ enabled: !!script && isOpen && scriptFilesData?.success && (scriptFilesData.ctExists || scriptFilesData.installExists) }
|
||||||
|
);
|
||||||
|
|
||||||
// Load script mutation
|
// Load script mutation
|
||||||
const loadScriptMutation = api.scripts.loadScript.useMutation({
|
const loadScriptMutation = api.scripts.loadScript.useMutation({
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
@@ -29,8 +40,9 @@ export function ScriptDetailModal({ script, isOpen, onClose, onInstallScript }:
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
const message = 'message' in data ? data.message : 'Script loaded successfully';
|
const message = 'message' in data ? data.message : 'Script loaded successfully';
|
||||||
setLoadMessage(`✅ ${message}`);
|
setLoadMessage(`✅ ${message}`);
|
||||||
// Refetch script files status to update the UI
|
// Refetch script files status and comparison data to update the UI
|
||||||
refetchScriptFiles();
|
refetchScriptFiles();
|
||||||
|
refetchComparison();
|
||||||
} else {
|
} else {
|
||||||
const error = 'error' in data ? data.error : 'Failed to load script';
|
const error = 'error' in data ? data.error : 'Failed to load script';
|
||||||
setLoadMessage(`❌ ${error}`);
|
setLoadMessage(`❌ ${error}`);
|
||||||
@@ -78,6 +90,15 @@ export function ScriptDetailModal({ script, isOpen, onClose, onInstallScript }:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleShowDiff = (filePath: string) => {
|
||||||
|
setSelectedDiffFile(filePath);
|
||||||
|
setDiffViewerOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewScript = () => {
|
||||||
|
setTextViewerOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||||
@@ -137,31 +158,96 @@ export function ScriptDetailModal({ script, isOpen, onClose, onInstallScript }:
|
|||||||
<span>Install</span>
|
<span>Install</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* View Button - only show if script files exist */}
|
||||||
|
{scriptFilesData?.success && (scriptFilesData.ctExists || scriptFilesData.installExists) && (
|
||||||
|
<button
|
||||||
|
onClick={handleViewScript}
|
||||||
|
className="flex items-center space-x-2 px-4 py-2 rounded-lg font-medium transition-colors bg-purple-600 text-white hover:bg-purple-700"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||||
|
</svg>
|
||||||
|
<span>View</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Load Script Button */}
|
{/* Load/Update Script Button */}
|
||||||
<button
|
{(() => {
|
||||||
onClick={handleLoadScript}
|
const hasLocalFiles = scriptFilesData?.success && (scriptFilesData.ctExists || scriptFilesData.installExists);
|
||||||
disabled={isLoading}
|
const hasDifferences = comparisonData?.success && comparisonData.hasDifferences;
|
||||||
className={`flex items-center space-x-2 px-4 py-2 rounded-lg font-medium transition-colors ${
|
const isUpToDate = hasLocalFiles && !hasDifferences;
|
||||||
isLoading
|
|
||||||
? 'bg-gray-400 text-white cursor-not-allowed'
|
if (!hasLocalFiles) {
|
||||||
: 'bg-green-600 text-white hover:bg-green-700'
|
// No local files - show Load Script button
|
||||||
}`}
|
return (
|
||||||
>
|
<button
|
||||||
{isLoading ? (
|
onClick={handleLoadScript}
|
||||||
<>
|
disabled={isLoading}
|
||||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
className={`flex items-center space-x-2 px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||||
<span>Loading...</span>
|
isLoading
|
||||||
</>
|
? 'bg-gray-400 text-white cursor-not-allowed'
|
||||||
) : (
|
: 'bg-green-600 text-white hover:bg-green-700'
|
||||||
<>
|
}`}
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
{isLoading ? (
|
||||||
</svg>
|
<>
|
||||||
<span>Load Script</span>
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||||
</>
|
<span>Loading...</span>
|
||||||
)}
|
</>
|
||||||
</button>
|
) : (
|
||||||
|
<>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
<span>Load Script</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
} else if (isUpToDate) {
|
||||||
|
// Local files exist and are up to date - show disabled Update button
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
disabled
|
||||||
|
className="flex items-center space-x-2 px-4 py-2 rounded-lg font-medium transition-colors bg-gray-400 text-white cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||||
|
</svg>
|
||||||
|
<span>Up to Date</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Local files exist but have differences - show Update button
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={handleLoadScript}
|
||||||
|
disabled={isLoading}
|
||||||
|
className={`flex items-center space-x-2 px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||||
|
isLoading
|
||||||
|
? 'bg-gray-400 text-white cursor-not-allowed'
|
||||||
|
: 'bg-orange-600 text-white hover:bg-orange-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||||
|
<span>Updating...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||||
|
</svg>
|
||||||
|
<span>Update Script</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})()}
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
@@ -192,12 +278,40 @@ export function ScriptDetailModal({ script, isOpen, onClose, onInstallScript }:
|
|||||||
<div className={`w-2 h-2 rounded-full ${scriptFilesData.installExists ? 'bg-green-500' : 'bg-gray-300'}`}></div>
|
<div className={`w-2 h-2 rounded-full ${scriptFilesData.installExists ? 'bg-green-500' : 'bg-gray-300'}`}></div>
|
||||||
<span>Install Script: {scriptFilesData.installExists ? 'Available' : 'Not loaded'}</span>
|
<span>Install Script: {scriptFilesData.installExists ? 'Available' : 'Not loaded'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{scriptFilesData?.success && (scriptFilesData.ctExists || scriptFilesData.installExists) && comparisonData?.success && (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className={`w-2 h-2 rounded-full ${comparisonData.hasDifferences ? 'bg-orange-500' : 'bg-green-500'}`}></div>
|
||||||
|
<span>Status: {comparisonData.hasDifferences ? 'Update available' : 'Up to date'}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{scriptFilesData.files.length > 0 && (
|
{scriptFilesData.files.length > 0 && (
|
||||||
<div className="mt-2 text-xs text-gray-600">
|
<div className="mt-2 text-xs text-gray-600">
|
||||||
Files: {scriptFilesData.files.join(', ')}
|
Files: {scriptFilesData.files.join(', ')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{scriptFilesData?.success && (scriptFilesData.ctExists || scriptFilesData.installExists) &&
|
||||||
|
comparisonData?.success && comparisonData.hasDifferences && comparisonData.differences.length > 0 && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="text-xs text-orange-600 mb-2">
|
||||||
|
Differences in: {comparisonData.differences.join(', ')}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{comparisonData.differences.map((filePath, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => handleShowDiff(filePath)}
|
||||||
|
className="px-2 py-1 text-xs bg-orange-100 text-orange-700 rounded hover:bg-orange-200 transition-colors flex items-center space-x-1"
|
||||||
|
>
|
||||||
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
<span>Show Diff: {filePath}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -372,6 +486,28 @@ export function ScriptDetailModal({ script, isOpen, onClose, onInstallScript }:
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Diff Viewer Modal */}
|
||||||
|
{selectedDiffFile && (
|
||||||
|
<DiffViewer
|
||||||
|
scriptSlug={script.slug}
|
||||||
|
filePath={selectedDiffFile}
|
||||||
|
isOpen={diffViewerOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setDiffViewerOpen(false);
|
||||||
|
setSelectedDiffFile(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Text Viewer Modal */}
|
||||||
|
{script && (
|
||||||
|
<TextViewer
|
||||||
|
scriptName={script.install_methods?.find(method => method.script?.startsWith('ct/'))?.script?.split('/').pop() || `${script.slug}.sh`}
|
||||||
|
isOpen={textViewerOpen}
|
||||||
|
onClose={() => setTextViewerOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { api } from '~/trpc/react';
|
import { api } from '~/trpc/react';
|
||||||
import { ScriptCard } from './ScriptCard';
|
import { ScriptCard } from './ScriptCard';
|
||||||
import { ScriptDetailModal } from './ScriptDetailModal';
|
import { ScriptDetailModal } from './ScriptDetailModal';
|
||||||
import type { ScriptCard as ScriptCardType, Script } from '~/types/script';
|
|
||||||
|
|
||||||
interface ScriptsGridProps {
|
interface ScriptsGridProps {
|
||||||
onInstallScript?: (scriptPath: string, scriptName: string) => void;
|
onInstallScript?: (scriptPath: string, scriptName: string) => void;
|
||||||
@@ -13,22 +13,86 @@ interface ScriptsGridProps {
|
|||||||
export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
||||||
const [selectedSlug, setSelectedSlug] = useState<string | null>(null);
|
const [selectedSlug, setSelectedSlug] = useState<string | null>(null);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
const { data: scriptCardsData, isLoading, error, refetch } = api.scripts.getScriptCards.useQuery();
|
const { data: scriptCardsData, isLoading: githubLoading, error: githubError, refetch } = api.scripts.getScriptCards.useQuery();
|
||||||
|
const { data: localScriptsData, isLoading: localLoading, error: localError } = api.scripts.getCtScripts.useQuery();
|
||||||
const { data: scriptData } = api.scripts.getScriptBySlug.useQuery(
|
const { data: scriptData } = api.scripts.getScriptBySlug.useQuery(
|
||||||
{ slug: selectedSlug ?? '' },
|
{ slug: selectedSlug ?? '' },
|
||||||
{ enabled: !!selectedSlug }
|
{ enabled: !!selectedSlug }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Debug logging
|
// Get GitHub scripts with download status
|
||||||
console.log('ScriptsGrid render:', {
|
const combinedScripts = React.useMemo(() => {
|
||||||
isLoading,
|
const githubScripts = scriptCardsData?.success ? scriptCardsData.cards
|
||||||
error: error?.message,
|
.filter(script => script && script.name) // Filter out invalid scripts
|
||||||
scriptCardsData: scriptCardsData?.success,
|
.map(script => ({
|
||||||
cardsCount: scriptCardsData?.cards?.length
|
...script,
|
||||||
});
|
source: 'github' as const,
|
||||||
|
isDownloaded: false, // Will be updated by status check
|
||||||
|
isUpToDate: false, // Will be updated by status check
|
||||||
|
})) : [];
|
||||||
|
|
||||||
const handleCardClick = (scriptCard: ScriptCardType) => {
|
return githubScripts;
|
||||||
|
}, [scriptCardsData]);
|
||||||
|
|
||||||
|
|
||||||
|
// Update scripts with download status
|
||||||
|
const scriptsWithStatus = React.useMemo(() => {
|
||||||
|
return combinedScripts.map(script => {
|
||||||
|
if (!script || !script.name) {
|
||||||
|
return script; // Return as-is if invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there's a corresponding local script
|
||||||
|
const hasLocalVersion = localScriptsData?.scripts?.some(local => {
|
||||||
|
if (!local || !local.name) return false;
|
||||||
|
const localName = local.name.replace(/\.sh$/, '');
|
||||||
|
return localName.toLowerCase() === script.name.toLowerCase() ||
|
||||||
|
localName.toLowerCase() === (script.slug || '').toLowerCase();
|
||||||
|
}) ?? false;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...script,
|
||||||
|
isDownloaded: hasLocalVersion,
|
||||||
|
// Removed isUpToDate - only show in modal for detailed comparison
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [combinedScripts, localScriptsData]);
|
||||||
|
|
||||||
|
// Filter scripts based on search query (name and slug only)
|
||||||
|
const filteredScripts = React.useMemo(() => {
|
||||||
|
if (!searchQuery || !searchQuery.trim()) {
|
||||||
|
return scriptsWithStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = searchQuery.toLowerCase().trim();
|
||||||
|
|
||||||
|
// If query is too short, don't filter
|
||||||
|
if (query.length < 1) {
|
||||||
|
return scriptsWithStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered = scriptsWithStatus.filter(script => {
|
||||||
|
// Ensure script exists and has required properties
|
||||||
|
if (!script || typeof script !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = (script.name || '').toLowerCase();
|
||||||
|
const slug = (script.slug || '').toLowerCase();
|
||||||
|
|
||||||
|
const matches = name.includes(query) || slug.includes(query);
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
});
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}, [scriptsWithStatus, searchQuery]);
|
||||||
|
|
||||||
|
|
||||||
|
const handleCardClick = (scriptCard: any) => {
|
||||||
|
// All scripts are GitHub scripts, open modal
|
||||||
setSelectedSlug(scriptCard.slug);
|
setSelectedSlug(scriptCard.slug);
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
};
|
};
|
||||||
@@ -38,7 +102,7 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
|||||||
setSelectedSlug(null);
|
setSelectedSlug(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (githubLoading || localLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||||
@@ -47,7 +111,7 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error || !scriptCardsData?.success) {
|
if (githubError || localError) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<div className="text-red-600 mb-4">
|
<div className="text-red-600 mb-4">
|
||||||
@@ -56,12 +120,8 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
|||||||
</svg>
|
</svg>
|
||||||
<p className="text-lg font-medium">Failed to load scripts</p>
|
<p className="text-lg font-medium">Failed to load scripts</p>
|
||||||
<p className="text-sm text-gray-500 mt-1">
|
<p className="text-sm text-gray-500 mt-1">
|
||||||
{scriptCardsData?.error || 'Unknown error occurred'}
|
{githubError?.message || localError?.message || 'Unknown error occurred'}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-4 text-xs text-gray-400">
|
|
||||||
<p>No JSON files found in scripts/json directory.</p>
|
|
||||||
<p>Use the "Resync Scripts" button to download from GitHub.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => refetch()}
|
onClick={() => refetch()}
|
||||||
@@ -73,9 +133,7 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const scripts = scriptCardsData?.cards || [];
|
if (!scriptsWithStatus || scriptsWithStatus.length === 0) {
|
||||||
|
|
||||||
if (!scripts || scripts.length === 0) {
|
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<div className="text-gray-500">
|
<div className="text-gray-500">
|
||||||
@@ -84,7 +142,7 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
|||||||
</svg>
|
</svg>
|
||||||
<p className="text-lg font-medium">No scripts found</p>
|
<p className="text-lg font-medium">No scripts found</p>
|
||||||
<p className="text-sm text-gray-500 mt-1">
|
<p className="text-sm text-gray-500 mt-1">
|
||||||
No script files were found in the repository.
|
No script files were found in the repository or local directory.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,11 +151,67 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
{/* Search Bar */}
|
||||||
{scripts.map((script, index) => {
|
<div className="mb-8">
|
||||||
|
<div className="relative max-w-md mx-auto">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search scripts by name..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-lg leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
||||||
|
/>
|
||||||
|
{searchQuery && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSearchQuery('')}
|
||||||
|
className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{searchQuery && (
|
||||||
|
<div className="text-center mt-2 text-sm text-gray-600">
|
||||||
|
{filteredScripts.length === 0 ? (
|
||||||
|
<span>No scripts found matching "{searchQuery}"</span>
|
||||||
|
) : (
|
||||||
|
<span>Found {filteredScripts.length} script{filteredScripts.length !== 1 ? 's' : ''} matching "{searchQuery}"</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scripts Grid */}
|
||||||
|
{filteredScripts.length === 0 && searchQuery ? (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<div className="text-gray-500">
|
||||||
|
<svg className="w-12 h-12 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
|
</svg>
|
||||||
|
<p className="text-lg font-medium">No matching scripts found</p>
|
||||||
|
<p className="text-sm text-gray-500 mt-1">
|
||||||
|
Try adjusting your search terms or clear the search to see all scripts.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setSearchQuery('')}
|
||||||
|
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
Clear Search
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||||
|
{filteredScripts.map((script, index) => {
|
||||||
// Add validation to ensure script has required properties
|
// Add validation to ensure script has required properties
|
||||||
if (!script || typeof script !== 'object') {
|
if (!script || typeof script !== 'object') {
|
||||||
console.warn('Invalid script object at index', index, script);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +223,8 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<ScriptDetailModal
|
<ScriptDetailModal
|
||||||
script={scriptData?.success ? scriptData.script : null}
|
script={scriptData?.success ? scriptData.script : null}
|
||||||
|
|||||||
@@ -1,163 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { api } from '~/trpc/react';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { Terminal } from './Terminal';
|
|
||||||
import { TextViewer } from './TextViewer';
|
|
||||||
|
|
||||||
|
|
||||||
interface ScriptsListProps {
|
|
||||||
onRunScript: (scriptPath: string, scriptName: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ScriptsList({ onRunScript }: ScriptsListProps) {
|
|
||||||
const { data, isLoading, error, refetch } = api.scripts.getCtScripts.useQuery();
|
|
||||||
const [selectedScript, setSelectedScript] = useState<string | null>(null);
|
|
||||||
const [viewerScript, setViewerScript] = useState<string | null>(null);
|
|
||||||
const [isViewerOpen, setIsViewerOpen] = useState(false);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center p-8">
|
|
||||||
<div className="text-lg text-gray-600">Loading scripts...</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center p-8">
|
|
||||||
<div className="text-lg text-red-600">
|
|
||||||
Error loading scripts: {error.message}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data?.scripts || data.scripts.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center p-8">
|
|
||||||
<div className="text-lg text-gray-600">No scripts found in the scripts directory</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatFileSize = (bytes: number): string => {
|
|
||||||
if (bytes === 0) return '0 Bytes';
|
|
||||||
const k = 1024;
|
|
||||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (date: Date): string => {
|
|
||||||
return new Date(date).toLocaleString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFileIcon = (extension: string): string => {
|
|
||||||
switch (extension) {
|
|
||||||
case '.sh':
|
|
||||||
case '.bash':
|
|
||||||
return '🐚';
|
|
||||||
case '.py':
|
|
||||||
return '🐍';
|
|
||||||
case '.js':
|
|
||||||
return '📜';
|
|
||||||
case '.ts':
|
|
||||||
return '🔷';
|
|
||||||
default:
|
|
||||||
return '📄';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
<div className="mb-6">
|
|
||||||
<h2 className="text-2xl font-bold text-gray-800 mb-2">Available Scripts</h2>
|
|
||||||
<p className="text-gray-600">
|
|
||||||
Found {data.scripts.length} script(s) in {data.directoryInfo.path}
|
|
||||||
</p>
|
|
||||||
<div className="mt-2 text-sm text-gray-500">
|
|
||||||
<p>Allowed extensions: {data.directoryInfo.allowedExtensions.join(', ')}</p>
|
|
||||||
<p>Max execution time: {Math.round(data.directoryInfo.maxExecutionTime / 1000)}s</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-4">
|
|
||||||
{data.scripts.map((script) => (
|
|
||||||
<div
|
|
||||||
key={script.path}
|
|
||||||
className="bg-white rounded-lg border border-gray-200 p-4 shadow-sm hover:shadow-md transition-shadow"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
{script.logo ? (
|
|
||||||
<img
|
|
||||||
src={script.logo}
|
|
||||||
alt={`${script.name} logo`}
|
|
||||||
className="w-8 h-8 rounded object-contain"
|
|
||||||
onError={(e) => {
|
|
||||||
// Fallback to file icon if logo fails to load
|
|
||||||
e.currentTarget.style.display = 'none';
|
|
||||||
const nextElement = e.currentTarget.nextElementSibling as HTMLElement;
|
|
||||||
if (nextElement) {
|
|
||||||
nextElement.style.display = 'block';
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<span className="text-2xl" style={{ display: script.logo ? 'none' : 'block' }}>
|
|
||||||
{getFileIcon(script.extension)}
|
|
||||||
</span>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-gray-800">{script.name}</h3>
|
|
||||||
<div className="text-sm text-gray-500 space-y-1">
|
|
||||||
<p>Size: {formatFileSize(script.size)}</p>
|
|
||||||
<p>Modified: {formatDate(script.lastModified)}</p>
|
|
||||||
<p>Extension: {script.extension}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setViewerScript(script.name);
|
|
||||||
setIsViewerOpen(true);
|
|
||||||
}}
|
|
||||||
className="px-3 py-1 text-sm bg-blue-100 text-blue-700 rounded hover:bg-blue-200 transition-colors"
|
|
||||||
>
|
|
||||||
View
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => onRunScript(`scripts/ct/${script.name}`, script.name)}
|
|
||||||
className="px-4 py-2 text-sm font-medium rounded transition-colors bg-green-600 text-white hover:bg-green-700"
|
|
||||||
>
|
|
||||||
▶️ Run
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedScript && (
|
|
||||||
<div className="mt-6">
|
|
||||||
<Terminal
|
|
||||||
scriptPath={`scripts/ct/${selectedScript.split('/').pop()}`}
|
|
||||||
onClose={() => setSelectedScript(null)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<TextViewer
|
|
||||||
scriptName={viewerScript || ''}
|
|
||||||
isOpen={isViewerOpen}
|
|
||||||
onClose={() => {
|
|
||||||
setIsViewerOpen(false);
|
|
||||||
setViewerScript(null);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -112,13 +112,11 @@ export function Terminal({ scriptPath, onClose }: TerminalProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Prevent multiple connections in React Strict Mode
|
// Prevent multiple connections in React Strict Mode
|
||||||
if (hasConnectedRef.current || isConnectingRef.current || (wsRef.current && wsRef.current.readyState === WebSocket.OPEN)) {
|
if (hasConnectedRef.current || isConnectingRef.current || (wsRef.current && wsRef.current.readyState === WebSocket.OPEN)) {
|
||||||
console.log('WebSocket already connected, connecting, or has connected, skipping...');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close any existing connection first
|
// Close any existing connection first
|
||||||
if (wsRef.current) {
|
if (wsRef.current) {
|
||||||
console.log('Closing existing WebSocket connection');
|
|
||||||
wsRef.current.close();
|
wsRef.current.close();
|
||||||
wsRef.current = null;
|
wsRef.current = null;
|
||||||
}
|
}
|
||||||
@@ -132,14 +130,10 @@ export function Terminal({ scriptPath, onClose }: TerminalProps) {
|
|||||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
const wsUrl = `${protocol}//${window.location.host}/ws/script-execution`;
|
const wsUrl = `${protocol}//${window.location.host}/ws/script-execution`;
|
||||||
|
|
||||||
console.log('Connecting to WebSocket:', wsUrl);
|
|
||||||
const ws = new WebSocket(wsUrl);
|
const ws = new WebSocket(wsUrl);
|
||||||
wsRef.current = ws;
|
wsRef.current = ws;
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
console.log('WebSocket connected successfully');
|
|
||||||
console.log('Script path:', scriptPath);
|
|
||||||
console.log('Execution ID:', executionId);
|
|
||||||
setIsConnected(true);
|
setIsConnected(true);
|
||||||
isConnectingRef.current = false;
|
isConnectingRef.current = false;
|
||||||
|
|
||||||
@@ -154,7 +148,6 @@ export function Terminal({ scriptPath, onClose }: TerminalProps) {
|
|||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const message: TerminalMessage = JSON.parse(event.data);
|
const message: TerminalMessage = JSON.parse(event.data);
|
||||||
console.log('Received message:', message);
|
|
||||||
handleMessage(message);
|
handleMessage(message);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error parsing WebSocket message:', error);
|
console.error('Error parsing WebSocket message:', error);
|
||||||
@@ -162,7 +155,6 @@ export function Terminal({ scriptPath, onClose }: TerminalProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = (event) => {
|
ws.onclose = (event) => {
|
||||||
console.log('WebSocket disconnected:', event.code, event.reason);
|
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
setIsRunning(false);
|
setIsRunning(false);
|
||||||
isConnectingRef.current = false;
|
isConnectingRef.current = false;
|
||||||
@@ -184,7 +176,6 @@ export function Terminal({ scriptPath, onClose }: TerminalProps) {
|
|||||||
isConnectingRef.current = false;
|
isConnectingRef.current = false;
|
||||||
hasConnectedRef.current = false;
|
hasConnectedRef.current = false;
|
||||||
if (wsRef.current && (wsRef.current.readyState === WebSocket.OPEN || wsRef.current.readyState === WebSocket.CONNECTING)) {
|
if (wsRef.current && (wsRef.current.readyState === WebSocket.OPEN || wsRef.current.readyState === WebSocket.CONNECTING)) {
|
||||||
console.log('Cleaning up WebSocket connection');
|
|
||||||
wsRef.current.close();
|
wsRef.current.close();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,15 +2,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ScriptsList } from './_components/ScriptsList';
|
|
||||||
import { ScriptsGrid } from './_components/ScriptsGrid';
|
import { ScriptsGrid } from './_components/ScriptsGrid';
|
||||||
import { RepoStatus } from './_components/RepoStatus';
|
|
||||||
import { ResyncButton } from './_components/ResyncButton';
|
import { ResyncButton } from './_components/ResyncButton';
|
||||||
import { Terminal } from './_components/Terminal';
|
import { Terminal } from './_components/Terminal';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [runningScript, setRunningScript] = useState<{ path: string; name: string } | null>(null);
|
const [runningScript, setRunningScript] = useState<{ path: string; name: string } | null>(null);
|
||||||
const [activeTab, setActiveTab] = useState<'local' | 'github'>('github');
|
|
||||||
|
|
||||||
const handleRunScript = (scriptPath: string, scriptName: string) => {
|
const handleRunScript = (scriptPath: string, scriptName: string) => {
|
||||||
setRunningScript({ path: scriptPath, name: scriptName });
|
setRunningScript({ path: scriptPath, name: scriptName });
|
||||||
@@ -26,39 +23,18 @@ export default function Home() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<h1 className="text-4xl font-bold text-gray-800 mb-2">
|
<h1 className="text-4xl font-bold text-gray-800 mb-2">
|
||||||
🚀 PVE Scripts Local Management
|
🚀 PVE Scripts Management
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600">
|
||||||
Manage and execute Proxmox helper scripts locally with live output streaming
|
Manage and execute Proxmox helper scripts locally with live output streaming
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Script Source Tabs */}
|
{/* Resync Button */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div className="flex space-x-1 bg-gray-100 p-1 rounded-lg">
|
<div></div>
|
||||||
<button
|
<ResyncButton />
|
||||||
onClick={() => setActiveTab('github')}
|
|
||||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
|
||||||
activeTab === 'github'
|
|
||||||
? 'bg-white text-gray-900 shadow-sm'
|
|
||||||
: 'text-gray-500 hover:text-gray-700'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Script Library
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('local')}
|
|
||||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
|
||||||
activeTab === 'local'
|
|
||||||
? 'bg-white text-gray-900 shadow-sm'
|
|
||||||
: 'text-gray-500 hover:text-gray-700'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Local Scripts
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{activeTab === 'github' && <ResyncButton />}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -73,11 +49,7 @@ export default function Home() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Scripts List */}
|
{/* Scripts List */}
|
||||||
{activeTab === 'github' ? (
|
<ScriptsGrid onInstallScript={handleRunScript} />
|
||||||
<ScriptsGrid onInstallScript={handleRunScript} />
|
|
||||||
) : (
|
|
||||||
<ScriptsList onRunScript={handleRunScript} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -219,5 +219,65 @@ export const scriptsRouter = createTRPCRouter({
|
|||||||
files: []
|
files: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Compare local and remote script content
|
||||||
|
compareScriptContent: publicProcedure
|
||||||
|
.input(z.object({ slug: z.string() }))
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
const script = await localScriptsService.getScriptBySlug(input.slug);
|
||||||
|
if (!script) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Script not found',
|
||||||
|
hasDifferences: false,
|
||||||
|
differences: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await scriptDownloaderService.compareScriptContent(script);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
...result
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in compareScriptContent:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to compare script content',
|
||||||
|
hasDifferences: false,
|
||||||
|
differences: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Get diff content for a specific script file
|
||||||
|
getScriptDiff: publicProcedure
|
||||||
|
.input(z.object({ slug: z.string(), filePath: z.string() }))
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
const script = await localScriptsService.getScriptBySlug(input.slug);
|
||||||
|
if (!script) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Script not found',
|
||||||
|
diff: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await scriptDownloaderService.getScriptDiff(script, input.filePath);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
...result
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in getScriptDiff:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to get script diff',
|
||||||
|
diff: null
|
||||||
|
};
|
||||||
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -73,28 +73,6 @@ export const createCallerFactory = t.createCallerFactory;
|
|||||||
*/
|
*/
|
||||||
export const createTRPCRouter = t.router;
|
export const createTRPCRouter = t.router;
|
||||||
|
|
||||||
/**
|
|
||||||
* Middleware for timing procedure execution and adding an artificial delay in development.
|
|
||||||
*
|
|
||||||
* You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating
|
|
||||||
* network latency that would occur in production but not in local development.
|
|
||||||
*/
|
|
||||||
const timingMiddleware = t.middleware(async ({ next, path }) => {
|
|
||||||
const start = Date.now();
|
|
||||||
|
|
||||||
if (t._config.isDev) {
|
|
||||||
// artificial delay in dev
|
|
||||||
const waitMs = Math.floor(Math.random() * 400) + 100;
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await next();
|
|
||||||
|
|
||||||
const end = Date.now();
|
|
||||||
console.log(`[TRPC] ${path} took ${end - start}ms to execute`);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public (unauthenticated) procedure
|
* Public (unauthenticated) procedure
|
||||||
@@ -103,4 +81,4 @@ const timingMiddleware = t.middleware(async ({ next, path }) => {
|
|||||||
* guarantee that a user querying is authorized, but you can still access user session data if they
|
* guarantee that a user querying is authorized, but you can still access user session data if they
|
||||||
* are logged in.
|
* are logged in.
|
||||||
*/
|
*/
|
||||||
export const publicProcedure = t.procedure.use(timingMiddleware);
|
export const publicProcedure = t.procedure;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { WebSocketServer, WebSocket } from 'ws';
|
import { WebSocketServer, type WebSocket } from 'ws';
|
||||||
import { IncomingMessage } from 'http';
|
import type { IncomingMessage } from 'http';
|
||||||
import { scriptManager } from '~/server/lib/scripts';
|
import { scriptManager } from '~/server/lib/scripts';
|
||||||
import { env } from '~/env.js';
|
|
||||||
|
|
||||||
interface ScriptExecutionMessage {
|
interface ScriptExecutionMessage {
|
||||||
type: 'start' | 'output' | 'error' | 'end';
|
type: 'start' | 'output' | 'error' | 'end';
|
||||||
@@ -22,8 +21,7 @@ export class ScriptExecutionHandler {
|
|||||||
this.wss.on('connection', this.handleConnection.bind(this));
|
this.wss.on('connection', this.handleConnection.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleConnection(ws: WebSocket, request: IncomingMessage) {
|
private handleConnection(ws: WebSocket, _request: IncomingMessage) {
|
||||||
console.log('New WebSocket connection for script execution');
|
|
||||||
|
|
||||||
ws.on('message', (data) => {
|
ws.on('message', (data) => {
|
||||||
try {
|
try {
|
||||||
@@ -40,7 +38,6 @@ export class ScriptExecutionHandler {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ws.on('close', () => {
|
ws.on('close', () => {
|
||||||
console.log('WebSocket connection closed');
|
|
||||||
// Clean up any active executions for this connection
|
// Clean up any active executions for this connection
|
||||||
this.cleanupActiveExecutions(ws);
|
this.cleanupActiveExecutions(ws);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { simpleGit, SimpleGit } from 'simple-git';
|
import { simpleGit, type SimpleGit } from 'simple-git';
|
||||||
import { env } from '~/env.js';
|
import { env } from '~/env.js';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
@@ -77,14 +77,13 @@ export class GitManager {
|
|||||||
return { success: false, message: 'No repository URL configured' };
|
return { success: false, message: 'No repository URL configured' };
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Cloning repository from ${env.REPO_URL}...`);
|
|
||||||
|
|
||||||
// Clone the repository
|
// Clone the repository
|
||||||
await this.git.clone(env.REPO_URL, this.repoPath, {
|
await this.git.clone(env.REPO_URL, this.repoPath, [
|
||||||
'--branch': env.REPO_BRANCH,
|
'--branch', env.REPO_BRANCH,
|
||||||
'--single-branch': true,
|
'--single-branch',
|
||||||
'--depth': 1
|
'--depth', '1'
|
||||||
});
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -105,32 +104,23 @@ export class GitManager {
|
|||||||
async initializeRepository(): Promise<void> {
|
async initializeRepository(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
if (!env.REPO_URL) {
|
if (!env.REPO_URL) {
|
||||||
console.log('No remote repository configured, skipping initialization');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRepo = await this.git.checkIsRepo();
|
const isRepo = await this.git.checkIsRepo();
|
||||||
if (!isRepo) {
|
if (!isRepo) {
|
||||||
console.log('Repository not found, cloning...');
|
|
||||||
const result = await this.cloneRepository();
|
const result = await this.cloneRepository();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log('Repository initialized successfully');
|
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to initialize repository:', result.message);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('Repository already exists, checking for updates...');
|
|
||||||
const behind = await this.isBehindRemote();
|
const behind = await this.isBehindRemote();
|
||||||
if (behind) {
|
if (behind) {
|
||||||
console.log('Repository is behind remote, pulling updates...');
|
|
||||||
const result = await this.pullUpdates();
|
const result = await this.pullUpdates();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log('Repository updated successfully');
|
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to update repository:', result.message);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('Repository is up to date');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -160,8 +150,8 @@ export class GitManager {
|
|||||||
return {
|
return {
|
||||||
isRepo: true,
|
isRepo: true,
|
||||||
isBehind,
|
isBehind,
|
||||||
lastCommit: log.latest?.hash,
|
lastCommit: log.latest?.hash || undefined,
|
||||||
branch: status.current
|
branch: status.current || undefined
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting repository status:', error);
|
console.error('Error getting repository status:', error);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { readdir, stat, access } from 'fs/promises';
|
import { readdir, stat } from 'fs/promises';
|
||||||
import { join, resolve, extname } from 'path';
|
import { join, resolve, extname } from 'path';
|
||||||
import { env } from '~/env.js';
|
import { env } from '~/env.js';
|
||||||
import { spawn, ChildProcess } from 'child_process';
|
import { spawn, type ChildProcess } from 'child_process';
|
||||||
import { localScriptsService } from '~/server/services/localScripts';
|
import { localScriptsService } from '~/server/services/localScripts';
|
||||||
|
|
||||||
export interface ScriptInfo {
|
export interface ScriptInfo {
|
||||||
@@ -98,7 +98,6 @@ export class ScriptManager {
|
|||||||
logo = scriptData?.logo || undefined;
|
logo = scriptData?.logo || undefined;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// JSON file might not exist, that's okay
|
// JSON file might not exist, that's okay
|
||||||
console.log(`No JSON data found for ${slug}:`, error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scripts.push({
|
scripts.push({
|
||||||
@@ -226,7 +225,6 @@ export class ScriptManager {
|
|||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
if (!childProcess.killed) {
|
if (!childProcess.killed) {
|
||||||
childProcess.kill('SIGTERM');
|
childProcess.kill('SIGTERM');
|
||||||
console.log(`Script execution timed out after ${this.maxExecutionTime}ms`);
|
|
||||||
}
|
}
|
||||||
}, this.maxExecutionTime);
|
}, this.maxExecutionTime);
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ export class LocalScriptsService {
|
|||||||
await writeFile(filePath, content, 'utf-8');
|
await writeFile(filePath, content, 'utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Successfully saved ${scripts.length} scripts to ${this.scriptsDirectory}`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving scripts from GitHub:', error);
|
console.error('Error saving scripts from GitHub:', error);
|
||||||
throw new Error('Failed to save scripts from GitHub');
|
throw new Error('Failed to save scripts from GitHub');
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ export class ScriptDownloaderService {
|
|||||||
files.push(`install/${installScriptName}`);
|
files.push(`install/${installScriptName}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Install script might not exist, that's okay
|
// Install script might not exist, that's okay
|
||||||
console.log(`Install script not found for ${script.slug}: ${error}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -153,6 +152,209 @@ export class ScriptDownloaderService {
|
|||||||
return { ctExists: false, installExists: false, files: [] };
|
return { ctExists: false, installExists: false, files: [] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async compareScriptContent(script: Script): Promise<{ hasDifferences: boolean; differences: string[] }> {
|
||||||
|
const differences: string[] = [];
|
||||||
|
let hasDifferences = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First check if any local files exist
|
||||||
|
const localFilesExist = await this.checkScriptExists(script);
|
||||||
|
if (!localFilesExist.ctExists && !localFilesExist.installExists) {
|
||||||
|
// No local files exist, so no comparison needed
|
||||||
|
return { hasDifferences: false, differences: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare CT script only if it exists locally
|
||||||
|
if (localFilesExist.ctExists && script.install_methods && script.install_methods.length > 0) {
|
||||||
|
for (const method of script.install_methods) {
|
||||||
|
if (method.script && method.script.startsWith('ct/')) {
|
||||||
|
const fileName = method.script.split('/').pop();
|
||||||
|
if (fileName) {
|
||||||
|
const localPath = join(this.scriptsDirectory, 'ct', fileName);
|
||||||
|
try {
|
||||||
|
// Read local content
|
||||||
|
const localContent = await readFile(localPath, 'utf-8');
|
||||||
|
|
||||||
|
// Download remote content
|
||||||
|
const remoteContent = await this.downloadFileFromGitHub(method.script);
|
||||||
|
|
||||||
|
// Apply the same modification that would be applied during load
|
||||||
|
const modifiedRemoteContent = this.modifyScriptContent(remoteContent);
|
||||||
|
|
||||||
|
// Compare content
|
||||||
|
if (localContent !== modifiedRemoteContent) {
|
||||||
|
hasDifferences = true;
|
||||||
|
differences.push(`ct/${fileName}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error comparing CT script ${fileName}:`, error);
|
||||||
|
// Don't add to differences if there's an error reading files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare install script only if it exists locally
|
||||||
|
if (localFilesExist.installExists) {
|
||||||
|
const installScriptName = `${script.slug}-install.sh`;
|
||||||
|
const localInstallPath = join(this.scriptsDirectory, 'install', installScriptName);
|
||||||
|
try {
|
||||||
|
// Read local content
|
||||||
|
const localContent = await readFile(localInstallPath, 'utf-8');
|
||||||
|
|
||||||
|
// Download remote content
|
||||||
|
const remoteContent = await this.downloadFileFromGitHub(`install/${installScriptName}`);
|
||||||
|
|
||||||
|
// Apply the same modification that would be applied during load
|
||||||
|
const modifiedRemoteContent = this.modifyScriptContent(remoteContent);
|
||||||
|
|
||||||
|
// Compare content
|
||||||
|
if (localContent !== modifiedRemoteContent) {
|
||||||
|
hasDifferences = true;
|
||||||
|
differences.push(`install/${installScriptName}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error comparing install script ${installScriptName}:`, error);
|
||||||
|
// Don't add to differences if there's an error reading files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { hasDifferences, differences };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error comparing script content:', error);
|
||||||
|
return { hasDifferences: false, differences: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getScriptDiff(script: Script, filePath: string): Promise<{ diff: string | null; localContent: string | null; remoteContent: string | null }> {
|
||||||
|
try {
|
||||||
|
let localContent: string | null = null;
|
||||||
|
let remoteContent: string | null = null;
|
||||||
|
|
||||||
|
if (filePath.startsWith('ct/')) {
|
||||||
|
// Handle CT script
|
||||||
|
const fileName = filePath.split('/').pop();
|
||||||
|
if (fileName) {
|
||||||
|
const localPath = join(this.scriptsDirectory, 'ct', fileName);
|
||||||
|
try {
|
||||||
|
localContent = await readFile(localPath, 'utf-8');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading local CT script:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find the corresponding script path in install_methods
|
||||||
|
const method = script.install_methods?.find(m => m.script === filePath);
|
||||||
|
if (method?.script) {
|
||||||
|
const downloadedContent = await this.downloadFileFromGitHub(method.script);
|
||||||
|
remoteContent = this.modifyScriptContent(downloadedContent);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error downloading remote CT script:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (filePath.startsWith('install/')) {
|
||||||
|
// Handle install script
|
||||||
|
const localPath = join(this.scriptsDirectory, filePath);
|
||||||
|
try {
|
||||||
|
localContent = await readFile(localPath, 'utf-8');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading local install script:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
remoteContent = await this.downloadFileFromGitHub(filePath);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error downloading remote install script:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!localContent || !remoteContent) {
|
||||||
|
return { diff: null, localContent, remoteContent };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate diff using a simple line-by-line comparison
|
||||||
|
const diff = this.generateDiff(localContent, remoteContent);
|
||||||
|
return { diff, localContent, remoteContent };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting script diff:', error);
|
||||||
|
return { diff: null, localContent: null, remoteContent: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateDiff(localContent: string, remoteContent: string): string {
|
||||||
|
const localLines = localContent.split('\n');
|
||||||
|
const remoteLines = remoteContent.split('\n');
|
||||||
|
|
||||||
|
let diff = '';
|
||||||
|
let i = 0;
|
||||||
|
let j = 0;
|
||||||
|
|
||||||
|
while (i < localLines.length || j < remoteLines.length) {
|
||||||
|
const localLine = localLines[i];
|
||||||
|
const remoteLine = remoteLines[j];
|
||||||
|
|
||||||
|
if (i >= localLines.length) {
|
||||||
|
// Only remote lines left
|
||||||
|
diff += `+${j + 1}: ${remoteLine}\n`;
|
||||||
|
j++;
|
||||||
|
} else if (j >= remoteLines.length) {
|
||||||
|
// Only local lines left
|
||||||
|
diff += `-${i + 1}: ${localLine}\n`;
|
||||||
|
i++;
|
||||||
|
} else if (localLine === remoteLine) {
|
||||||
|
// Lines are the same
|
||||||
|
diff += ` ${i + 1}: ${localLine}\n`;
|
||||||
|
i++;
|
||||||
|
j++;
|
||||||
|
} else {
|
||||||
|
// Lines are different - find the best match
|
||||||
|
let found = false;
|
||||||
|
for (let k = j + 1; k < Math.min(j + 10, remoteLines.length); k++) {
|
||||||
|
if (localLine === remoteLines[k]) {
|
||||||
|
// Found match in remote, local line was removed
|
||||||
|
for (let l = j; l < k; l++) {
|
||||||
|
diff += `+${l + 1}: ${remoteLines[l]}\n`;
|
||||||
|
}
|
||||||
|
diff += ` ${i + 1}: ${localLine}\n`;
|
||||||
|
i++;
|
||||||
|
j = k + 1;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
for (let k = i + 1; k < Math.min(i + 10, localLines.length); k++) {
|
||||||
|
if (remoteLine === localLines[k]) {
|
||||||
|
// Found match in local, remote line was added
|
||||||
|
diff += `-${i + 1}: ${localLine}\n`;
|
||||||
|
for (let l = i + 1; l < k; l++) {
|
||||||
|
diff += `-${l + 1}: ${localLines[l]}\n`;
|
||||||
|
}
|
||||||
|
diff += `+${j + 1}: ${remoteLine}\n`;
|
||||||
|
i = k + 1;
|
||||||
|
j++;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
// No match found, lines are different
|
||||||
|
diff += `-${i + 1}: ${localLine}\n`;
|
||||||
|
diff += `+${j + 1}: ${remoteLine}\n`;
|
||||||
|
i++;
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return diff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Singleton instance
|
// Singleton instance
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ export interface ScriptCard {
|
|||||||
type: string;
|
type: string;
|
||||||
updateable: boolean;
|
updateable: boolean;
|
||||||
website: string | null;
|
website: string | null;
|
||||||
|
source?: 'github' | 'local';
|
||||||
|
isDownloaded?: boolean;
|
||||||
|
localPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GitHubFile {
|
export interface GitHubFile {
|
||||||
|
|||||||
Reference in New Issue
Block a user