mirror of
https://github.com/Heretek-AI/Heretek-ProxmoxVE.git
synced 2026-10-11 21:09:51 +00:00
Sync from Heretek-AI/ProxmoxVE - 2026-03-21
This commit is contained in:
+15
-14
@@ -32,7 +32,8 @@ function update_script() {
|
||||
fi
|
||||
|
||||
# Get current version
|
||||
CURRENT_VERSION=$(su - openclaw -c "export PATH=/home/openclaw/.npm-global/bin:\$PATH && openclaw --version" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
|
||||
# Using sudo -u since openclaw user has nologin shell
|
||||
CURRENT_VERSION=$(sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" openclaw --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
|
||||
|
||||
# Get latest version from npm
|
||||
LATEST_VERSION=$(npm view openclaw version 2>/dev/null || echo "unknown")
|
||||
@@ -50,7 +51,7 @@ function update_script() {
|
||||
msg_info "Updating OpenClaw from v${CURRENT_VERSION} to v${LATEST_VERSION}"
|
||||
|
||||
msg_info "Stopping Gateway Service"
|
||||
su - openclaw -c "systemctl --user stop openclaw-gateway" 2>/dev/null || true
|
||||
sudo -u openclaw systemctl --user stop openclaw-gateway 2>/dev/null || true
|
||||
msg_ok "Stopped Gateway Service"
|
||||
|
||||
msg_info "Backing up Configuration"
|
||||
@@ -64,12 +65,12 @@ function update_script() {
|
||||
fi
|
||||
|
||||
msg_info "Updating OpenClaw Package"
|
||||
# Run npm update as the openclaw user, not root
|
||||
su - openclaw -c "export PATH=/home/openclaw/.npm-global/bin:\$PATH && npm update -g openclaw" 2>/dev/null
|
||||
# Run npm update as the openclaw user using sudo -u (nologin shell)
|
||||
sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" npm update -g openclaw 2>/dev/null
|
||||
msg_ok "Updated OpenClaw Package"
|
||||
|
||||
msg_info "Verifying Update"
|
||||
NEW_VERSION=$(su - openclaw -c "export PATH=/home/openclaw/.npm-global/bin:\$PATH && openclaw --version" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
|
||||
NEW_VERSION=$(sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" openclaw --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
|
||||
if [[ "$NEW_VERSION" == "$LATEST_VERSION" ]]; then
|
||||
msg_ok "Verified OpenClaw v${NEW_VERSION}"
|
||||
else
|
||||
@@ -77,18 +78,18 @@ function update_script() {
|
||||
fi
|
||||
|
||||
msg_info "Starting Gateway Service"
|
||||
su - openclaw -c "systemctl --user start openclaw-gateway" 2>/dev/null || true
|
||||
sudo -u openclaw systemctl --user start openclaw-gateway 2>/dev/null || true
|
||||
|
||||
# Wait for service to start
|
||||
sleep 3
|
||||
|
||||
# Check if service is running
|
||||
if su - openclaw -c "systemctl --user is-active openclaw-gateway" 2>/dev/null | grep -q "active"; then
|
||||
if sudo -u openclaw systemctl --user is-active openclaw-gateway 2>/dev/null | grep -q "active"; then
|
||||
msg_ok "Gateway Service Started"
|
||||
else
|
||||
msg_warn "Gateway Service may not have started properly"
|
||||
msg_info "Checking service status..."
|
||||
su - openclaw -c "systemctl --user status openclaw-gateway" 2>/dev/null || true
|
||||
sudo -u openclaw systemctl --user status openclaw-gateway 2>/dev/null || true
|
||||
fi
|
||||
|
||||
msg_ok "Updated successfully to v${LATEST_VERSION}!"
|
||||
@@ -98,15 +99,15 @@ function update_script() {
|
||||
echo " Post-Update Verification"
|
||||
echo "═══════════════════════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo " Note: The openclaw user has no login shell. Use 'sudo -u openclaw' to run commands."
|
||||
echo ""
|
||||
echo " Run these commands to verify the update:"
|
||||
echo " su - openclaw"
|
||||
echo " openclaw --version"
|
||||
echo " openclaw doctor"
|
||||
echo " openclaw gateway status"
|
||||
echo " sudo -u openclaw openclaw --version"
|
||||
echo " sudo -u openclaw openclaw doctor"
|
||||
echo " sudo -u openclaw openclaw gateway status"
|
||||
echo ""
|
||||
echo " View logs if issues occur:"
|
||||
echo " su - openclaw"
|
||||
echo " openclaw logs --follow"
|
||||
echo " sudo -u openclaw openclaw logs --follow"
|
||||
echo ""
|
||||
echo " Backup saved to: $BACKUP_DIR"
|
||||
echo ""
|
||||
|
||||
@@ -6,17 +6,16 @@ After installation, OpenClaw requires a **model provider** to function. This is
|
||||
|
||||
### Essential Commands
|
||||
|
||||
```bash
|
||||
# Switch to openclaw user
|
||||
su - openclaw
|
||||
**Note:** The `openclaw` user has no login shell for security. Use `sudo -u openclaw` to run commands.
|
||||
|
||||
```bash
|
||||
# Verify installation
|
||||
openclaw --version
|
||||
openclaw doctor
|
||||
openclaw gateway status
|
||||
sudo -u openclaw openclaw --version
|
||||
sudo -u openclaw openclaw doctor
|
||||
sudo -u openclaw openclaw gateway status
|
||||
|
||||
# View logs
|
||||
openclaw logs --follow
|
||||
sudo -u openclaw openclaw logs --follow
|
||||
```
|
||||
|
||||
---
|
||||
@@ -54,8 +53,7 @@ ollama pull all-minilm # Smallest, fastest (45MB)
|
||||
#### 3. Configure OpenClaw for Ollama
|
||||
|
||||
```bash
|
||||
su - openclaw
|
||||
openclaw configure
|
||||
sudo -u openclaw openclaw configure
|
||||
# Select "Ollama" when prompted for model provider
|
||||
# Enter Ollama URL (default: http://localhost:11434)
|
||||
```
|
||||
@@ -107,12 +105,11 @@ If Ollama runs on a different machine:
|
||||
### Option B: OpenAI API
|
||||
|
||||
```bash
|
||||
su - openclaw
|
||||
openclaw models auth add --provider openai
|
||||
sudo -u openclaw openclaw models auth add --provider openai
|
||||
# Enter your API key when prompted
|
||||
|
||||
# Set as default model
|
||||
openclaw models set openai:gpt-4o
|
||||
sudo -u openclaw openclaw models set openai:gpt-4o
|
||||
```
|
||||
|
||||
Manual configuration:
|
||||
@@ -140,12 +137,11 @@ Manual configuration:
|
||||
### Option C: Anthropic Claude
|
||||
|
||||
```bash
|
||||
su - openclaw
|
||||
openclaw models auth add --provider anthropic
|
||||
sudo -u openclaw openclaw models auth add --provider anthropic
|
||||
# Enter your API key when prompted
|
||||
|
||||
# Set as default model
|
||||
openclaw models set anthropic:claude-opus-4-6
|
||||
sudo -u openclaw openclaw models set anthropic:claude-opus-4-6
|
||||
```
|
||||
|
||||
### Option D: Other Providers
|
||||
@@ -199,8 +195,7 @@ OpenClaw supports multiple messaging channels. Configure channels to interact th
|
||||
3. **Configure OpenClaw**:
|
||||
|
||||
```bash
|
||||
su - openclaw
|
||||
openclaw channels add --channel telegram --token "YOUR_BOT_TOKEN"
|
||||
sudo -u openclaw openclaw channels add --channel telegram --token "YOUR_BOT_TOKEN"
|
||||
```
|
||||
|
||||
4. **Start messaging** your bot on Telegram
|
||||
@@ -213,8 +208,7 @@ openclaw channels add --channel telegram --token "YOUR_BOT_TOKEN"
|
||||
4. **Configure OpenClaw**:
|
||||
|
||||
```bash
|
||||
su - openclaw
|
||||
openclaw channels add --channel discord --token "YOUR_BOT_TOKEN"
|
||||
sudo -u openclaw openclaw channels add --channel discord --token "YOUR_BOT_TOKEN"
|
||||
```
|
||||
|
||||
### WhatsApp Setup
|
||||
@@ -222,8 +216,7 @@ openclaw channels add --channel discord --token "YOUR_BOT_TOKEN"
|
||||
WhatsApp requires QR code pairing:
|
||||
|
||||
```bash
|
||||
su - openclaw
|
||||
openclaw channels add --channel whatsapp
|
||||
sudo -u openclaw openclaw channels add --channel whatsapp
|
||||
# A QR code will be displayed
|
||||
# Scan with WhatsApp on your phone (Settings > Linked Devices)
|
||||
```
|
||||
@@ -419,8 +412,8 @@ origin not allowed (open the Control UI from the gateway host or allow it in gat
|
||||
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
|
||||
|
||||
# Create configuration with allowed origins
|
||||
mkdir -p ~/.openclaw
|
||||
cat > ~/.openclaw/openclaw.json << EOF
|
||||
sudo -u openclaw mkdir -p /home/openclaw/.openclaw
|
||||
sudo -u openclaw tee /home/openclaw/.openclaw/openclaw.json > /dev/null << EOF
|
||||
{
|
||||
"gateway": {
|
||||
"bind": "lan",
|
||||
@@ -437,7 +430,7 @@ cat > ~/.openclaw/openclaw.json << EOF
|
||||
EOF
|
||||
|
||||
# Apply and restart
|
||||
su - openclaw -c "systemctl --user restart openclaw-gateway"
|
||||
sudo -u openclaw systemctl --user restart openclaw-gateway
|
||||
```
|
||||
|
||||
**Note:** Even with `allowedOrigins` configured, you still need a secure context (HTTPS or localhost) for the device identity requirement. Use SSH tunneling for LAN access.
|
||||
@@ -464,11 +457,11 @@ By default, OpenClaw binds to localhost (127.0.0.1) only. To make it accessible
|
||||
|
||||
```bash
|
||||
# Open the gateway configuration
|
||||
openclaw configure --section gateway
|
||||
sudo -u openclaw openclaw configure --section gateway
|
||||
|
||||
# Set bind to "lan" when prompted
|
||||
# Then restart the service
|
||||
su - openclaw -c "systemctl --user restart openclaw-gateway"
|
||||
sudo -u openclaw systemctl --user restart openclaw-gateway
|
||||
```
|
||||
|
||||
### Method 2: Manual Configuration
|
||||
@@ -476,9 +469,8 @@ su - openclaw -c "systemctl --user restart openclaw-gateway"
|
||||
Edit the configuration file:
|
||||
|
||||
```bash
|
||||
# Edit as the openclaw user
|
||||
su - openclaw
|
||||
nano ~/.openclaw/openclaw.json
|
||||
# Edit the configuration file
|
||||
nano /home/openclaw/.openclaw/openclaw.json
|
||||
```
|
||||
|
||||
Add the configuration with your IP:
|
||||
@@ -498,7 +490,7 @@ Add the configuration with your IP:
|
||||
Apply changes:
|
||||
|
||||
```bash
|
||||
systemctl --user restart openclaw-gateway
|
||||
sudo -u openclaw systemctl --user restart openclaw-gateway
|
||||
```
|
||||
|
||||
### Method 3: Quick Fix for Existing Installations
|
||||
@@ -509,26 +501,26 @@ For existing installations, run these commands:
|
||||
# Get your container IP
|
||||
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
|
||||
|
||||
# Create configuration with allowed origins (as openclaw user)
|
||||
su - openclaw -c "mkdir -p ~/.openclaw"
|
||||
su - openclaw -c "cat > ~/.openclaw/openclaw.json << EOF
|
||||
# Create configuration with allowed origins
|
||||
sudo -u openclaw mkdir -p /home/openclaw/.openclaw
|
||||
sudo -u openclaw tee /home/openclaw/.openclaw/openclaw.json > /dev/null << EOF
|
||||
{
|
||||
\"gateway\": {
|
||||
\"bind\": \"lan\",
|
||||
\"port\": 18789,
|
||||
\"controlUi\": {
|
||||
\"allowedOrigins\": [
|
||||
\"http://localhost:18789\",
|
||||
\"http://127.0.0.1:18789\",
|
||||
\"http://${CONTAINER_IP}:18789\"
|
||||
"gateway": {
|
||||
"bind": "lan",
|
||||
"port": 18789,
|
||||
"controlUi": {
|
||||
"allowedOrigins": [
|
||||
"http://localhost:18789",
|
||||
"http://127.0.0.1:18789",
|
||||
"http://${CONTAINER_IP}:18789"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF"
|
||||
EOF
|
||||
|
||||
# Restart the service
|
||||
su - openclaw -c "systemctl --user restart openclaw-gateway"
|
||||
sudo -u openclaw systemctl --user restart openclaw-gateway
|
||||
```
|
||||
|
||||
### Method 4: Environment Variable
|
||||
@@ -545,7 +537,7 @@ OPENCLAW_BIND=lan
|
||||
Check the current binding:
|
||||
|
||||
```bash
|
||||
openclaw gateway status
|
||||
sudo -u openclaw openclaw gateway status
|
||||
```
|
||||
|
||||
Output should show:
|
||||
@@ -604,9 +596,9 @@ http://localhost:18789
|
||||
2. Set OpenClaw bind to `tailnet`:
|
||||
|
||||
```bash
|
||||
openclaw configure --section gateway
|
||||
sudo -u openclaw openclaw configure --section gateway
|
||||
# Set bind to "tailnet"
|
||||
su - openclaw -c "systemctl --user restart openclaw-gateway"
|
||||
sudo -u openclaw systemctl --user restart openclaw-gateway
|
||||
```
|
||||
|
||||
3. Access via Tailscale IP: `http://<tailscale-ip>:18789`
|
||||
@@ -616,8 +608,8 @@ http://localhost:18789
|
||||
### Gateway Not Starting
|
||||
|
||||
```bash
|
||||
# Check logs (as openclaw user)
|
||||
su - openclaw -c "journalctl --user -u openclaw-gateway -f"
|
||||
# Check logs
|
||||
sudo -u openclaw journalctl --user -u openclaw-gateway -f
|
||||
|
||||
# Or from root
|
||||
journalctl --user -u openclaw-gateway -f --user-unit
|
||||
@@ -625,12 +617,12 @@ journalctl --user -u openclaw-gateway -f --user-unit
|
||||
# Common issues:
|
||||
# - Port already in use: lsof -i :18789
|
||||
# - Permission denied: ensure running as correct user
|
||||
# - Config errors: openclaw doctor
|
||||
# - Config errors: sudo -u openclaw openclaw doctor
|
||||
```
|
||||
|
||||
### Cannot Connect from Remote
|
||||
|
||||
1. Verify binding: `openclaw gateway status` should show `0.0.0.0:18789`
|
||||
1. Verify binding: `sudo -u openclaw openclaw gateway status` should show `0.0.0.0:18789`
|
||||
2. Check firewall: `ufw status` or `iptables -L -n`
|
||||
3. Verify network connectivity: `ping <server-ip>`
|
||||
|
||||
@@ -638,99 +630,86 @@ journalctl --user -u openclaw-gateway -f --user-unit
|
||||
|
||||
```bash
|
||||
# View current token
|
||||
openclaw gateway status
|
||||
sudo -u openclaw openclaw gateway status
|
||||
|
||||
# Regenerate token (if needed)
|
||||
openclaw doctor --generate-gateway-token
|
||||
sudo -u openclaw openclaw doctor --generate-gateway-token
|
||||
```
|
||||
|
||||
### Model Provider Issues
|
||||
|
||||
```bash
|
||||
# Check model status
|
||||
openclaw models status
|
||||
sudo -u openclaw openclaw models status
|
||||
|
||||
# Probe providers (may consume tokens)
|
||||
openclaw models status --probe
|
||||
sudo -u openclaw openclaw models status --probe
|
||||
|
||||
# Set default model
|
||||
openclaw models set <provider:model>
|
||||
sudo -u openclaw openclaw models set <provider:model>
|
||||
|
||||
# Example:
|
||||
openclaw models set ollama:llama3.2
|
||||
openclaw models set openai:gpt-4o
|
||||
sudo -u openclaw openclaw models set ollama:llama3.2
|
||||
sudo -u openclaw openclaw models set openai:gpt-4o
|
||||
```
|
||||
|
||||
### Channel Issues
|
||||
|
||||
```bash
|
||||
# Check channel status
|
||||
openclaw channels status --probe
|
||||
sudo -u openclaw openclaw channels status --probe
|
||||
|
||||
# View channel logs
|
||||
openclaw channels logs --channel telegram
|
||||
sudo -u openclaw openclaw channels logs --channel telegram
|
||||
|
||||
# Re-login to a channel
|
||||
openclaw channels login --channel whatsapp
|
||||
sudo -u openclaw openclaw channels login --channel whatsapp
|
||||
```
|
||||
|
||||
## Installing Homebrew (Optional)
|
||||
## Using Homebrew
|
||||
|
||||
If you need to install additional tools via Homebrew, follow these steps:
|
||||
Homebrew is installed automatically during the OpenClaw installation. It provides access to additional packages that may be useful for extending OpenClaw functionality.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
The `openclaw` user needs sudo access and a password set. By default, the user is created without a password.
|
||||
|
||||
### Step 1: Set Password for openclaw User
|
||||
|
||||
As root, set a password for the openclaw user:
|
||||
### Verify Installation
|
||||
|
||||
```bash
|
||||
passwd openclaw
|
||||
# Enter and confirm the new password
|
||||
```
|
||||
|
||||
### Step 2: Install Homebrew
|
||||
|
||||
Switch to the openclaw user and run the installer:
|
||||
|
||||
```bash
|
||||
su - openclaw
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
```
|
||||
|
||||
When prompted:
|
||||
|
||||
1. Enter the openclaw password for sudo access
|
||||
2. Press ENTER to continue when the installer shows the directories it will create
|
||||
|
||||
### Step 3: Add Homebrew to PATH
|
||||
|
||||
After installation, add Homebrew to your shell:
|
||||
|
||||
```bash
|
||||
echo >> /home/openclaw/.bashrc
|
||||
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"' >> /home/openclaw/.bashrc
|
||||
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"
|
||||
```
|
||||
|
||||
### Step 4: Verify Installation
|
||||
|
||||
```bash
|
||||
brew --version
|
||||
sudo -u openclaw brew --version
|
||||
```
|
||||
|
||||
### Installing Packages
|
||||
|
||||
Once Homebrew is installed, you can install packages:
|
||||
Once Homebrew is installed, you can install additional packages:
|
||||
|
||||
```bash
|
||||
brew install <package-name>
|
||||
```
|
||||
|
||||
### Common Useful Packages
|
||||
|
||||
```bash
|
||||
# Example: Install additional tools
|
||||
brew install jq # JSON processor
|
||||
brew install yq # YAML processor
|
||||
brew install git-delta # Better git diff viewer
|
||||
```
|
||||
|
||||
**Note:** Homebrew packages are installed to `/home/linuxbrew/.linuxbrew/` and are available to all users on the system.
|
||||
|
||||
### Troubleshooting Homebrew
|
||||
|
||||
If Homebrew is not working correctly:
|
||||
|
||||
```bash
|
||||
# Check if Homebrew is in PATH
|
||||
which brew
|
||||
|
||||
# If not, add it manually
|
||||
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"
|
||||
|
||||
# Add to .bashrc permanently
|
||||
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"' >> ~/.bashrc
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Embedding Model Comparison
|
||||
|
||||
@@ -89,6 +89,14 @@
|
||||
{
|
||||
"text": "Auth token is displayed during installation - save it for pairing devices",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "Homebrew is installed automatically - use 'sudo -u openclaw brew install <package>' to install additional tools",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "The openclaw user has no login shell - use 'sudo -u openclaw <command>' to run commands",
|
||||
"type": "warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+57
-27
@@ -45,29 +45,61 @@ NODE_VERSION="24" setup_nodejs
|
||||
# Install uv (Python package manager) - OpenClaw uses Python for some tools
|
||||
PYTHON_VERSION="3.12" setup_uv
|
||||
|
||||
# Create unprivileged user for OpenClaw
|
||||
# Create unprivileged user for OpenClaw (with nologin shell for security)
|
||||
msg_info "Creating OpenClaw User"
|
||||
if ! id -u openclaw &>/dev/null; then
|
||||
useradd -r -s /bin/bash -d /home/openclaw -m openclaw
|
||||
# Add openclaw to sudoers for specific operations if needed
|
||||
usermod -aG sudo openclaw 2>/dev/null || true
|
||||
# Create user with nologin shell to prevent direct login
|
||||
useradd -r -s /usr/sbin/nologin -d /home/openclaw -m openclaw
|
||||
fi
|
||||
msg_ok "Created OpenClaw User"
|
||||
|
||||
# Configure password-less sudo for openclaw user
|
||||
msg_info "Configuring Password-less Sudo for OpenClaw User"
|
||||
cat <<EOF >/etc/sudoers.d/openclaw
|
||||
# Allow openclaw user to run any command without password
|
||||
openclaw ALL=(ALL) NOPASSWD: ALL
|
||||
EOF
|
||||
chmod 440 /etc/sudoers.d/openclaw
|
||||
msg_ok "Configured Password-less Sudo"
|
||||
|
||||
# Configure npm to use user-writable directory for global packages
|
||||
msg_info "Configuring npm for User Packages"
|
||||
mkdir -p /home/openclaw/.npm-global
|
||||
chown -R openclaw:openclaw /home/openclaw/.npm-global
|
||||
# Set npm prefix for openclaw user to use home directory
|
||||
run_user_cmd="su - openclaw -c"
|
||||
$run_user_cmd "npm config set prefix /home/openclaw/.npm-global"
|
||||
# Using sudo -u since openclaw user has nologin shell
|
||||
sudo -u openclaw npm config set prefix /home/openclaw/.npm-global
|
||||
# Add to PATH for openclaw user
|
||||
echo 'export PATH=/home/openclaw/.npm-global/bin:$PATH' >> /home/openclaw/.bashrc
|
||||
msg_ok "Configured npm for User Packages"
|
||||
|
||||
# Install Homebrew for the openclaw user
|
||||
msg_info "Installing Homebrew"
|
||||
# Create Homebrew directory structure
|
||||
mkdir -p /home/linuxbrew/.linuxbrew
|
||||
chown -R openclaw:openclaw /home/linuxbrew 2>/dev/null || true
|
||||
|
||||
# Download Homebrew installer
|
||||
curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh -o /tmp/brew-install.sh
|
||||
|
||||
# Run installer non-interactively as openclaw user
|
||||
# CI=1 and NONINTERACTIVE=1 enable fully automated installation
|
||||
# Using sudo -u since openclaw user has nologin shell
|
||||
CI=1 NONINTERACTIVE=1 sudo -u openclaw bash /tmp/brew-install.sh || true
|
||||
rm -f /tmp/brew-install.sh
|
||||
|
||||
# Add Homebrew to PATH for openclaw user
|
||||
if [[ -f /home/linuxbrew/.linuxbrew/bin/brew ]]; then
|
||||
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"' >> /home/openclaw/.bashrc
|
||||
msg_ok "Installed Homebrew"
|
||||
else
|
||||
msg_warn "Homebrew installation may have failed - check logs"
|
||||
fi
|
||||
|
||||
msg_info "Installing OpenClaw"
|
||||
# Install OpenClaw globally as the openclaw user
|
||||
$run_user_cmd "npm install -g openclaw@latest"
|
||||
# Using sudo -u since openclaw user has nologin shell
|
||||
sudo -u openclaw npm install -g openclaw@latest
|
||||
msg_ok "Installed OpenClaw"
|
||||
|
||||
msg_info "Creating Directories"
|
||||
@@ -184,7 +216,8 @@ export PATH="/home/openclaw/.npm-global/bin:$PATH"
|
||||
export OPENCLAW_CONFIG_PATH="/home/openclaw/.openclaw/openclaw.json"
|
||||
|
||||
# Install the gateway service using the recommended method
|
||||
$run_user_cmd "export PATH=/home/openclaw/.npm-global/bin:\$PATH && openclaw gateway install" 2>/dev/null || true
|
||||
# Using sudo -u since openclaw user has nologin shell
|
||||
sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" openclaw gateway install 2>/dev/null || true
|
||||
|
||||
# If the service wasn't created, create it manually
|
||||
if [[ ! -f /home/openclaw/.config/systemd/user/openclaw-gateway.service ]]; then
|
||||
@@ -220,8 +253,9 @@ msg_ok "Started Caddy HTTPS Proxy"
|
||||
|
||||
msg_info "Starting OpenClaw Gateway"
|
||||
# Start the gateway service as the openclaw user
|
||||
$run_user_cmd "systemctl --user enable openclaw-gateway" 2>/dev/null || true
|
||||
$run_user_cmd "systemctl --user start openclaw-gateway" 2>/dev/null || true
|
||||
# Using sudo -u since openclaw user has nologin shell
|
||||
sudo -u openclaw systemctl --user enable openclaw-gateway 2>/dev/null || true
|
||||
sudo -u openclaw systemctl --user start openclaw-gateway 2>/dev/null || true
|
||||
msg_ok "Started OpenClaw Gateway"
|
||||
|
||||
# Wait for gateway to be ready
|
||||
@@ -229,10 +263,10 @@ msg_info "Verifying Installation"
|
||||
sleep 5
|
||||
|
||||
# Check if gateway is running
|
||||
if $run_user_cmd "systemctl --user is-active openclaw-gateway" 2>/dev/null | grep -q "active"; then
|
||||
if sudo -u openclaw systemctl --user is-active openclaw-gateway 2>/dev/null | grep -q "active"; then
|
||||
msg_ok "Gateway Service is Active"
|
||||
else
|
||||
msg_warn "Gateway Service may not be running - check logs with: su - openclaw -c 'journalctl --user -u openclaw-gateway'"
|
||||
msg_warn "Gateway Service may not be running - check logs with: sudo -u openclaw journalctl --user -u openclaw-gateway"
|
||||
fi
|
||||
|
||||
# Display auth token for pairing
|
||||
@@ -250,6 +284,8 @@ echo "═══════���══════════════
|
||||
echo " Next Steps:"
|
||||
echo "═══════════════════════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo " Note: The openclaw user has no login shell. Use 'sudo -u openclaw' to run commands."
|
||||
echo ""
|
||||
echo " 1. Configure a Model Provider (REQUIRED for OpenClaw to function):"
|
||||
echo ""
|
||||
echo " Option A - Ollama (Local, Free):"
|
||||
@@ -257,34 +293,28 @@ echo " • Install Ollama: curl -fsSL https://ollama.com/install.sh | sh"
|
||||
echo " • Pull a model: ollama pull llama3.2"
|
||||
echo " • Pull embedding model: ollama pull nomic-embed-text"
|
||||
echo " • Configure OpenClaw:"
|
||||
echo " su - openclaw"
|
||||
echo " openclaw configure"
|
||||
echo " sudo -u openclaw openclaw configure"
|
||||
echo " # Select Ollama as provider"
|
||||
echo ""
|
||||
echo " Option B - OpenAI API:"
|
||||
echo " su - openclaw"
|
||||
echo " openclaw models auth add --provider openai"
|
||||
echo " sudo -u openclaw openclaw models auth add --provider openai"
|
||||
echo " # Enter your API key when prompted"
|
||||
echo ""
|
||||
echo " Option C - Anthropic Claude:"
|
||||
echo " su - openclaw"
|
||||
echo " openclaw models auth add --provider anthropic"
|
||||
echo " sudo -u openclaw openclaw models auth add --provider anthropic"
|
||||
echo " # Enter your API key when prompted"
|
||||
echo ""
|
||||
echo " 2. Configure Channels (Optional - for messaging):"
|
||||
echo " su - openclaw"
|
||||
echo " openclaw channels add --channel telegram --token YOUR_BOT_TOKEN"
|
||||
echo " openclaw channels add --channel discord --token YOUR_BOT_TOKEN"
|
||||
echo " sudo -u openclaw openclaw channels add --channel telegram --token YOUR_BOT_TOKEN"
|
||||
echo " sudo -u openclaw openclaw channels add --channel discord --token YOUR_BOT_TOKEN"
|
||||
echo ""
|
||||
echo " 3. Verify Installation:"
|
||||
echo " su - openclaw"
|
||||
echo " openclaw doctor"
|
||||
echo " openclaw gateway status"
|
||||
echo " openclaw models status"
|
||||
echo " sudo -u openclaw openclaw doctor"
|
||||
echo " sudo -u openclaw openclaw gateway status"
|
||||
echo " sudo -u openclaw openclaw models status"
|
||||
echo ""
|
||||
echo " 4. View Logs:"
|
||||
echo " su - openclaw"
|
||||
echo " openclaw logs --follow"
|
||||
echo " sudo -u openclaw openclaw logs --follow"
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════════════════════════════════════════"
|
||||
echo " Memory Search Configuration"
|
||||
|
||||
+56
-19
@@ -4,6 +4,7 @@ source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/build.func)
|
||||
# Author: BIllyOutlast
|
||||
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/openclaw/openclaw
|
||||
# Documentation: https://docs.openclaw.ai
|
||||
|
||||
APP="openclaw"
|
||||
var_tags="${var_tags:-ai;assistant;automation}"
|
||||
@@ -25,13 +26,14 @@ function update_script() {
|
||||
check_container_resources
|
||||
|
||||
# Check if openclaw is installed (npm global bin location varies)
|
||||
if ! command -v openclaw &>/dev/null; then
|
||||
if ! command -v openclaw &>/dev/null && [[ ! -f /home/openclaw/.npm-global/bin/openclaw ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Get current version
|
||||
CURRENT_VERSION=$(openclaw --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
|
||||
# Using sudo -u since openclaw user has nologin shell
|
||||
CURRENT_VERSION=$(sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" openclaw --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
|
||||
|
||||
# Get latest version from npm
|
||||
LATEST_VERSION=$(npm view openclaw version 2>/dev/null || echo "unknown")
|
||||
@@ -48,33 +50,68 @@ function update_script() {
|
||||
|
||||
msg_info "Updating OpenClaw from v${CURRENT_VERSION} to v${LATEST_VERSION}"
|
||||
|
||||
msg_info "Stopping Service"
|
||||
su - openclaw -c "systemctl --user stop openclaw-gateway" 2>/dev/null || true
|
||||
msg_ok "Stopped Service"
|
||||
msg_info "Stopping Gateway Service"
|
||||
sudo -u openclaw systemctl --user stop openclaw-gateway 2>/dev/null || true
|
||||
msg_ok "Stopped Gateway Service"
|
||||
|
||||
msg_info "Backing up Configuration"
|
||||
BACKUP_DIR="/tmp/openclaw_backup_$(date +%Y%m%d_%H%M%S)"
|
||||
if [[ -d /home/openclaw/.openclaw ]]; then
|
||||
cp -r /home/openclaw/.openclaw /tmp/openclaw_backup
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
cp -r /home/openclaw/.openclaw/* "$BACKUP_DIR"/ 2>/dev/null || true
|
||||
msg_ok "Backed up Configuration to $BACKUP_DIR"
|
||||
else
|
||||
msg_warn "No configuration directory found"
|
||||
fi
|
||||
msg_ok "Backed up Configuration"
|
||||
|
||||
msg_info "Updating OpenClaw"
|
||||
$STD npm update -g openclaw
|
||||
msg_ok "Updated OpenClaw"
|
||||
msg_info "Updating OpenClaw Package"
|
||||
# Run npm update as the openclaw user using sudo -u (nologin shell)
|
||||
sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" npm update -g openclaw 2>/dev/null
|
||||
msg_ok "Updated OpenClaw Package"
|
||||
|
||||
msg_info "Restoring Configuration"
|
||||
if [[ -d /tmp/openclaw_backup ]]; then
|
||||
cp -r /tmp/openclaw_backup/. /home/openclaw/.openclaw 2>/dev/null || true
|
||||
rm -rf /tmp/openclaw_backup
|
||||
msg_info "Verifying Update"
|
||||
NEW_VERSION=$(sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" openclaw --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
|
||||
if [[ "$NEW_VERSION" == "$LATEST_VERSION" ]]; then
|
||||
msg_ok "Verified OpenClaw v${NEW_VERSION}"
|
||||
else
|
||||
msg_warn "Version verification failed. Installed: v${NEW_VERSION}, Expected: v${LATEST_VERSION}"
|
||||
fi
|
||||
chown -R openclaw:openclaw /home/openclaw/.openclaw 2>/dev/null || true
|
||||
msg_ok "Restored Configuration"
|
||||
|
||||
msg_info "Starting Service"
|
||||
su - openclaw -c "systemctl --user start openclaw-gateway" 2>/dev/null || true
|
||||
msg_ok "Started Service"
|
||||
msg_info "Starting Gateway Service"
|
||||
sudo -u openclaw systemctl --user start openclaw-gateway 2>/dev/null || true
|
||||
|
||||
# Wait for service to start
|
||||
sleep 3
|
||||
|
||||
# Check if service is running
|
||||
if sudo -u openclaw systemctl --user is-active openclaw-gateway 2>/dev/null | grep -q "active"; then
|
||||
msg_ok "Gateway Service Started"
|
||||
else
|
||||
msg_warn "Gateway Service may not have started properly"
|
||||
msg_info "Checking service status..."
|
||||
sudo -u openclaw systemctl --user status openclaw-gateway 2>/dev/null || true
|
||||
fi
|
||||
|
||||
msg_ok "Updated successfully to v${LATEST_VERSION}!"
|
||||
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════════════════════════════════════════"
|
||||
echo " Post-Update Verification"
|
||||
echo "═══════════════════════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo " Note: The openclaw user has no login shell. Use 'sudo -u openclaw' to run commands."
|
||||
echo ""
|
||||
echo " Run these commands to verify the update:"
|
||||
echo " sudo -u openclaw openclaw --version"
|
||||
echo " sudo -u openclaw openclaw doctor"
|
||||
echo " sudo -u openclaw openclaw gateway status"
|
||||
echo ""
|
||||
echo " View logs if issues occur:"
|
||||
echo " sudo -u openclaw openclaw logs --follow"
|
||||
echo ""
|
||||
echo " Backup saved to: $BACKUP_DIR"
|
||||
echo ""
|
||||
|
||||
exit
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,315 @@
|
||||
# OpenClaw Configuration Guide
|
||||
|
||||
## Quick Start
|
||||
|
||||
After installation, OpenClaw requires a **model provider** to function. This is the most critical configuration step.
|
||||
|
||||
### Essential Commands
|
||||
|
||||
**Note:** The `openclaw` user has no login shell for security. Use `sudo -u openclaw` to run commands.
|
||||
|
||||
```bash
|
||||
# Verify installation
|
||||
sudo -u openclaw openclaw --version
|
||||
sudo -u openclaw openclaw doctor
|
||||
sudo -u openclaw openclaw gateway status
|
||||
|
||||
# View logs
|
||||
sudo -u openclaw openclaw logs --follow
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Model Provider Configuration (REQUIRED)
|
||||
|
||||
OpenClaw requires a model provider to process messages. Choose one of the following options:
|
||||
|
||||
### Option A: Ollama (Local, Free, Recommended for Offline)
|
||||
|
||||
Ollama runs models locally on your hardware - no API costs, complete privacy.
|
||||
|
||||
#### 1. Install Ollama
|
||||
|
||||
```bash
|
||||
# On the OpenClaw container or a separate server
|
||||
curl -fsSL https://ollama.com/install.sh | sh
|
||||
```
|
||||
|
||||
#### 2. Pull Required Models
|
||||
|
||||
```bash
|
||||
# Chat model (choose one)
|
||||
ollama pull llama3.2 # Good balance (recommended)
|
||||
ollama pull llama3.1:8b # Larger, more capable
|
||||
ollama pull mistral # Alternative option
|
||||
ollama pull gemma2 # Google's model
|
||||
|
||||
# Embedding model (required for memory search)
|
||||
ollama pull nomic-embed-text # Recommended (274MB)
|
||||
ollama pull mxbai-embed-large # Higher quality (670MB)
|
||||
ollama pull all-minilm # Smallest, fastest (45MB)
|
||||
```
|
||||
|
||||
#### 3. Configure OpenClaw for Ollama
|
||||
|
||||
```bash
|
||||
sudo -u openclaw openclaw configure
|
||||
# Select "Ollama" when prompted for model provider
|
||||
# Enter Ollama URL (default: http://localhost:11434)
|
||||
```
|
||||
|
||||
Or manually edit `~/.openclaw/openclaw.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"model": {
|
||||
"primary": "ollama:llama3.2"
|
||||
},
|
||||
"memorySearch": {
|
||||
"provider": "ollama",
|
||||
"model": "nomic-embed-text",
|
||||
"remote": {
|
||||
"baseUrl": "http://localhost:11434"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Ollama on a Separate Server
|
||||
|
||||
If Ollama runs on a different machine:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"model": {
|
||||
"primary": "ollama:llama3.2"
|
||||
},
|
||||
"memorySearch": {
|
||||
"provider": "ollama",
|
||||
"model": "nomic-embed-text",
|
||||
"remote": {
|
||||
"baseUrl": "http://192.168.1.100:11434"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Option B: OpenAI API
|
||||
|
||||
```bash
|
||||
sudo -u openclaw openclaw models auth add --provider openai
|
||||
# Enter your API key when prompted
|
||||
|
||||
# Set as default model
|
||||
sudo -u openclaw openclaw models set openai:gpt-4o
|
||||
```
|
||||
|
||||
Manual configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"secrets": {
|
||||
"providers": {
|
||||
"openai-api": {
|
||||
"source": "env",
|
||||
"env": "OPENAI_API_KEY"
|
||||
}
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"model": {
|
||||
"primary": "openai:gpt-4o"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Option C: Anthropic Claude
|
||||
|
||||
```bash
|
||||
sudo -u openclaw openclaw models auth add --provider anthropic
|
||||
# Enter your API key when prompted
|
||||
|
||||
# Set as default model
|
||||
sudo -u openclaw openclaw models set anthropic:claude-opus-4-6
|
||||
```
|
||||
|
||||
### Option D: Other Providers
|
||||
|
||||
OpenClaw supports many providers:
|
||||
|
||||
| Provider | Command |
|
||||
| ------------- | ------------------------------------------------ |
|
||||
| Google Gemini | `openclaw models auth add --provider google` |
|
||||
| Mistral | `openclaw models auth add --provider mistral` |
|
||||
| OpenRouter | `openclaw models auth add --provider openrouter` |
|
||||
| Groq | `openclaw models auth add --provider groq` |
|
||||
| xAI | `openclaw models auth add --provider xai` |
|
||||
| Together AI | `openclaw models auth add --provider together` |
|
||||
|
||||
### Verify Model Configuration
|
||||
|
||||
```bash
|
||||
# Check model status
|
||||
openclaw models status
|
||||
|
||||
# Test with a message
|
||||
openclaw agent --message "Hello, are you working?" --local
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Channel Configuration
|
||||
|
||||
OpenClaw supports multiple messaging channels. Configure channels to interact through your preferred platform.
|
||||
|
||||
### Supported Channels
|
||||
|
||||
| Channel | Type | Setup Method |
|
||||
| --------------- | ------------- | ------------------------------- |
|
||||
| Telegram | Bot API | Bot token from @BotFather |
|
||||
| Discord | Bot API | Bot token from Developer Portal |
|
||||
| WhatsApp | Baileys | QR code pairing |
|
||||
| Slack | Bolt SDK | App credentials |
|
||||
| Signal | signal-cli | Phone number pairing |
|
||||
| Matrix | Plugin | Homeserver config |
|
||||
| iMessage | BlueBubbles | macOS server required |
|
||||
| Microsoft Teams | Bot Framework | App registration |
|
||||
| IRC | Native | Server connection |
|
||||
| Google Chat | Webhook | App configuration |
|
||||
|
||||
### Telegram Setup (Easiest)
|
||||
|
||||
1. **Create a bot** by messaging [@BotFather](https://t.me/BotFather) on Telegram
|
||||
2. **Get your bot token** (format: `123456789:ABCdefGHIjklMNOpqrsTUVwxyz`)
|
||||
3. **Configure OpenClaw**:
|
||||
|
||||
```bash
|
||||
sudo -u openclaw openclaw channels add --channel telegram --token "YOUR_BOT_TOKEN"
|
||||
```
|
||||
|
||||
4. **Start messaging** your bot on Telegram
|
||||
|
||||
### Discord Setup
|
||||
|
||||
1. **Create a bot** at [Discord Developer Portal](https://discord.com/developers/applications)
|
||||
2. **Get your bot token** from the Bot section
|
||||
3. **Invite bot to server** using OAuth2 URL generator
|
||||
4. **Configure OpenClaw**:
|
||||
|
||||
```bash
|
||||
sudo -u openclaw openclaw channels add --channel discord --token "YOUR_BOT_TOKEN"
|
||||
```
|
||||
|
||||
### WhatsApp Setup
|
||||
|
||||
WhatsApp requires QR code pairing:
|
||||
|
||||
```bash
|
||||
sudo -u openclaw openclaw channels add --channel whatsapp
|
||||
# A QR code will be displayed
|
||||
# Scan with WhatsApp on your phone (Settings > Linked Devices)
|
||||
```
|
||||
|
||||
### Multiple Accounts
|
||||
|
||||
You can configure multiple accounts per channel:
|
||||
|
||||
```bash
|
||||
# Add a second Telegram account
|
||||
openclaw channels add --channel telegram --account work --name "Work Bot" --token "WORK_BOT_TOKEN"
|
||||
|
||||
# List all channels
|
||||
openclaw channels list
|
||||
|
||||
# Check channel status
|
||||
openclaw channels status --probe
|
||||
```
|
||||
|
||||
### Channel Routing
|
||||
|
||||
Route different channels to different agents:
|
||||
|
||||
```bash
|
||||
# Create an agent
|
||||
openclaw agents add --workspace ~/.openclaw-workspaces/work
|
||||
|
||||
# Bind channels to agent
|
||||
openclaw agents bind --agent <agent-id> --bind telegram:work
|
||||
openclaw agents bind --agent <agent-id> --bind discord:main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Memory Configuration
|
||||
|
||||
OpenClaw's memory search requires an embedding provider to index and search through your memory files.
|
||||
|
||||
### Memory Files Location
|
||||
|
||||
Memory files are stored in `~/.openclaw/workspace/memory/`:
|
||||
|
||||
```bash
|
||||
# Create memory directory
|
||||
mkdir -p ~/.openclaw/workspace/memory
|
||||
|
||||
# Create your first memory file
|
||||
cat > ~/.openclaw/workspace/MEMORY.md << 'EOF'
|
||||
# Personal Memory
|
||||
|
||||
## Preferences
|
||||
- I prefer concise responses
|
||||
- Use bullet points for lists
|
||||
|
||||
## Projects
|
||||
- Home automation: running on Proxmox
|
||||
- Media server: Jellyfin on port 8096
|
||||
EOF
|
||||
```
|
||||
|
||||
### Configure Memory Search
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"memorySearch": {
|
||||
"provider": "ollama",
|
||||
"model": "nomic-embed-text",
|
||||
"remote": {
|
||||
"baseUrl": "http://localhost:11434"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Memory Commands
|
||||
|
||||
```bash
|
||||
# Check memory status
|
||||
openclaw memory status
|
||||
|
||||
# Reindex memory files
|
||||
openclaw memory index
|
||||
|
||||
# Search memory
|
||||
openclaw memory search "project details"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## "Device Identity Required" Error Fix
|
||||
|
||||
If you see this error when accessing the Control UI from another machine:
|
||||
@@ -43,7 +353,6 @@ nano ~/.openclaw/openclaw.json
|
||||
"allowInsecureAuth": true
|
||||
},
|
||||
"auth": {
|
||||
"mode": "token",
|
||||
"token": "your-secure-token-here"
|
||||
}
|
||||
}
|
||||
@@ -64,7 +373,7 @@ su - openclaw -c "systemctl --user restart openclaw-gateway"
|
||||
|
||||
### Solution 3: HTTPS Reverse Proxy (Production)
|
||||
|
||||
Set up Caddy or Nginx with SSL certificates for secure HTTPS access.
|
||||
The installation includes Caddy for HTTPS access on port 18790.
|
||||
|
||||
### Solution 4: Tailscale VPN
|
||||
|
||||
@@ -86,6 +395,8 @@ For emergency access only, you can completely disable device auth:
|
||||
|
||||
**⚠️ WARNING:** This is a severe security downgrade. Revert immediately after emergency use.
|
||||
|
||||
---
|
||||
|
||||
## "Origin Not Allowed" Error Fix
|
||||
|
||||
If you see this error when accessing the Control UI from another machine:
|
||||
@@ -101,8 +412,8 @@ origin not allowed (open the Control UI from the gateway host or allow it in gat
|
||||
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
|
||||
|
||||
# Create configuration with allowed origins
|
||||
mkdir -p ~/.openclaw
|
||||
cat > ~/.openclaw/openclaw.json << EOF
|
||||
sudo -u openclaw mkdir -p /home/openclaw/.openclaw
|
||||
sudo -u openclaw tee /home/openclaw/.openclaw/openclaw.json > /dev/null << EOF
|
||||
{
|
||||
"gateway": {
|
||||
"bind": "lan",
|
||||
@@ -119,11 +430,13 @@ cat > ~/.openclaw/openclaw.json << EOF
|
||||
EOF
|
||||
|
||||
# Apply and restart
|
||||
su - openclaw -c "systemctl --user restart openclaw-gateway"
|
||||
sudo -u openclaw systemctl --user restart openclaw-gateway
|
||||
```
|
||||
|
||||
**Note:** Even with `allowedOrigins` configured, you still need a secure context (HTTPS or localhost) for the device identity requirement. Use SSH tunneling for LAN access.
|
||||
|
||||
---
|
||||
|
||||
## Network Binding Configuration
|
||||
|
||||
By default, OpenClaw binds to localhost (127.0.0.1) only. To make it accessible from other machines on your network, you need to:
|
||||
@@ -144,11 +457,11 @@ By default, OpenClaw binds to localhost (127.0.0.1) only. To make it accessible
|
||||
|
||||
```bash
|
||||
# Open the gateway configuration
|
||||
openclaw configure --section gateway
|
||||
sudo -u openclaw openclaw configure --section gateway
|
||||
|
||||
# Set bind to "lan" when prompted
|
||||
# Then restart the service
|
||||
su - openclaw -c "systemctl --user restart openclaw-gateway"
|
||||
sudo -u openclaw systemctl --user restart openclaw-gateway
|
||||
```
|
||||
|
||||
### Method 2: Manual Configuration
|
||||
@@ -156,9 +469,8 @@ su - openclaw -c "systemctl --user restart openclaw-gateway"
|
||||
Edit the configuration file:
|
||||
|
||||
```bash
|
||||
# Edit as the openclaw user
|
||||
su - openclaw
|
||||
nano ~/.openclaw/openclaw.json
|
||||
# Edit the configuration file
|
||||
nano /home/openclaw/.openclaw/openclaw.json
|
||||
```
|
||||
|
||||
Add the configuration with your IP:
|
||||
@@ -178,7 +490,7 @@ Add the configuration with your IP:
|
||||
Apply changes:
|
||||
|
||||
```bash
|
||||
systemctl --user restart openclaw-gateway
|
||||
sudo -u openclaw systemctl --user restart openclaw-gateway
|
||||
```
|
||||
|
||||
### Method 3: Quick Fix for Existing Installations
|
||||
@@ -189,26 +501,26 @@ For existing installations, run these commands:
|
||||
# Get your container IP
|
||||
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
|
||||
|
||||
# Create configuration with allowed origins (as openclaw user)
|
||||
su - openclaw -c "mkdir -p ~/.openclaw"
|
||||
su - openclaw -c "cat > ~/.openclaw/openclaw.json << EOF
|
||||
# Create configuration with allowed origins
|
||||
sudo -u openclaw mkdir -p /home/openclaw/.openclaw
|
||||
sudo -u openclaw tee /home/openclaw/.openclaw/openclaw.json > /dev/null << EOF
|
||||
{
|
||||
\"gateway\": {
|
||||
\"bind\": \"lan\",
|
||||
\"port\": 18789,
|
||||
\"controlUi\": {
|
||||
\"allowedOrigins\": [
|
||||
\"http://localhost:18789\",
|
||||
\"http://127.0.0.1:18789\",
|
||||
\"http://${CONTAINER_IP}:18789\"
|
||||
"gateway": {
|
||||
"bind": "lan",
|
||||
"port": 18789,
|
||||
"controlUi": {
|
||||
"allowedOrigins": [
|
||||
"http://localhost:18789",
|
||||
"http://127.0.0.1:18789",
|
||||
"http://${CONTAINER_IP}:18789"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF"
|
||||
EOF
|
||||
|
||||
# Restart the service
|
||||
su - openclaw -c "systemctl --user restart openclaw-gateway"
|
||||
sudo -u openclaw systemctl --user restart openclaw-gateway
|
||||
```
|
||||
|
||||
### Method 4: Environment Variable
|
||||
@@ -225,7 +537,7 @@ OPENCLAW_BIND=lan
|
||||
Check the current binding:
|
||||
|
||||
```bash
|
||||
openclaw gateway status
|
||||
sudo -u openclaw openclaw gateway status
|
||||
```
|
||||
|
||||
Output should show:
|
||||
@@ -284,9 +596,9 @@ http://localhost:18789
|
||||
2. Set OpenClaw bind to `tailnet`:
|
||||
|
||||
```bash
|
||||
openclaw configure --section gateway
|
||||
sudo -u openclaw openclaw configure --section gateway
|
||||
# Set bind to "tailnet"
|
||||
su - openclaw -c "systemctl --user restart openclaw-gateway"
|
||||
sudo -u openclaw systemctl --user restart openclaw-gateway
|
||||
```
|
||||
|
||||
3. Access via Tailscale IP: `http://<tailscale-ip>:18789`
|
||||
@@ -296,8 +608,8 @@ http://localhost:18789
|
||||
### Gateway Not Starting
|
||||
|
||||
```bash
|
||||
# Check logs (as openclaw user)
|
||||
su - openclaw -c "journalctl --user -u openclaw-gateway -f"
|
||||
# Check logs
|
||||
sudo -u openclaw journalctl --user -u openclaw-gateway -f
|
||||
|
||||
# Or from root
|
||||
journalctl --user -u openclaw-gateway -f --user-unit
|
||||
@@ -305,83 +617,147 @@ journalctl --user -u openclaw-gateway -f --user-unit
|
||||
# Common issues:
|
||||
# - Port already in use: lsof -i :18789
|
||||
# - Permission denied: ensure running as correct user
|
||||
# - Config errors: sudo -u openclaw openclaw doctor
|
||||
```
|
||||
|
||||
### Cannot Connect from Remote
|
||||
|
||||
1. Verify binding: `openclaw gateway status` should show `0.0.0.0:18789`
|
||||
1. Verify binding: `sudo -u openclaw openclaw gateway status` should show `0.0.0.0:18789`
|
||||
2. Check firewall: `ufw status` or `iptables -L -n`
|
||||
3. Verify network connectivity: `ping <server-ip>`
|
||||
|
||||
### Token Authentication Issues
|
||||
|
||||
```bash
|
||||
# Regenerate token
|
||||
openclaw doctor --generate-gateway-token
|
||||
|
||||
# View current token
|
||||
openclaw gateway status
|
||||
sudo -u openclaw openclaw gateway status
|
||||
|
||||
# Regenerate token (if needed)
|
||||
sudo -u openclaw openclaw doctor --generate-gateway-token
|
||||
```
|
||||
|
||||
## Installing Homebrew (Optional)
|
||||
|
||||
If you need to install additional tools via Homebrew, follow these steps:
|
||||
|
||||
### Prerequisites
|
||||
|
||||
The `openclaw` user needs sudo access and a password set. By default, the user is created without a password.
|
||||
|
||||
### Step 1: Set Password for openclaw User
|
||||
|
||||
As root, set a password for the openclaw user:
|
||||
### Model Provider Issues
|
||||
|
||||
```bash
|
||||
passwd openclaw
|
||||
# Enter and confirm the new password
|
||||
# Check model status
|
||||
sudo -u openclaw openclaw models status
|
||||
|
||||
# Probe providers (may consume tokens)
|
||||
sudo -u openclaw openclaw models status --probe
|
||||
|
||||
# Set default model
|
||||
sudo -u openclaw openclaw models set <provider:model>
|
||||
|
||||
# Example:
|
||||
sudo -u openclaw openclaw models set ollama:llama3.2
|
||||
sudo -u openclaw openclaw models set openai:gpt-4o
|
||||
```
|
||||
|
||||
### Step 2: Install Homebrew
|
||||
|
||||
Switch to the openclaw user and run the installer:
|
||||
### Channel Issues
|
||||
|
||||
```bash
|
||||
su - openclaw
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
# Check channel status
|
||||
sudo -u openclaw openclaw channels status --probe
|
||||
|
||||
# View channel logs
|
||||
sudo -u openclaw openclaw channels logs --channel telegram
|
||||
|
||||
# Re-login to a channel
|
||||
sudo -u openclaw openclaw channels login --channel whatsapp
|
||||
```
|
||||
|
||||
When prompted:
|
||||
## Using Homebrew
|
||||
|
||||
1. Enter the openclaw password for sudo access
|
||||
2. Press ENTER to continue when the installer shows the directories it will create
|
||||
Homebrew is installed automatically during the OpenClaw installation. It provides access to additional packages that may be useful for extending OpenClaw functionality.
|
||||
|
||||
### Step 3: Add Homebrew to PATH
|
||||
|
||||
After installation, add Homebrew to your shell:
|
||||
### Verify Installation
|
||||
|
||||
```bash
|
||||
echo >> /home/openclaw/.bashrc
|
||||
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"' >> /home/openclaw/.bashrc
|
||||
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"
|
||||
```
|
||||
|
||||
### Step 4: Verify Installation
|
||||
|
||||
```bash
|
||||
brew --version
|
||||
sudo -u openclaw brew --version
|
||||
```
|
||||
|
||||
### Installing Packages
|
||||
|
||||
Once Homebrew is installed, you can install packages:
|
||||
Once Homebrew is installed, you can install additional packages:
|
||||
|
||||
```bash
|
||||
brew install <package-name>
|
||||
```
|
||||
|
||||
### Common Useful Packages
|
||||
|
||||
```bash
|
||||
# Example: Install additional tools
|
||||
brew install jq # JSON processor
|
||||
brew install yq # YAML processor
|
||||
brew install git-delta # Better git diff viewer
|
||||
```
|
||||
|
||||
**Note:** Homebrew packages are installed to `/home/linuxbrew/.linuxbrew/` and are available to all users on the system.
|
||||
|
||||
### Troubleshooting Homebrew
|
||||
|
||||
If Homebrew is not working correctly:
|
||||
|
||||
```bash
|
||||
# Check if Homebrew is in PATH
|
||||
which brew
|
||||
|
||||
# If not, add it manually
|
||||
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"
|
||||
|
||||
# Add to .bashrc permanently
|
||||
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"' >> ~/.bashrc
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Embedding Model Comparison
|
||||
|
||||
| Model | Size | Quality | Speed | Use Case |
|
||||
| ----------------- | ------ | ------- | ------- | ----------------------------- |
|
||||
| nomic-embed-text | ~274MB | Good | Fast | General purpose (recommended) |
|
||||
| mxbai-embed-large | ~670MB | Better | Medium | Higher accuracy needed |
|
||||
| all-minilm | ~45MB | Basic | Fastest | Resource-constrained |
|
||||
|
||||
---
|
||||
|
||||
## Hybrid Search (BM25 + Vector)
|
||||
|
||||
Enable hybrid search for better recall:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"memorySearch": {
|
||||
"provider": "ollama",
|
||||
"model": "nomic-embed-text",
|
||||
"remote": {
|
||||
"baseUrl": "http://localhost:11434"
|
||||
},
|
||||
"query": {
|
||||
"hybrid": {
|
||||
"enabled": true,
|
||||
"vectorWeight": 0.7,
|
||||
"textWeight": 0.3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [OpenClaw Gateway Documentation](https://docs.openclaw.ai/gateway)
|
||||
- [OpenClaw Security Guide](https://docs.openclaw.ai/gateway/security)
|
||||
- [OpenClaw Troubleshooting](https://docs.openclaw.ai/gateway/troubleshooting)
|
||||
- [OpenClaw Memory Configuration Reference](https://docs.openclaw.ai/reference/memory-config)
|
||||
- [OpenClaw CLI Reference](https://docs.openclaw.ai/cli)
|
||||
- [OpenClaw Channels](https://docs.openclaw.ai/channels)
|
||||
- [OpenClaw Model Providers](https://docs.openclaw.ai/providers)
|
||||
- [Ollama Documentation](https://ollama.com/docs)
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Requires Node.js 22+ (installed automatically)",
|
||||
"text": "Requires Node.js 24 (installed automatically)",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
@@ -59,12 +59,44 @@
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Supports OpenAI, Anthropic, and other AI providers - API keys required",
|
||||
"text": "CRITICAL: A model provider MUST be configured after installation (Ollama, OpenAI, Anthropic, etc.)",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "Can work with local models via Ollama for offline operation",
|
||||
"text": "To configure a model provider: su - openclaw && openclaw models auth add --provider <provider>",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "For local/offline use: Install Ollama and run 'ollama pull llama3.2' then 'ollama pull nomic-embed-text'",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Memory search requires embedding provider (Ollama, OpenAI, Gemini, Voyage, or local model)",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "To configure channels (Telegram, Discord, WhatsApp, etc.): su - openclaw && openclaw channels add --channel <name>",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Verify installation: su - openclaw && openclaw doctor && openclaw gateway status",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "View logs: su - openclaw && openclaw logs --follow",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Auth token is displayed during installation - save it for pairing devices",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "Homebrew is installed automatically - use 'sudo -u openclaw brew install <package>' to install additional tools",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "The openclaw user has no login shell - use 'sudo -u openclaw <command>' to run commands",
|
||||
"type": "warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# Author: BIllyOutlast
|
||||
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/openclaw/openclaw
|
||||
# Documentation: https://docs.openclaw.ai
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
@@ -38,41 +39,78 @@ $STD apt-get update
|
||||
$STD apt-get install -y caddy
|
||||
msg_ok "Installed Caddy"
|
||||
|
||||
# Setup Node.js 24 (recommended by OpenClaw)
|
||||
# Setup Node.js 24 (recommended by OpenClaw documentation)
|
||||
NODE_VERSION="24" setup_nodejs
|
||||
|
||||
# Install uv (Python package manager) - OpenClaw uses Python for some tools
|
||||
PYTHON_VERSION="3.12" setup_uv
|
||||
|
||||
# Create unprivileged user for OpenClaw
|
||||
# Create unprivileged user for OpenClaw (with nologin shell for security)
|
||||
msg_info "Creating OpenClaw User"
|
||||
if ! id -u openclaw &>/dev/null; then
|
||||
useradd -r -s /bin/bash -d /home/openclaw -m openclaw
|
||||
# Add openclaw to sudoers for specific operations if needed
|
||||
usermod -aG sudo openclaw 2>/dev/null || true
|
||||
# Create user with nologin shell to prevent direct login
|
||||
useradd -r -s /usr/sbin/nologin -d /home/openclaw -m openclaw
|
||||
fi
|
||||
msg_ok "Created OpenClaw User"
|
||||
|
||||
# Configure password-less sudo for openclaw user
|
||||
msg_info "Configuring Password-less Sudo for OpenClaw User"
|
||||
cat <<EOF >/etc/sudoers.d/openclaw
|
||||
# Allow openclaw user to run any command without password
|
||||
openclaw ALL=(ALL) NOPASSWD: ALL
|
||||
EOF
|
||||
chmod 440 /etc/sudoers.d/openclaw
|
||||
msg_ok "Configured Password-less Sudo"
|
||||
|
||||
# Configure npm to use user-writable directory for global packages
|
||||
msg_info "Configuring npm for User Packages"
|
||||
mkdir -p /home/openclaw/.npm-global
|
||||
chown -R openclaw:openclaw /home/openclaw/.npm-global
|
||||
# Set npm prefix for openclaw user to use home directory
|
||||
run_user_cmd="su - openclaw -c"
|
||||
$run_user_cmd "npm config set prefix /home/openclaw/.npm-global"
|
||||
# Using sudo -u since openclaw user has nologin shell
|
||||
sudo -u openclaw npm config set prefix /home/openclaw/.npm-global
|
||||
# Add to PATH for openclaw user
|
||||
echo 'export PATH=/home/openclaw/.npm-global/bin:$PATH' >> /home/openclaw/.bashrc
|
||||
msg_ok "Configured npm for User Packages"
|
||||
|
||||
# Install Homebrew for the openclaw user
|
||||
msg_info "Installing Homebrew"
|
||||
# Create Homebrew directory structure
|
||||
mkdir -p /home/linuxbrew/.linuxbrew
|
||||
chown -R openclaw:openclaw /home/linuxbrew 2>/dev/null || true
|
||||
|
||||
# Download Homebrew installer
|
||||
curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh -o /tmp/brew-install.sh
|
||||
|
||||
# Run installer non-interactively as openclaw user
|
||||
# CI=1 and NONINTERACTIVE=1 enable fully automated installation
|
||||
# Using sudo -u since openclaw user has nologin shell
|
||||
CI=1 NONINTERACTIVE=1 sudo -u openclaw bash /tmp/brew-install.sh || true
|
||||
rm -f /tmp/brew-install.sh
|
||||
|
||||
# Add Homebrew to PATH for openclaw user
|
||||
if [[ -f /home/linuxbrew/.linuxbrew/bin/brew ]]; then
|
||||
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"' >> /home/openclaw/.bashrc
|
||||
msg_ok "Installed Homebrew"
|
||||
else
|
||||
msg_warn "Homebrew installation may have failed - check logs"
|
||||
fi
|
||||
|
||||
msg_info "Installing OpenClaw"
|
||||
$STD npm install -g openclaw@latest
|
||||
# Install OpenClaw globally as the openclaw user
|
||||
# Using sudo -u since openclaw user has nologin shell
|
||||
sudo -u openclaw npm install -g openclaw@latest
|
||||
msg_ok "Installed OpenClaw"
|
||||
|
||||
msg_info "Creating Directories"
|
||||
mkdir -p /opt/openclaw
|
||||
mkdir -p /home/openclaw/.openclaw
|
||||
mkdir -p /home/openclaw/.openclaw/workspace
|
||||
mkdir -p /home/openclaw/.openclaw/workspace/memory
|
||||
chown -R openclaw:openclaw /opt/openclaw
|
||||
chown -R openclaw:openclaw /home/openclaw/.openclaw
|
||||
# Security: Restrict state directory to owner only (fixes world-readable warning)
|
||||
chmod 700 /home/openclaw/.openclaw
|
||||
msg_ok "Created Directories"
|
||||
|
||||
msg_info "Creating OpenClaw Configuration"
|
||||
@@ -80,11 +118,16 @@ msg_info "Creating OpenClaw Configuration"
|
||||
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
|
||||
# Get hostname for HTTPS origins
|
||||
HOSTNAME_FQDN=$(hostname -f 2>/dev/null || hostname)
|
||||
# Generate a secure random auth token (32 bytes = 64 hex chars)
|
||||
AUTH_TOKEN=$(openssl rand -hex 32)
|
||||
cat <<EOF >/home/openclaw/.openclaw/openclaw.json
|
||||
{
|
||||
"gateway": {
|
||||
"bind": "lan",
|
||||
"port": 18789,
|
||||
"auth": {
|
||||
"token": "${AUTH_TOKEN}"
|
||||
},
|
||||
"controlUi": {
|
||||
"allowedOrigins": [
|
||||
"http://localhost:18789",
|
||||
@@ -94,11 +137,21 @@ cat <<EOF >/home/openclaw/.openclaw/openclaw.json
|
||||
"https://${CONTAINER_IP}:18790",
|
||||
"https://${HOSTNAME_FQDN}:18790"
|
||||
]
|
||||
},
|
||||
"reload": {
|
||||
"mode": "hybrid"
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"workspace": "/home/openclaw/.openclaw/workspace"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
chown openclaw:openclaw /home/openclaw/.openclaw/openclaw.json
|
||||
# Security: Restrict config file to owner only (fixes world-readable critical issue)
|
||||
chmod 600 /home/openclaw/.openclaw/openclaw.json
|
||||
msg_ok "Created OpenClaw Configuration"
|
||||
|
||||
msg_info "Creating Self-Signed Certificate for HTTPS"
|
||||
@@ -156,18 +209,133 @@ msg_info "Enabling Lingering for OpenClaw User"
|
||||
loginctl enable-linger openclaw 2>/dev/null || true
|
||||
msg_ok "Enabled Lingering"
|
||||
|
||||
msg_info "Initializing OpenClaw Gateway Service"
|
||||
# Run OpenClaw installer as the openclaw user to create user-level systemd service
|
||||
# The installer will prompt for hooks and create the service automatically
|
||||
msg_info "Installing OpenClaw Gateway Service"
|
||||
# Run OpenClaw onboarding to create user-level systemd service properly
|
||||
# Using --non-interactive with essential defaults
|
||||
export PATH="/home/openclaw/.npm-global/bin:$PATH"
|
||||
$run_user_cmd "openclaw install --no-hooks" 2>/dev/null || true
|
||||
msg_ok "Initialized OpenClaw Gateway Service"
|
||||
export OPENCLAW_CONFIG_PATH="/home/openclaw/.openclaw/openclaw.json"
|
||||
|
||||
# Install the gateway service using the recommended method
|
||||
# Using sudo -u since openclaw user has nologin shell
|
||||
sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" openclaw gateway install 2>/dev/null || true
|
||||
|
||||
# If the service wasn't created, create it manually
|
||||
if [[ ! -f /home/openclaw/.config/systemd/user/openclaw-gateway.service ]]; then
|
||||
msg_info "Creating Manual Systemd Service"
|
||||
mkdir -p /home/openclaw/.config/systemd/user
|
||||
cat <<EOF >/home/openclaw/.config/systemd/user/openclaw-gateway.service
|
||||
[Unit]
|
||||
Description=OpenClaw Gateway
|
||||
Documentation=https://docs.openclaw.ai
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/home/openclaw/.openclaw
|
||||
Environment="PATH=/home/openclaw/.npm-global/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
Environment="OPENCLAW_CONFIG_PATH=/home/openclaw/.openclaw/openclaw.json"
|
||||
ExecStart=/home/openclaw/.npm-global/bin/openclaw gateway
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
chown -R openclaw:openclaw /home/openclaw/.config
|
||||
chmod 600 /home/openclaw/.config/systemd/user/openclaw-gateway.service
|
||||
fi
|
||||
msg_ok "Installed OpenClaw Gateway Service"
|
||||
|
||||
msg_info "Starting Caddy HTTPS Proxy"
|
||||
# Restart Caddy to apply configuration
|
||||
systemctl restart caddy
|
||||
msg_ok "Started Caddy HTTPS Proxy"
|
||||
|
||||
msg_info "Starting OpenClaw Gateway"
|
||||
# Start the gateway service as the openclaw user
|
||||
# Using sudo -u since openclaw user has nologin shell
|
||||
sudo -u openclaw systemctl --user enable openclaw-gateway 2>/dev/null || true
|
||||
sudo -u openclaw systemctl --user start openclaw-gateway 2>/dev/null || true
|
||||
msg_ok "Started OpenClaw Gateway"
|
||||
|
||||
# Wait for gateway to be ready
|
||||
msg_info "Verifying Installation"
|
||||
sleep 5
|
||||
|
||||
# Check if gateway is running
|
||||
if sudo -u openclaw systemctl --user is-active openclaw-gateway 2>/dev/null | grep -q "active"; then
|
||||
msg_ok "Gateway Service is Active"
|
||||
else
|
||||
msg_warn "Gateway Service may not be running - check logs with: sudo -u openclaw journalctl --user -u openclaw-gateway"
|
||||
fi
|
||||
|
||||
# Display auth token for pairing
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════════════════════════════════════════"
|
||||
echo " OpenClaw Auth Token (save this for pairing):"
|
||||
echo " ${AUTH_TOKEN}"
|
||||
echo "═══════════════════════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo " Access URLs:"
|
||||
echo " HTTP: http://${CONTAINER_IP}:18789"
|
||||
echo " HTTPS: https://${CONTAINER_IP}:18790"
|
||||
echo ""
|
||||
echo "═══════���═══════════════════════════════════════════════════════════════════════"
|
||||
echo " Next Steps:"
|
||||
echo "═══════════════════════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo " Note: The openclaw user has no login shell. Use 'sudo -u openclaw' to run commands."
|
||||
echo ""
|
||||
echo " 1. Configure a Model Provider (REQUIRED for OpenClaw to function):"
|
||||
echo ""
|
||||
echo " Option A - Ollama (Local, Free):"
|
||||
echo " • Install Ollama: curl -fsSL https://ollama.com/install.sh | sh"
|
||||
echo " • Pull a model: ollama pull llama3.2"
|
||||
echo " • Pull embedding model: ollama pull nomic-embed-text"
|
||||
echo " • Configure OpenClaw:"
|
||||
echo " sudo -u openclaw openclaw configure"
|
||||
echo " # Select Ollama as provider"
|
||||
echo ""
|
||||
echo " Option B - OpenAI API:"
|
||||
echo " sudo -u openclaw openclaw models auth add --provider openai"
|
||||
echo " # Enter your API key when prompted"
|
||||
echo ""
|
||||
echo " Option C - Anthropic Claude:"
|
||||
echo " sudo -u openclaw openclaw models auth add --provider anthropic"
|
||||
echo " # Enter your API key when prompted"
|
||||
echo ""
|
||||
echo " 2. Configure Channels (Optional - for messaging):"
|
||||
echo " sudo -u openclaw openclaw channels add --channel telegram --token YOUR_BOT_TOKEN"
|
||||
echo " sudo -u openclaw openclaw channels add --channel discord --token YOUR_BOT_TOKEN"
|
||||
echo ""
|
||||
echo " 3. Verify Installation:"
|
||||
echo " sudo -u openclaw openclaw doctor"
|
||||
echo " sudo -u openclaw openclaw gateway status"
|
||||
echo " sudo -u openclaw openclaw models status"
|
||||
echo ""
|
||||
echo " 4. View Logs:"
|
||||
echo " sudo -u openclaw openclaw logs --follow"
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════════════════════════════════════════"
|
||||
echo " Memory Search Configuration"
|
||||
echo "═══════════════════════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo " Memory search requires an embedding provider. Configure one of:"
|
||||
echo " - Ollama (local): https://docs.openclaw.ai/reference/memory-config"
|
||||
echo " - OpenAI, Gemini, Voyage, or Mistral (cloud)"
|
||||
echo ""
|
||||
echo " To configure Ollama (recommended for offline use):"
|
||||
echo " 1. Ensure Ollama is running on this host or accessible remotely"
|
||||
echo " 2. Pull an embedding model: ollama pull nomic-embed-text"
|
||||
echo " 3. Edit config: nano ~/.openclaw/openclaw.json"
|
||||
echo ""
|
||||
echo " Add to config under 'agents.defaults.memorySearch':"
|
||||
echo ' { "provider": "ollama", "model": "nomic-embed-text",'
|
||||
echo ' "remote": { "baseUrl": "http://YOUR_OLLAMA_HOST:11434" } }'
|
||||
echo ""
|
||||
echo " See docs/guides/openclaw-configuration.md for detailed setup."
|
||||
echo ""
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
|
||||
@@ -4,6 +4,7 @@ source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/build.func)
|
||||
# Author: BIllyOutlast
|
||||
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/openclaw/openclaw
|
||||
# Documentation: https://docs.openclaw.ai
|
||||
|
||||
APP="openclaw"
|
||||
var_tags="${var_tags:-ai;assistant;automation}"
|
||||
@@ -25,13 +26,14 @@ function update_script() {
|
||||
check_container_resources
|
||||
|
||||
# Check if openclaw is installed (npm global bin location varies)
|
||||
if ! command -v openclaw &>/dev/null; then
|
||||
if ! command -v openclaw &>/dev/null && [[ ! -f /home/openclaw/.npm-global/bin/openclaw ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Get current version
|
||||
CURRENT_VERSION=$(openclaw --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
|
||||
# Using sudo -u since openclaw user has nologin shell
|
||||
CURRENT_VERSION=$(sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" openclaw --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
|
||||
|
||||
# Get latest version from npm
|
||||
LATEST_VERSION=$(npm view openclaw version 2>/dev/null || echo "unknown")
|
||||
@@ -48,33 +50,68 @@ function update_script() {
|
||||
|
||||
msg_info "Updating OpenClaw from v${CURRENT_VERSION} to v${LATEST_VERSION}"
|
||||
|
||||
msg_info "Stopping Service"
|
||||
su - openclaw -c "systemctl --user stop openclaw-gateway" 2>/dev/null || true
|
||||
msg_ok "Stopped Service"
|
||||
msg_info "Stopping Gateway Service"
|
||||
sudo -u openclaw systemctl --user stop openclaw-gateway 2>/dev/null || true
|
||||
msg_ok "Stopped Gateway Service"
|
||||
|
||||
msg_info "Backing up Configuration"
|
||||
BACKUP_DIR="/tmp/openclaw_backup_$(date +%Y%m%d_%H%M%S)"
|
||||
if [[ -d /home/openclaw/.openclaw ]]; then
|
||||
cp -r /home/openclaw/.openclaw /tmp/openclaw_backup
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
cp -r /home/openclaw/.openclaw/* "$BACKUP_DIR"/ 2>/dev/null || true
|
||||
msg_ok "Backed up Configuration to $BACKUP_DIR"
|
||||
else
|
||||
msg_warn "No configuration directory found"
|
||||
fi
|
||||
msg_ok "Backed up Configuration"
|
||||
|
||||
msg_info "Updating OpenClaw"
|
||||
$STD npm update -g openclaw
|
||||
msg_ok "Updated OpenClaw"
|
||||
msg_info "Updating OpenClaw Package"
|
||||
# Run npm update as the openclaw user using sudo -u (nologin shell)
|
||||
sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" npm update -g openclaw 2>/dev/null
|
||||
msg_ok "Updated OpenClaw Package"
|
||||
|
||||
msg_info "Restoring Configuration"
|
||||
if [[ -d /tmp/openclaw_backup ]]; then
|
||||
cp -r /tmp/openclaw_backup/. /home/openclaw/.openclaw 2>/dev/null || true
|
||||
rm -rf /tmp/openclaw_backup
|
||||
msg_info "Verifying Update"
|
||||
NEW_VERSION=$(sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" openclaw --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
|
||||
if [[ "$NEW_VERSION" == "$LATEST_VERSION" ]]; then
|
||||
msg_ok "Verified OpenClaw v${NEW_VERSION}"
|
||||
else
|
||||
msg_warn "Version verification failed. Installed: v${NEW_VERSION}, Expected: v${LATEST_VERSION}"
|
||||
fi
|
||||
chown -R openclaw:openclaw /home/openclaw/.openclaw 2>/dev/null || true
|
||||
msg_ok "Restored Configuration"
|
||||
|
||||
msg_info "Starting Service"
|
||||
su - openclaw -c "systemctl --user start openclaw-gateway" 2>/dev/null || true
|
||||
msg_ok "Started Service"
|
||||
msg_info "Starting Gateway Service"
|
||||
sudo -u openclaw systemctl --user start openclaw-gateway 2>/dev/null || true
|
||||
|
||||
# Wait for service to start
|
||||
sleep 3
|
||||
|
||||
# Check if service is running
|
||||
if sudo -u openclaw systemctl --user is-active openclaw-gateway 2>/dev/null | grep -q "active"; then
|
||||
msg_ok "Gateway Service Started"
|
||||
else
|
||||
msg_warn "Gateway Service may not have started properly"
|
||||
msg_info "Checking service status..."
|
||||
sudo -u openclaw systemctl --user status openclaw-gateway 2>/dev/null || true
|
||||
fi
|
||||
|
||||
msg_ok "Updated successfully to v${LATEST_VERSION}!"
|
||||
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════════════════════════════════════════"
|
||||
echo " Post-Update Verification"
|
||||
echo "═══════════════════════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo " Note: The openclaw user has no login shell. Use 'sudo -u openclaw' to run commands."
|
||||
echo ""
|
||||
echo " Run these commands to verify the update:"
|
||||
echo " sudo -u openclaw openclaw --version"
|
||||
echo " sudo -u openclaw openclaw doctor"
|
||||
echo " sudo -u openclaw openclaw gateway status"
|
||||
echo ""
|
||||
echo " View logs if issues occur:"
|
||||
echo " sudo -u openclaw openclaw logs --follow"
|
||||
echo ""
|
||||
echo " Backup saved to: $BACKUP_DIR"
|
||||
echo ""
|
||||
|
||||
exit
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,315 @@
|
||||
# OpenClaw Configuration Guide
|
||||
|
||||
## Quick Start
|
||||
|
||||
After installation, OpenClaw requires a **model provider** to function. This is the most critical configuration step.
|
||||
|
||||
### Essential Commands
|
||||
|
||||
**Note:** The `openclaw` user has no login shell for security. Use `sudo -u openclaw` to run commands.
|
||||
|
||||
```bash
|
||||
# Verify installation
|
||||
sudo -u openclaw openclaw --version
|
||||
sudo -u openclaw openclaw doctor
|
||||
sudo -u openclaw openclaw gateway status
|
||||
|
||||
# View logs
|
||||
sudo -u openclaw openclaw logs --follow
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Model Provider Configuration (REQUIRED)
|
||||
|
||||
OpenClaw requires a model provider to process messages. Choose one of the following options:
|
||||
|
||||
### Option A: Ollama (Local, Free, Recommended for Offline)
|
||||
|
||||
Ollama runs models locally on your hardware - no API costs, complete privacy.
|
||||
|
||||
#### 1. Install Ollama
|
||||
|
||||
```bash
|
||||
# On the OpenClaw container or a separate server
|
||||
curl -fsSL https://ollama.com/install.sh | sh
|
||||
```
|
||||
|
||||
#### 2. Pull Required Models
|
||||
|
||||
```bash
|
||||
# Chat model (choose one)
|
||||
ollama pull llama3.2 # Good balance (recommended)
|
||||
ollama pull llama3.1:8b # Larger, more capable
|
||||
ollama pull mistral # Alternative option
|
||||
ollama pull gemma2 # Google's model
|
||||
|
||||
# Embedding model (required for memory search)
|
||||
ollama pull nomic-embed-text # Recommended (274MB)
|
||||
ollama pull mxbai-embed-large # Higher quality (670MB)
|
||||
ollama pull all-minilm # Smallest, fastest (45MB)
|
||||
```
|
||||
|
||||
#### 3. Configure OpenClaw for Ollama
|
||||
|
||||
```bash
|
||||
sudo -u openclaw openclaw configure
|
||||
# Select "Ollama" when prompted for model provider
|
||||
# Enter Ollama URL (default: http://localhost:11434)
|
||||
```
|
||||
|
||||
Or manually edit `~/.openclaw/openclaw.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"model": {
|
||||
"primary": "ollama:llama3.2"
|
||||
},
|
||||
"memorySearch": {
|
||||
"provider": "ollama",
|
||||
"model": "nomic-embed-text",
|
||||
"remote": {
|
||||
"baseUrl": "http://localhost:11434"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Ollama on a Separate Server
|
||||
|
||||
If Ollama runs on a different machine:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"model": {
|
||||
"primary": "ollama:llama3.2"
|
||||
},
|
||||
"memorySearch": {
|
||||
"provider": "ollama",
|
||||
"model": "nomic-embed-text",
|
||||
"remote": {
|
||||
"baseUrl": "http://192.168.1.100:11434"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Option B: OpenAI API
|
||||
|
||||
```bash
|
||||
sudo -u openclaw openclaw models auth add --provider openai
|
||||
# Enter your API key when prompted
|
||||
|
||||
# Set as default model
|
||||
sudo -u openclaw openclaw models set openai:gpt-4o
|
||||
```
|
||||
|
||||
Manual configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"secrets": {
|
||||
"providers": {
|
||||
"openai-api": {
|
||||
"source": "env",
|
||||
"env": "OPENAI_API_KEY"
|
||||
}
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"model": {
|
||||
"primary": "openai:gpt-4o"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Option C: Anthropic Claude
|
||||
|
||||
```bash
|
||||
sudo -u openclaw openclaw models auth add --provider anthropic
|
||||
# Enter your API key when prompted
|
||||
|
||||
# Set as default model
|
||||
sudo -u openclaw openclaw models set anthropic:claude-opus-4-6
|
||||
```
|
||||
|
||||
### Option D: Other Providers
|
||||
|
||||
OpenClaw supports many providers:
|
||||
|
||||
| Provider | Command |
|
||||
| ------------- | ------------------------------------------------ |
|
||||
| Google Gemini | `openclaw models auth add --provider google` |
|
||||
| Mistral | `openclaw models auth add --provider mistral` |
|
||||
| OpenRouter | `openclaw models auth add --provider openrouter` |
|
||||
| Groq | `openclaw models auth add --provider groq` |
|
||||
| xAI | `openclaw models auth add --provider xai` |
|
||||
| Together AI | `openclaw models auth add --provider together` |
|
||||
|
||||
### Verify Model Configuration
|
||||
|
||||
```bash
|
||||
# Check model status
|
||||
openclaw models status
|
||||
|
||||
# Test with a message
|
||||
openclaw agent --message "Hello, are you working?" --local
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Channel Configuration
|
||||
|
||||
OpenClaw supports multiple messaging channels. Configure channels to interact through your preferred platform.
|
||||
|
||||
### Supported Channels
|
||||
|
||||
| Channel | Type | Setup Method |
|
||||
| --------------- | ------------- | ------------------------------- |
|
||||
| Telegram | Bot API | Bot token from @BotFather |
|
||||
| Discord | Bot API | Bot token from Developer Portal |
|
||||
| WhatsApp | Baileys | QR code pairing |
|
||||
| Slack | Bolt SDK | App credentials |
|
||||
| Signal | signal-cli | Phone number pairing |
|
||||
| Matrix | Plugin | Homeserver config |
|
||||
| iMessage | BlueBubbles | macOS server required |
|
||||
| Microsoft Teams | Bot Framework | App registration |
|
||||
| IRC | Native | Server connection |
|
||||
| Google Chat | Webhook | App configuration |
|
||||
|
||||
### Telegram Setup (Easiest)
|
||||
|
||||
1. **Create a bot** by messaging [@BotFather](https://t.me/BotFather) on Telegram
|
||||
2. **Get your bot token** (format: `123456789:ABCdefGHIjklMNOpqrsTUVwxyz`)
|
||||
3. **Configure OpenClaw**:
|
||||
|
||||
```bash
|
||||
sudo -u openclaw openclaw channels add --channel telegram --token "YOUR_BOT_TOKEN"
|
||||
```
|
||||
|
||||
4. **Start messaging** your bot on Telegram
|
||||
|
||||
### Discord Setup
|
||||
|
||||
1. **Create a bot** at [Discord Developer Portal](https://discord.com/developers/applications)
|
||||
2. **Get your bot token** from the Bot section
|
||||
3. **Invite bot to server** using OAuth2 URL generator
|
||||
4. **Configure OpenClaw**:
|
||||
|
||||
```bash
|
||||
sudo -u openclaw openclaw channels add --channel discord --token "YOUR_BOT_TOKEN"
|
||||
```
|
||||
|
||||
### WhatsApp Setup
|
||||
|
||||
WhatsApp requires QR code pairing:
|
||||
|
||||
```bash
|
||||
sudo -u openclaw openclaw channels add --channel whatsapp
|
||||
# A QR code will be displayed
|
||||
# Scan with WhatsApp on your phone (Settings > Linked Devices)
|
||||
```
|
||||
|
||||
### Multiple Accounts
|
||||
|
||||
You can configure multiple accounts per channel:
|
||||
|
||||
```bash
|
||||
# Add a second Telegram account
|
||||
openclaw channels add --channel telegram --account work --name "Work Bot" --token "WORK_BOT_TOKEN"
|
||||
|
||||
# List all channels
|
||||
openclaw channels list
|
||||
|
||||
# Check channel status
|
||||
openclaw channels status --probe
|
||||
```
|
||||
|
||||
### Channel Routing
|
||||
|
||||
Route different channels to different agents:
|
||||
|
||||
```bash
|
||||
# Create an agent
|
||||
openclaw agents add --workspace ~/.openclaw-workspaces/work
|
||||
|
||||
# Bind channels to agent
|
||||
openclaw agents bind --agent <agent-id> --bind telegram:work
|
||||
openclaw agents bind --agent <agent-id> --bind discord:main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Memory Configuration
|
||||
|
||||
OpenClaw's memory search requires an embedding provider to index and search through your memory files.
|
||||
|
||||
### Memory Files Location
|
||||
|
||||
Memory files are stored in `~/.openclaw/workspace/memory/`:
|
||||
|
||||
```bash
|
||||
# Create memory directory
|
||||
mkdir -p ~/.openclaw/workspace/memory
|
||||
|
||||
# Create your first memory file
|
||||
cat > ~/.openclaw/workspace/MEMORY.md << 'EOF'
|
||||
# Personal Memory
|
||||
|
||||
## Preferences
|
||||
- I prefer concise responses
|
||||
- Use bullet points for lists
|
||||
|
||||
## Projects
|
||||
- Home automation: running on Proxmox
|
||||
- Media server: Jellyfin on port 8096
|
||||
EOF
|
||||
```
|
||||
|
||||
### Configure Memory Search
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"memorySearch": {
|
||||
"provider": "ollama",
|
||||
"model": "nomic-embed-text",
|
||||
"remote": {
|
||||
"baseUrl": "http://localhost:11434"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Memory Commands
|
||||
|
||||
```bash
|
||||
# Check memory status
|
||||
openclaw memory status
|
||||
|
||||
# Reindex memory files
|
||||
openclaw memory index
|
||||
|
||||
# Search memory
|
||||
openclaw memory search "project details"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## "Device Identity Required" Error Fix
|
||||
|
||||
If you see this error when accessing the Control UI from another machine:
|
||||
@@ -43,7 +353,6 @@ nano ~/.openclaw/openclaw.json
|
||||
"allowInsecureAuth": true
|
||||
},
|
||||
"auth": {
|
||||
"mode": "token",
|
||||
"token": "your-secure-token-here"
|
||||
}
|
||||
}
|
||||
@@ -64,7 +373,7 @@ su - openclaw -c "systemctl --user restart openclaw-gateway"
|
||||
|
||||
### Solution 3: HTTPS Reverse Proxy (Production)
|
||||
|
||||
Set up Caddy or Nginx with SSL certificates for secure HTTPS access.
|
||||
The installation includes Caddy for HTTPS access on port 18790.
|
||||
|
||||
### Solution 4: Tailscale VPN
|
||||
|
||||
@@ -86,6 +395,8 @@ For emergency access only, you can completely disable device auth:
|
||||
|
||||
**⚠️ WARNING:** This is a severe security downgrade. Revert immediately after emergency use.
|
||||
|
||||
---
|
||||
|
||||
## "Origin Not Allowed" Error Fix
|
||||
|
||||
If you see this error when accessing the Control UI from another machine:
|
||||
@@ -101,8 +412,8 @@ origin not allowed (open the Control UI from the gateway host or allow it in gat
|
||||
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
|
||||
|
||||
# Create configuration with allowed origins
|
||||
mkdir -p ~/.openclaw
|
||||
cat > ~/.openclaw/openclaw.json << EOF
|
||||
sudo -u openclaw mkdir -p /home/openclaw/.openclaw
|
||||
sudo -u openclaw tee /home/openclaw/.openclaw/openclaw.json > /dev/null << EOF
|
||||
{
|
||||
"gateway": {
|
||||
"bind": "lan",
|
||||
@@ -119,11 +430,13 @@ cat > ~/.openclaw/openclaw.json << EOF
|
||||
EOF
|
||||
|
||||
# Apply and restart
|
||||
su - openclaw -c "systemctl --user restart openclaw-gateway"
|
||||
sudo -u openclaw systemctl --user restart openclaw-gateway
|
||||
```
|
||||
|
||||
**Note:** Even with `allowedOrigins` configured, you still need a secure context (HTTPS or localhost) for the device identity requirement. Use SSH tunneling for LAN access.
|
||||
|
||||
---
|
||||
|
||||
## Network Binding Configuration
|
||||
|
||||
By default, OpenClaw binds to localhost (127.0.0.1) only. To make it accessible from other machines on your network, you need to:
|
||||
@@ -144,11 +457,11 @@ By default, OpenClaw binds to localhost (127.0.0.1) only. To make it accessible
|
||||
|
||||
```bash
|
||||
# Open the gateway configuration
|
||||
openclaw configure --section gateway
|
||||
sudo -u openclaw openclaw configure --section gateway
|
||||
|
||||
# Set bind to "lan" when prompted
|
||||
# Then restart the service
|
||||
su - openclaw -c "systemctl --user restart openclaw-gateway"
|
||||
sudo -u openclaw systemctl --user restart openclaw-gateway
|
||||
```
|
||||
|
||||
### Method 2: Manual Configuration
|
||||
@@ -156,9 +469,8 @@ su - openclaw -c "systemctl --user restart openclaw-gateway"
|
||||
Edit the configuration file:
|
||||
|
||||
```bash
|
||||
# Edit as the openclaw user
|
||||
su - openclaw
|
||||
nano ~/.openclaw/openclaw.json
|
||||
# Edit the configuration file
|
||||
nano /home/openclaw/.openclaw/openclaw.json
|
||||
```
|
||||
|
||||
Add the configuration with your IP:
|
||||
@@ -178,7 +490,7 @@ Add the configuration with your IP:
|
||||
Apply changes:
|
||||
|
||||
```bash
|
||||
systemctl --user restart openclaw-gateway
|
||||
sudo -u openclaw systemctl --user restart openclaw-gateway
|
||||
```
|
||||
|
||||
### Method 3: Quick Fix for Existing Installations
|
||||
@@ -189,26 +501,26 @@ For existing installations, run these commands:
|
||||
# Get your container IP
|
||||
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
|
||||
|
||||
# Create configuration with allowed origins (as openclaw user)
|
||||
su - openclaw -c "mkdir -p ~/.openclaw"
|
||||
su - openclaw -c "cat > ~/.openclaw/openclaw.json << EOF
|
||||
# Create configuration with allowed origins
|
||||
sudo -u openclaw mkdir -p /home/openclaw/.openclaw
|
||||
sudo -u openclaw tee /home/openclaw/.openclaw/openclaw.json > /dev/null << EOF
|
||||
{
|
||||
\"gateway\": {
|
||||
\"bind\": \"lan\",
|
||||
\"port\": 18789,
|
||||
\"controlUi\": {
|
||||
\"allowedOrigins\": [
|
||||
\"http://localhost:18789\",
|
||||
\"http://127.0.0.1:18789\",
|
||||
\"http://${CONTAINER_IP}:18789\"
|
||||
"gateway": {
|
||||
"bind": "lan",
|
||||
"port": 18789,
|
||||
"controlUi": {
|
||||
"allowedOrigins": [
|
||||
"http://localhost:18789",
|
||||
"http://127.0.0.1:18789",
|
||||
"http://${CONTAINER_IP}:18789"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF"
|
||||
EOF
|
||||
|
||||
# Restart the service
|
||||
su - openclaw -c "systemctl --user restart openclaw-gateway"
|
||||
sudo -u openclaw systemctl --user restart openclaw-gateway
|
||||
```
|
||||
|
||||
### Method 4: Environment Variable
|
||||
@@ -225,7 +537,7 @@ OPENCLAW_BIND=lan
|
||||
Check the current binding:
|
||||
|
||||
```bash
|
||||
openclaw gateway status
|
||||
sudo -u openclaw openclaw gateway status
|
||||
```
|
||||
|
||||
Output should show:
|
||||
@@ -284,9 +596,9 @@ http://localhost:18789
|
||||
2. Set OpenClaw bind to `tailnet`:
|
||||
|
||||
```bash
|
||||
openclaw configure --section gateway
|
||||
sudo -u openclaw openclaw configure --section gateway
|
||||
# Set bind to "tailnet"
|
||||
su - openclaw -c "systemctl --user restart openclaw-gateway"
|
||||
sudo -u openclaw systemctl --user restart openclaw-gateway
|
||||
```
|
||||
|
||||
3. Access via Tailscale IP: `http://<tailscale-ip>:18789`
|
||||
@@ -296,8 +608,8 @@ http://localhost:18789
|
||||
### Gateway Not Starting
|
||||
|
||||
```bash
|
||||
# Check logs (as openclaw user)
|
||||
su - openclaw -c "journalctl --user -u openclaw-gateway -f"
|
||||
# Check logs
|
||||
sudo -u openclaw journalctl --user -u openclaw-gateway -f
|
||||
|
||||
# Or from root
|
||||
journalctl --user -u openclaw-gateway -f --user-unit
|
||||
@@ -305,83 +617,147 @@ journalctl --user -u openclaw-gateway -f --user-unit
|
||||
# Common issues:
|
||||
# - Port already in use: lsof -i :18789
|
||||
# - Permission denied: ensure running as correct user
|
||||
# - Config errors: sudo -u openclaw openclaw doctor
|
||||
```
|
||||
|
||||
### Cannot Connect from Remote
|
||||
|
||||
1. Verify binding: `openclaw gateway status` should show `0.0.0.0:18789`
|
||||
1. Verify binding: `sudo -u openclaw openclaw gateway status` should show `0.0.0.0:18789`
|
||||
2. Check firewall: `ufw status` or `iptables -L -n`
|
||||
3. Verify network connectivity: `ping <server-ip>`
|
||||
|
||||
### Token Authentication Issues
|
||||
|
||||
```bash
|
||||
# Regenerate token
|
||||
openclaw doctor --generate-gateway-token
|
||||
|
||||
# View current token
|
||||
openclaw gateway status
|
||||
sudo -u openclaw openclaw gateway status
|
||||
|
||||
# Regenerate token (if needed)
|
||||
sudo -u openclaw openclaw doctor --generate-gateway-token
|
||||
```
|
||||
|
||||
## Installing Homebrew (Optional)
|
||||
|
||||
If you need to install additional tools via Homebrew, follow these steps:
|
||||
|
||||
### Prerequisites
|
||||
|
||||
The `openclaw` user needs sudo access and a password set. By default, the user is created without a password.
|
||||
|
||||
### Step 1: Set Password for openclaw User
|
||||
|
||||
As root, set a password for the openclaw user:
|
||||
### Model Provider Issues
|
||||
|
||||
```bash
|
||||
passwd openclaw
|
||||
# Enter and confirm the new password
|
||||
# Check model status
|
||||
sudo -u openclaw openclaw models status
|
||||
|
||||
# Probe providers (may consume tokens)
|
||||
sudo -u openclaw openclaw models status --probe
|
||||
|
||||
# Set default model
|
||||
sudo -u openclaw openclaw models set <provider:model>
|
||||
|
||||
# Example:
|
||||
sudo -u openclaw openclaw models set ollama:llama3.2
|
||||
sudo -u openclaw openclaw models set openai:gpt-4o
|
||||
```
|
||||
|
||||
### Step 2: Install Homebrew
|
||||
|
||||
Switch to the openclaw user and run the installer:
|
||||
### Channel Issues
|
||||
|
||||
```bash
|
||||
su - openclaw
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
# Check channel status
|
||||
sudo -u openclaw openclaw channels status --probe
|
||||
|
||||
# View channel logs
|
||||
sudo -u openclaw openclaw channels logs --channel telegram
|
||||
|
||||
# Re-login to a channel
|
||||
sudo -u openclaw openclaw channels login --channel whatsapp
|
||||
```
|
||||
|
||||
When prompted:
|
||||
## Using Homebrew
|
||||
|
||||
1. Enter the openclaw password for sudo access
|
||||
2. Press ENTER to continue when the installer shows the directories it will create
|
||||
Homebrew is installed automatically during the OpenClaw installation. It provides access to additional packages that may be useful for extending OpenClaw functionality.
|
||||
|
||||
### Step 3: Add Homebrew to PATH
|
||||
|
||||
After installation, add Homebrew to your shell:
|
||||
### Verify Installation
|
||||
|
||||
```bash
|
||||
echo >> /home/openclaw/.bashrc
|
||||
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"' >> /home/openclaw/.bashrc
|
||||
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"
|
||||
```
|
||||
|
||||
### Step 4: Verify Installation
|
||||
|
||||
```bash
|
||||
brew --version
|
||||
sudo -u openclaw brew --version
|
||||
```
|
||||
|
||||
### Installing Packages
|
||||
|
||||
Once Homebrew is installed, you can install packages:
|
||||
Once Homebrew is installed, you can install additional packages:
|
||||
|
||||
```bash
|
||||
brew install <package-name>
|
||||
```
|
||||
|
||||
### Common Useful Packages
|
||||
|
||||
```bash
|
||||
# Example: Install additional tools
|
||||
brew install jq # JSON processor
|
||||
brew install yq # YAML processor
|
||||
brew install git-delta # Better git diff viewer
|
||||
```
|
||||
|
||||
**Note:** Homebrew packages are installed to `/home/linuxbrew/.linuxbrew/` and are available to all users on the system.
|
||||
|
||||
### Troubleshooting Homebrew
|
||||
|
||||
If Homebrew is not working correctly:
|
||||
|
||||
```bash
|
||||
# Check if Homebrew is in PATH
|
||||
which brew
|
||||
|
||||
# If not, add it manually
|
||||
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"
|
||||
|
||||
# Add to .bashrc permanently
|
||||
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"' >> ~/.bashrc
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Embedding Model Comparison
|
||||
|
||||
| Model | Size | Quality | Speed | Use Case |
|
||||
| ----------------- | ------ | ------- | ------- | ----------------------------- |
|
||||
| nomic-embed-text | ~274MB | Good | Fast | General purpose (recommended) |
|
||||
| mxbai-embed-large | ~670MB | Better | Medium | Higher accuracy needed |
|
||||
| all-minilm | ~45MB | Basic | Fastest | Resource-constrained |
|
||||
|
||||
---
|
||||
|
||||
## Hybrid Search (BM25 + Vector)
|
||||
|
||||
Enable hybrid search for better recall:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"memorySearch": {
|
||||
"provider": "ollama",
|
||||
"model": "nomic-embed-text",
|
||||
"remote": {
|
||||
"baseUrl": "http://localhost:11434"
|
||||
},
|
||||
"query": {
|
||||
"hybrid": {
|
||||
"enabled": true,
|
||||
"vectorWeight": 0.7,
|
||||
"textWeight": 0.3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [OpenClaw Gateway Documentation](https://docs.openclaw.ai/gateway)
|
||||
- [OpenClaw Security Guide](https://docs.openclaw.ai/gateway/security)
|
||||
- [OpenClaw Troubleshooting](https://docs.openclaw.ai/gateway/troubleshooting)
|
||||
- [OpenClaw Memory Configuration Reference](https://docs.openclaw.ai/reference/memory-config)
|
||||
- [OpenClaw CLI Reference](https://docs.openclaw.ai/cli)
|
||||
- [OpenClaw Channels](https://docs.openclaw.ai/channels)
|
||||
- [OpenClaw Model Providers](https://docs.openclaw.ai/providers)
|
||||
- [Ollama Documentation](https://ollama.com/docs)
|
||||
|
||||
@@ -4,6 +4,7 @@ source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/build.func)
|
||||
# Author: BIllyOutlast
|
||||
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/openclaw/openclaw
|
||||
# Documentation: https://docs.openclaw.ai
|
||||
|
||||
APP="openclaw"
|
||||
var_tags="${var_tags:-ai;assistant;automation}"
|
||||
@@ -24,13 +25,15 @@ function update_script() {
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
|
||||
if [[ ! -x /usr/bin/openclaw ]]; then
|
||||
# Check if openclaw is installed (npm global bin location varies)
|
||||
if ! command -v openclaw &>/dev/null && [[ ! -f /home/openclaw/.npm-global/bin/openclaw ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Get current version
|
||||
CURRENT_VERSION=$(openclaw --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
|
||||
# Using sudo -u since openclaw user has nologin shell
|
||||
CURRENT_VERSION=$(sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" openclaw --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
|
||||
|
||||
# Get latest version from npm
|
||||
LATEST_VERSION=$(npm view openclaw version 2>/dev/null || echo "unknown")
|
||||
@@ -47,32 +50,68 @@ function update_script() {
|
||||
|
||||
msg_info "Updating OpenClaw from v${CURRENT_VERSION} to v${LATEST_VERSION}"
|
||||
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop openclaw
|
||||
msg_ok "Stopped Service"
|
||||
msg_info "Stopping Gateway Service"
|
||||
sudo -u openclaw systemctl --user stop openclaw-gateway 2>/dev/null || true
|
||||
msg_ok "Stopped Gateway Service"
|
||||
|
||||
msg_info "Backing up Configuration"
|
||||
if [[ -d /root/.openclaw ]]; then
|
||||
cp -r /root/.openclaw /tmp/openclaw_backup
|
||||
BACKUP_DIR="/tmp/openclaw_backup_$(date +%Y%m%d_%H%M%S)"
|
||||
if [[ -d /home/openclaw/.openclaw ]]; then
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
cp -r /home/openclaw/.openclaw/* "$BACKUP_DIR"/ 2>/dev/null || true
|
||||
msg_ok "Backed up Configuration to $BACKUP_DIR"
|
||||
else
|
||||
msg_warn "No configuration directory found"
|
||||
fi
|
||||
msg_ok "Backed up Configuration"
|
||||
|
||||
msg_info "Updating OpenClaw"
|
||||
$STD npm update -g openclaw
|
||||
msg_ok "Updated OpenClaw"
|
||||
msg_info "Updating OpenClaw Package"
|
||||
# Run npm update as the openclaw user using sudo -u (nologin shell)
|
||||
sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" npm update -g openclaw 2>/dev/null
|
||||
msg_ok "Updated OpenClaw Package"
|
||||
|
||||
msg_info "Restoring Configuration"
|
||||
if [[ -d /tmp/openclaw_backup ]]; then
|
||||
cp -r /tmp/openclaw_backup/. /root/.openclaw 2>/dev/null || true
|
||||
rm -rf /tmp/openclaw_backup
|
||||
msg_info "Verifying Update"
|
||||
NEW_VERSION=$(sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" openclaw --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
|
||||
if [[ "$NEW_VERSION" == "$LATEST_VERSION" ]]; then
|
||||
msg_ok "Verified OpenClaw v${NEW_VERSION}"
|
||||
else
|
||||
msg_warn "Version verification failed. Installed: v${NEW_VERSION}, Expected: v${LATEST_VERSION}"
|
||||
fi
|
||||
msg_ok "Restored Configuration"
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start openclaw
|
||||
msg_ok "Started Service"
|
||||
msg_info "Starting Gateway Service"
|
||||
sudo -u openclaw systemctl --user start openclaw-gateway 2>/dev/null || true
|
||||
|
||||
# Wait for service to start
|
||||
sleep 3
|
||||
|
||||
# Check if service is running
|
||||
if sudo -u openclaw systemctl --user is-active openclaw-gateway 2>/dev/null | grep -q "active"; then
|
||||
msg_ok "Gateway Service Started"
|
||||
else
|
||||
msg_warn "Gateway Service may not have started properly"
|
||||
msg_info "Checking service status..."
|
||||
sudo -u openclaw systemctl --user status openclaw-gateway 2>/dev/null || true
|
||||
fi
|
||||
|
||||
msg_ok "Updated successfully to v${LATEST_VERSION}!"
|
||||
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════════════════════════════════════════"
|
||||
echo " Post-Update Verification"
|
||||
echo "═══════════════════════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo " Note: The openclaw user has no login shell. Use 'sudo -u openclaw' to run commands."
|
||||
echo ""
|
||||
echo " Run these commands to verify the update:"
|
||||
echo " sudo -u openclaw openclaw --version"
|
||||
echo " sudo -u openclaw openclaw doctor"
|
||||
echo " sudo -u openclaw openclaw gateway status"
|
||||
echo ""
|
||||
echo " View logs if issues occur:"
|
||||
echo " sudo -u openclaw openclaw logs --follow"
|
||||
echo ""
|
||||
echo " Backup saved to: $BACKUP_DIR"
|
||||
echo ""
|
||||
|
||||
exit
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,315 @@
|
||||
# OpenClaw Configuration Guide
|
||||
|
||||
## Quick Start
|
||||
|
||||
After installation, OpenClaw requires a **model provider** to function. This is the most critical configuration step.
|
||||
|
||||
### Essential Commands
|
||||
|
||||
**Note:** The `openclaw` user has no login shell for security. Use `sudo -u openclaw` to run commands.
|
||||
|
||||
```bash
|
||||
# Verify installation
|
||||
sudo -u openclaw openclaw --version
|
||||
sudo -u openclaw openclaw doctor
|
||||
sudo -u openclaw openclaw gateway status
|
||||
|
||||
# View logs
|
||||
sudo -u openclaw openclaw logs --follow
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Model Provider Configuration (REQUIRED)
|
||||
|
||||
OpenClaw requires a model provider to process messages. Choose one of the following options:
|
||||
|
||||
### Option A: Ollama (Local, Free, Recommended for Offline)
|
||||
|
||||
Ollama runs models locally on your hardware - no API costs, complete privacy.
|
||||
|
||||
#### 1. Install Ollama
|
||||
|
||||
```bash
|
||||
# On the OpenClaw container or a separate server
|
||||
curl -fsSL https://ollama.com/install.sh | sh
|
||||
```
|
||||
|
||||
#### 2. Pull Required Models
|
||||
|
||||
```bash
|
||||
# Chat model (choose one)
|
||||
ollama pull llama3.2 # Good balance (recommended)
|
||||
ollama pull llama3.1:8b # Larger, more capable
|
||||
ollama pull mistral # Alternative option
|
||||
ollama pull gemma2 # Google's model
|
||||
|
||||
# Embedding model (required for memory search)
|
||||
ollama pull nomic-embed-text # Recommended (274MB)
|
||||
ollama pull mxbai-embed-large # Higher quality (670MB)
|
||||
ollama pull all-minilm # Smallest, fastest (45MB)
|
||||
```
|
||||
|
||||
#### 3. Configure OpenClaw for Ollama
|
||||
|
||||
```bash
|
||||
sudo -u openclaw openclaw configure
|
||||
# Select "Ollama" when prompted for model provider
|
||||
# Enter Ollama URL (default: http://localhost:11434)
|
||||
```
|
||||
|
||||
Or manually edit `~/.openclaw/openclaw.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"model": {
|
||||
"primary": "ollama:llama3.2"
|
||||
},
|
||||
"memorySearch": {
|
||||
"provider": "ollama",
|
||||
"model": "nomic-embed-text",
|
||||
"remote": {
|
||||
"baseUrl": "http://localhost:11434"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Ollama on a Separate Server
|
||||
|
||||
If Ollama runs on a different machine:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"model": {
|
||||
"primary": "ollama:llama3.2"
|
||||
},
|
||||
"memorySearch": {
|
||||
"provider": "ollama",
|
||||
"model": "nomic-embed-text",
|
||||
"remote": {
|
||||
"baseUrl": "http://192.168.1.100:11434"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Option B: OpenAI API
|
||||
|
||||
```bash
|
||||
sudo -u openclaw openclaw models auth add --provider openai
|
||||
# Enter your API key when prompted
|
||||
|
||||
# Set as default model
|
||||
sudo -u openclaw openclaw models set openai:gpt-4o
|
||||
```
|
||||
|
||||
Manual configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"secrets": {
|
||||
"providers": {
|
||||
"openai-api": {
|
||||
"source": "env",
|
||||
"env": "OPENAI_API_KEY"
|
||||
}
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"model": {
|
||||
"primary": "openai:gpt-4o"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Option C: Anthropic Claude
|
||||
|
||||
```bash
|
||||
sudo -u openclaw openclaw models auth add --provider anthropic
|
||||
# Enter your API key when prompted
|
||||
|
||||
# Set as default model
|
||||
sudo -u openclaw openclaw models set anthropic:claude-opus-4-6
|
||||
```
|
||||
|
||||
### Option D: Other Providers
|
||||
|
||||
OpenClaw supports many providers:
|
||||
|
||||
| Provider | Command |
|
||||
| ------------- | ------------------------------------------------ |
|
||||
| Google Gemini | `openclaw models auth add --provider google` |
|
||||
| Mistral | `openclaw models auth add --provider mistral` |
|
||||
| OpenRouter | `openclaw models auth add --provider openrouter` |
|
||||
| Groq | `openclaw models auth add --provider groq` |
|
||||
| xAI | `openclaw models auth add --provider xai` |
|
||||
| Together AI | `openclaw models auth add --provider together` |
|
||||
|
||||
### Verify Model Configuration
|
||||
|
||||
```bash
|
||||
# Check model status
|
||||
openclaw models status
|
||||
|
||||
# Test with a message
|
||||
openclaw agent --message "Hello, are you working?" --local
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Channel Configuration
|
||||
|
||||
OpenClaw supports multiple messaging channels. Configure channels to interact through your preferred platform.
|
||||
|
||||
### Supported Channels
|
||||
|
||||
| Channel | Type | Setup Method |
|
||||
| --------------- | ------------- | ------------------------------- |
|
||||
| Telegram | Bot API | Bot token from @BotFather |
|
||||
| Discord | Bot API | Bot token from Developer Portal |
|
||||
| WhatsApp | Baileys | QR code pairing |
|
||||
| Slack | Bolt SDK | App credentials |
|
||||
| Signal | signal-cli | Phone number pairing |
|
||||
| Matrix | Plugin | Homeserver config |
|
||||
| iMessage | BlueBubbles | macOS server required |
|
||||
| Microsoft Teams | Bot Framework | App registration |
|
||||
| IRC | Native | Server connection |
|
||||
| Google Chat | Webhook | App configuration |
|
||||
|
||||
### Telegram Setup (Easiest)
|
||||
|
||||
1. **Create a bot** by messaging [@BotFather](https://t.me/BotFather) on Telegram
|
||||
2. **Get your bot token** (format: `123456789:ABCdefGHIjklMNOpqrsTUVwxyz`)
|
||||
3. **Configure OpenClaw**:
|
||||
|
||||
```bash
|
||||
sudo -u openclaw openclaw channels add --channel telegram --token "YOUR_BOT_TOKEN"
|
||||
```
|
||||
|
||||
4. **Start messaging** your bot on Telegram
|
||||
|
||||
### Discord Setup
|
||||
|
||||
1. **Create a bot** at [Discord Developer Portal](https://discord.com/developers/applications)
|
||||
2. **Get your bot token** from the Bot section
|
||||
3. **Invite bot to server** using OAuth2 URL generator
|
||||
4. **Configure OpenClaw**:
|
||||
|
||||
```bash
|
||||
sudo -u openclaw openclaw channels add --channel discord --token "YOUR_BOT_TOKEN"
|
||||
```
|
||||
|
||||
### WhatsApp Setup
|
||||
|
||||
WhatsApp requires QR code pairing:
|
||||
|
||||
```bash
|
||||
sudo -u openclaw openclaw channels add --channel whatsapp
|
||||
# A QR code will be displayed
|
||||
# Scan with WhatsApp on your phone (Settings > Linked Devices)
|
||||
```
|
||||
|
||||
### Multiple Accounts
|
||||
|
||||
You can configure multiple accounts per channel:
|
||||
|
||||
```bash
|
||||
# Add a second Telegram account
|
||||
openclaw channels add --channel telegram --account work --name "Work Bot" --token "WORK_BOT_TOKEN"
|
||||
|
||||
# List all channels
|
||||
openclaw channels list
|
||||
|
||||
# Check channel status
|
||||
openclaw channels status --probe
|
||||
```
|
||||
|
||||
### Channel Routing
|
||||
|
||||
Route different channels to different agents:
|
||||
|
||||
```bash
|
||||
# Create an agent
|
||||
openclaw agents add --workspace ~/.openclaw-workspaces/work
|
||||
|
||||
# Bind channels to agent
|
||||
openclaw agents bind --agent <agent-id> --bind telegram:work
|
||||
openclaw agents bind --agent <agent-id> --bind discord:main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Memory Configuration
|
||||
|
||||
OpenClaw's memory search requires an embedding provider to index and search through your memory files.
|
||||
|
||||
### Memory Files Location
|
||||
|
||||
Memory files are stored in `~/.openclaw/workspace/memory/`:
|
||||
|
||||
```bash
|
||||
# Create memory directory
|
||||
mkdir -p ~/.openclaw/workspace/memory
|
||||
|
||||
# Create your first memory file
|
||||
cat > ~/.openclaw/workspace/MEMORY.md << 'EOF'
|
||||
# Personal Memory
|
||||
|
||||
## Preferences
|
||||
- I prefer concise responses
|
||||
- Use bullet points for lists
|
||||
|
||||
## Projects
|
||||
- Home automation: running on Proxmox
|
||||
- Media server: Jellyfin on port 8096
|
||||
EOF
|
||||
```
|
||||
|
||||
### Configure Memory Search
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"memorySearch": {
|
||||
"provider": "ollama",
|
||||
"model": "nomic-embed-text",
|
||||
"remote": {
|
||||
"baseUrl": "http://localhost:11434"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Memory Commands
|
||||
|
||||
```bash
|
||||
# Check memory status
|
||||
openclaw memory status
|
||||
|
||||
# Reindex memory files
|
||||
openclaw memory index
|
||||
|
||||
# Search memory
|
||||
openclaw memory search "project details"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## "Device Identity Required" Error Fix
|
||||
|
||||
If you see this error when accessing the Control UI from another machine:
|
||||
@@ -43,7 +353,6 @@ nano ~/.openclaw/openclaw.json
|
||||
"allowInsecureAuth": true
|
||||
},
|
||||
"auth": {
|
||||
"mode": "token",
|
||||
"token": "your-secure-token-here"
|
||||
}
|
||||
}
|
||||
@@ -53,7 +362,7 @@ nano ~/.openclaw/openclaw.json
|
||||
Then restart:
|
||||
|
||||
```bash
|
||||
systemctl restart openclaw
|
||||
su - openclaw -c "systemctl --user restart openclaw-gateway"
|
||||
```
|
||||
|
||||
**Important:**
|
||||
@@ -64,7 +373,7 @@ systemctl restart openclaw
|
||||
|
||||
### Solution 3: HTTPS Reverse Proxy (Production)
|
||||
|
||||
Set up Caddy or Nginx with SSL certificates for secure HTTPS access.
|
||||
The installation includes Caddy for HTTPS access on port 18790.
|
||||
|
||||
### Solution 4: Tailscale VPN
|
||||
|
||||
@@ -86,6 +395,8 @@ For emergency access only, you can completely disable device auth:
|
||||
|
||||
**⚠️ WARNING:** This is a severe security downgrade. Revert immediately after emergency use.
|
||||
|
||||
---
|
||||
|
||||
## "Origin Not Allowed" Error Fix
|
||||
|
||||
If you see this error when accessing the Control UI from another machine:
|
||||
@@ -101,8 +412,8 @@ origin not allowed (open the Control UI from the gateway host or allow it in gat
|
||||
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
|
||||
|
||||
# Create configuration with allowed origins
|
||||
mkdir -p ~/.openclaw
|
||||
cat > ~/.openclaw/openclaw.json << EOF
|
||||
sudo -u openclaw mkdir -p /home/openclaw/.openclaw
|
||||
sudo -u openclaw tee /home/openclaw/.openclaw/openclaw.json > /dev/null << EOF
|
||||
{
|
||||
"gateway": {
|
||||
"bind": "lan",
|
||||
@@ -118,16 +429,14 @@ cat > ~/.openclaw/openclaw.json << EOF
|
||||
}
|
||||
EOF
|
||||
|
||||
# Update service to use lan binding
|
||||
sed -i 's|--port 18789|--port 18789 --bind lan|' /etc/systemd/system/openclaw.service
|
||||
|
||||
# Apply and restart
|
||||
systemctl daemon-reload
|
||||
systemctl restart openclaw
|
||||
sudo -u openclaw systemctl --user restart openclaw-gateway
|
||||
```
|
||||
|
||||
**Note:** Even with `allowedOrigins` configured, you still need a secure context (HTTPS or localhost) for the device identity requirement. Use SSH tunneling for LAN access.
|
||||
|
||||
---
|
||||
|
||||
## Network Binding Configuration
|
||||
|
||||
By default, OpenClaw binds to localhost (127.0.0.1) only. To make it accessible from other machines on your network, you need to:
|
||||
@@ -148,20 +457,20 @@ By default, OpenClaw binds to localhost (127.0.0.1) only. To make it accessible
|
||||
|
||||
```bash
|
||||
# Open the gateway configuration
|
||||
openclaw configure --section gateway
|
||||
sudo -u openclaw openclaw configure --section gateway
|
||||
|
||||
# Set bind to "lan" when prompted
|
||||
# Then restart the service
|
||||
systemctl restart openclaw
|
||||
sudo -u openclaw systemctl --user restart openclaw-gateway
|
||||
```
|
||||
|
||||
### Method 2: Manual Configuration
|
||||
|
||||
Edit both the configuration file and systemd service:
|
||||
Edit the configuration file:
|
||||
|
||||
```bash
|
||||
# Create/edit the configuration file
|
||||
nano ~/.openclaw/openclaw.json
|
||||
# Edit the configuration file
|
||||
nano /home/openclaw/.openclaw/openclaw.json
|
||||
```
|
||||
|
||||
Add the configuration with your IP:
|
||||
@@ -178,24 +487,10 @@ Add the configuration with your IP:
|
||||
}
|
||||
```
|
||||
|
||||
Then edit the systemd service:
|
||||
|
||||
```bash
|
||||
nano /etc/systemd/system/openclaw.service
|
||||
```
|
||||
|
||||
Modify the `ExecStart` line to include `--bind lan`:
|
||||
|
||||
```ini
|
||||
[Service]
|
||||
ExecStart=/usr/bin/openclaw gateway --port 18789 --bind lan
|
||||
```
|
||||
|
||||
Apply changes:
|
||||
|
||||
```bash
|
||||
systemctl daemon-reload
|
||||
systemctl restart openclaw
|
||||
sudo -u openclaw systemctl --user restart openclaw-gateway
|
||||
```
|
||||
|
||||
### Method 3: Quick Fix for Existing Installations
|
||||
@@ -207,8 +502,8 @@ For existing installations, run these commands:
|
||||
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
|
||||
|
||||
# Create configuration with allowed origins
|
||||
mkdir -p ~/.openclaw
|
||||
cat > ~/.openclaw/openclaw.json << EOF
|
||||
sudo -u openclaw mkdir -p /home/openclaw/.openclaw
|
||||
sudo -u openclaw tee /home/openclaw/.openclaw/openclaw.json > /dev/null << EOF
|
||||
{
|
||||
"gateway": {
|
||||
"bind": "lan",
|
||||
@@ -224,12 +519,8 @@ cat > ~/.openclaw/openclaw.json << EOF
|
||||
}
|
||||
EOF
|
||||
|
||||
# Update service to use lan binding
|
||||
sed -i 's|--port 18789|--port 18789 --bind lan|' /etc/systemd/system/openclaw.service
|
||||
|
||||
# Apply and restart
|
||||
systemctl daemon-reload
|
||||
systemctl restart openclaw
|
||||
# Restart the service
|
||||
sudo -u openclaw systemctl --user restart openclaw-gateway
|
||||
```
|
||||
|
||||
### Method 4: Environment Variable
|
||||
@@ -246,7 +537,7 @@ OPENCLAW_BIND=lan
|
||||
Check the current binding:
|
||||
|
||||
```bash
|
||||
openclaw gateway status
|
||||
sudo -u openclaw openclaw gateway status
|
||||
```
|
||||
|
||||
Output should show:
|
||||
@@ -305,9 +596,9 @@ http://localhost:18789
|
||||
2. Set OpenClaw bind to `tailnet`:
|
||||
|
||||
```bash
|
||||
openclaw configure --section gateway
|
||||
sudo -u openclaw openclaw configure --section gateway
|
||||
# Set bind to "tailnet"
|
||||
systemctl restart openclaw
|
||||
sudo -u openclaw systemctl --user restart openclaw-gateway
|
||||
```
|
||||
|
||||
3. Access via Tailscale IP: `http://<tailscale-ip>:18789`
|
||||
@@ -318,88 +609,155 @@ http://localhost:18789
|
||||
|
||||
```bash
|
||||
# Check logs
|
||||
journalctl -u openclaw -f
|
||||
sudo -u openclaw journalctl --user -u openclaw-gateway -f
|
||||
|
||||
# Or from root
|
||||
journalctl --user -u openclaw-gateway -f --user-unit
|
||||
|
||||
# Common issues:
|
||||
# - Port already in use: lsof -i :18789
|
||||
# - Permission denied: ensure running as root or correct user
|
||||
# - Permission denied: ensure running as correct user
|
||||
# - Config errors: sudo -u openclaw openclaw doctor
|
||||
```
|
||||
|
||||
### Cannot Connect from Remote
|
||||
|
||||
1. Verify binding: `openclaw gateway status` should show `0.0.0.0:18789`
|
||||
1. Verify binding: `sudo -u openclaw openclaw gateway status` should show `0.0.0.0:18789`
|
||||
2. Check firewall: `ufw status` or `iptables -L -n`
|
||||
3. Verify network connectivity: `ping <server-ip>`
|
||||
|
||||
### Token Authentication Issues
|
||||
|
||||
```bash
|
||||
# Regenerate token
|
||||
openclaw doctor --generate-gateway-token
|
||||
|
||||
# View current token
|
||||
openclaw gateway status
|
||||
sudo -u openclaw openclaw gateway status
|
||||
|
||||
# Regenerate token (if needed)
|
||||
sudo -u openclaw openclaw doctor --generate-gateway-token
|
||||
```
|
||||
|
||||
## Installing Homebrew (Optional)
|
||||
|
||||
If you need to install additional tools via Homebrew, follow these steps:
|
||||
|
||||
### Prerequisites
|
||||
|
||||
The `openclaw` user needs sudo access and a password set. By default, the user is created without a password.
|
||||
|
||||
### Step 1: Set Password for openclaw User
|
||||
|
||||
As root, set a password for the openclaw user:
|
||||
### Model Provider Issues
|
||||
|
||||
```bash
|
||||
passwd openclaw
|
||||
# Enter and confirm the new password
|
||||
# Check model status
|
||||
sudo -u openclaw openclaw models status
|
||||
|
||||
# Probe providers (may consume tokens)
|
||||
sudo -u openclaw openclaw models status --probe
|
||||
|
||||
# Set default model
|
||||
sudo -u openclaw openclaw models set <provider:model>
|
||||
|
||||
# Example:
|
||||
sudo -u openclaw openclaw models set ollama:llama3.2
|
||||
sudo -u openclaw openclaw models set openai:gpt-4o
|
||||
```
|
||||
|
||||
### Step 2: Install Homebrew
|
||||
|
||||
Switch to the openclaw user and run the installer:
|
||||
### Channel Issues
|
||||
|
||||
```bash
|
||||
su - openclaw
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
# Check channel status
|
||||
sudo -u openclaw openclaw channels status --probe
|
||||
|
||||
# View channel logs
|
||||
sudo -u openclaw openclaw channels logs --channel telegram
|
||||
|
||||
# Re-login to a channel
|
||||
sudo -u openclaw openclaw channels login --channel whatsapp
|
||||
```
|
||||
|
||||
When prompted:
|
||||
## Using Homebrew
|
||||
|
||||
1. Enter the openclaw password for sudo access
|
||||
2. Press ENTER to continue when the installer shows the directories it will create
|
||||
Homebrew is installed automatically during the OpenClaw installation. It provides access to additional packages that may be useful for extending OpenClaw functionality.
|
||||
|
||||
### Step 3: Add Homebrew to PATH
|
||||
|
||||
After installation, add Homebrew to your shell:
|
||||
### Verify Installation
|
||||
|
||||
```bash
|
||||
echo >> /home/openclaw/.bashrc
|
||||
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"' >> /home/openclaw/.bashrc
|
||||
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"
|
||||
```
|
||||
|
||||
### Step 4: Verify Installation
|
||||
|
||||
```bash
|
||||
brew --version
|
||||
sudo -u openclaw brew --version
|
||||
```
|
||||
|
||||
### Installing Packages
|
||||
|
||||
Once Homebrew is installed, you can install packages:
|
||||
Once Homebrew is installed, you can install additional packages:
|
||||
|
||||
```bash
|
||||
brew install <package-name>
|
||||
```
|
||||
|
||||
### Common Useful Packages
|
||||
|
||||
```bash
|
||||
# Example: Install additional tools
|
||||
brew install jq # JSON processor
|
||||
brew install yq # YAML processor
|
||||
brew install git-delta # Better git diff viewer
|
||||
```
|
||||
|
||||
**Note:** Homebrew packages are installed to `/home/linuxbrew/.linuxbrew/` and are available to all users on the system.
|
||||
|
||||
### Troubleshooting Homebrew
|
||||
|
||||
If Homebrew is not working correctly:
|
||||
|
||||
```bash
|
||||
# Check if Homebrew is in PATH
|
||||
which brew
|
||||
|
||||
# If not, add it manually
|
||||
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"
|
||||
|
||||
# Add to .bashrc permanently
|
||||
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"' >> ~/.bashrc
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Embedding Model Comparison
|
||||
|
||||
| Model | Size | Quality | Speed | Use Case |
|
||||
| ----------------- | ------ | ------- | ------- | ----------------------------- |
|
||||
| nomic-embed-text | ~274MB | Good | Fast | General purpose (recommended) |
|
||||
| mxbai-embed-large | ~670MB | Better | Medium | Higher accuracy needed |
|
||||
| all-minilm | ~45MB | Basic | Fastest | Resource-constrained |
|
||||
|
||||
---
|
||||
|
||||
## Hybrid Search (BM25 + Vector)
|
||||
|
||||
Enable hybrid search for better recall:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"memorySearch": {
|
||||
"provider": "ollama",
|
||||
"model": "nomic-embed-text",
|
||||
"remote": {
|
||||
"baseUrl": "http://localhost:11434"
|
||||
},
|
||||
"query": {
|
||||
"hybrid": {
|
||||
"enabled": true,
|
||||
"vectorWeight": 0.7,
|
||||
"textWeight": 0.3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [OpenClaw Gateway Documentation](https://docs.openclaw.ai/gateway)
|
||||
- [OpenClaw Security Guide](https://docs.openclaw.ai/gateway/security)
|
||||
- [OpenClaw Troubleshooting](https://docs.openclaw.ai/gateway/troubleshooting)
|
||||
- [OpenClaw Memory Configuration Reference](https://docs.openclaw.ai/reference/memory-config)
|
||||
- [OpenClaw CLI Reference](https://docs.openclaw.ai/cli)
|
||||
- [OpenClaw Channels](https://docs.openclaw.ai/channels)
|
||||
- [OpenClaw Model Providers](https://docs.openclaw.ai/providers)
|
||||
- [Ollama Documentation](https://ollama.com/docs)
|
||||
|
||||
@@ -38,14 +38,11 @@ $STD apt-get update
|
||||
$STD apt-get install -y caddy
|
||||
msg_ok "Installed Caddy"
|
||||
|
||||
# Setup Node.js 22 (required by OpenClaw)
|
||||
NODE_VERSION="22" setup_nodejs
|
||||
# Setup Node.js 24 (recommended by OpenClaw)
|
||||
NODE_VERSION="24" setup_nodejs
|
||||
|
||||
# Install uv (Python package manager)
|
||||
msg_info "Installing uv (Python Package Manager)"
|
||||
$STD curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
msg_ok "Installed uv"
|
||||
# Install uv (Python package manager) - OpenClaw uses Python for some tools
|
||||
PYTHON_VERSION="3.12" setup_uv
|
||||
|
||||
# Create unprivileged user for OpenClaw
|
||||
msg_info "Creating OpenClaw User"
|
||||
@@ -154,32 +151,17 @@ chown caddy:caddy /var/log/caddy
|
||||
systemctl enable -q caddy
|
||||
msg_ok "Created Caddy HTTPS Reverse Proxy"
|
||||
|
||||
msg_info "Creating OpenClaw Service"
|
||||
cat <<EOF >/etc/systemd/system/openclaw.service
|
||||
[Unit]
|
||||
Description=OpenClaw Gateway - Personal AI Assistant
|
||||
After=network.target network-online.target
|
||||
Wants=network-online.target
|
||||
msg_info "Enabling Lingering for OpenClaw User"
|
||||
# Enable lingering so user-level systemd services start on boot
|
||||
loginctl enable-linger openclaw 2>/dev/null || true
|
||||
msg_ok "Enabled Lingering"
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=openclaw
|
||||
Group=openclaw
|
||||
WorkingDirectory=/opt/openclaw
|
||||
Environment=NODE_ENV=production
|
||||
Environment=PATH=/home/openclaw/.npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
ExecStart=/usr/bin/openclaw gateway --port 18789 --bind lan
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=openclaw
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q --now openclaw
|
||||
msg_ok "Created OpenClaw Service"
|
||||
msg_info "Initializing OpenClaw Gateway Service"
|
||||
# Run OpenClaw installer as the openclaw user to create user-level systemd service
|
||||
# The installer will prompt for hooks and create the service automatically
|
||||
export PATH="/home/openclaw/.npm-global/bin:$PATH"
|
||||
$run_user_cmd "openclaw install --no-hooks" 2>/dev/null || true
|
||||
msg_ok "Initialized OpenClaw Gateway Service"
|
||||
|
||||
msg_info "Starting Caddy HTTPS Proxy"
|
||||
# Restart Caddy to apply configuration
|
||||
|
||||
+13
-7
@@ -1,9 +1,7 @@
|
||||
{
|
||||
"name": "OpenClaw",
|
||||
"slug": "openclaw",
|
||||
"categories": [
|
||||
20
|
||||
],
|
||||
"categories": [20],
|
||||
"date_created": "2026-03-12",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
@@ -12,7 +10,7 @@
|
||||
"documentation": "https://docs.openclaw.ai",
|
||||
"website": "https://openclaw.ai",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/openclaw.webp",
|
||||
"config_path": "/root/.openclaw/openclaw.json",
|
||||
"config_path": "/home/openclaw/.openclaw/openclaw.json",
|
||||
"description": "OpenClaw is a personal AI assistant that runs on your own devices. It supports multiple messaging channels including WhatsApp, Telegram, Slack, Discord, Signal, iMessage, IRC, Microsoft Teams, Matrix, and more. Features voice wake mode, live canvas, browser control, skills platform, and a Gateway WebSocket control plane.",
|
||||
"install_methods": [
|
||||
{
|
||||
@@ -37,7 +35,11 @@
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "WebSocket endpoint: ws://<IP>:18789",
|
||||
"text": "HTTPS Access (secure): https://<IP>:18790",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "WebSocket endpoint: ws://<IP>:18789 (or wss://<IP>:18790 for secure)",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
@@ -45,11 +47,15 @@
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Run 'openclaw onboard' after installation to configure channels and AI providers",
|
||||
"text": "Configuration stored in /home/openclaw/.openclaw/",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Configuration stored in /root/.openclaw/",
|
||||
"text": "HTTPS uses self-signed certificate - click 'Advanced' -> 'Proceed to site' in browser",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "For remote LAN access, use HTTPS (port 18790) or SSH tunnel: ssh -L 18789:localhost:18789 root@<container-ip>",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
|
||||
+5
-2
@@ -83,5 +83,8 @@ description
|
||||
msg_ok "Completed Successfully!"
|
||||
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}:18789${CL}"
|
||||
echo -e "${INFO}${YW} Gateway WebSocket: ws://${IP}:18789${CL}"
|
||||
echo -e "${TAB}${GATEWAY}${BGN}http://localhost:18789${CL}"
|
||||
echo -e "${INFO}${YW} HTTPS Access (secure, works from any machine):${CL}"
|
||||
echo -e "${TAB}${GATEWAY}${BGN}https://${IP}:18790${CL}"
|
||||
echo -e "${INFO}${YW} Note: Your browser will warn about self-signed certificate.${CL}"
|
||||
echo -e "${TAB}${YW}Click 'Advanced' -> 'Proceed to site' to continue.${CL}"
|
||||
|
||||
+116
-3
@@ -22,14 +22,70 @@ ssh -L 18789:localhost:18789 root@<container-ip>
|
||||
http://localhost:18789
|
||||
```
|
||||
|
||||
### Solution 2: HTTPS Reverse Proxy (Production)
|
||||
### Solution 2: Allow Insecure Auth (LAN Only)
|
||||
|
||||
**Warning:** This reduces security. Only use on trusted networks.
|
||||
|
||||
Add `allowInsecureAuth: true` to your OpenClaw configuration:
|
||||
|
||||
```bash
|
||||
# Edit configuration
|
||||
nano ~/.openclaw/openclaw.json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"gateway": {
|
||||
"bind": "lan",
|
||||
"port": 18789,
|
||||
"controlUi": {
|
||||
"allowedOrigins": ["http://localhost:18789", "http://127.0.0.1:18789", "http://192.168.31.39:18789"],
|
||||
"allowInsecureAuth": true
|
||||
},
|
||||
"auth": {
|
||||
"mode": "token",
|
||||
"token": "your-secure-token-here"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then restart:
|
||||
|
||||
```bash
|
||||
systemctl restart openclaw
|
||||
```
|
||||
|
||||
**Important:**
|
||||
|
||||
- `allowInsecureAuth` only works for localhost connections in non-secure HTTP contexts
|
||||
- It does **NOT** bypass device identity for remote (non-localhost) connections
|
||||
- You must still use SSH tunnel or HTTPS for remote LAN access
|
||||
|
||||
### Solution 3: HTTPS Reverse Proxy (Production)
|
||||
|
||||
Set up Caddy or Nginx with SSL certificates for secure HTTPS access.
|
||||
|
||||
### Solution 3: Tailscale VPN
|
||||
### Solution 4: Tailscale VPN
|
||||
|
||||
Install Tailscale on both machines and access via Tailscale IP.
|
||||
|
||||
### Emergency Break-Glass (NOT Recommended)
|
||||
|
||||
For emergency access only, you can completely disable device auth:
|
||||
|
||||
```json
|
||||
{
|
||||
"gateway": {
|
||||
"controlUi": {
|
||||
"dangerouslyDisableDeviceAuth": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ WARNING:** This is a severe security downgrade. Revert immediately after emergency use.
|
||||
|
||||
## "Origin Not Allowed" Error Fix
|
||||
|
||||
If you see this error when accessing the Control UI from another machine:
|
||||
@@ -116,7 +172,7 @@ Add the configuration with your IP:
|
||||
"bind": "lan",
|
||||
"port": 18789,
|
||||
"controlUi": {
|
||||
"allowedOrigins": ["http://localhost:18789", "http://127.0.0.1:18789", "http://192.168.31.39:18789"]
|
||||
"allowedOrigins": ["http://localhost:18789", "http://127.0.0.1:18789", "http://192.168.1.4:18789"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -285,6 +341,63 @@ openclaw doctor --generate-gateway-token
|
||||
openclaw gateway status
|
||||
```
|
||||
|
||||
## Installing Homebrew (Optional)
|
||||
|
||||
If you need to install additional tools via Homebrew, follow these steps:
|
||||
|
||||
### Prerequisites
|
||||
|
||||
The `openclaw` user needs sudo access and a password set. By default, the user is created without a password.
|
||||
|
||||
### Step 1: Set Password for openclaw User
|
||||
|
||||
As root, set a password for the openclaw user:
|
||||
|
||||
```bash
|
||||
passwd openclaw
|
||||
# Enter and confirm the new password
|
||||
```
|
||||
|
||||
### Step 2: Install Homebrew
|
||||
|
||||
Switch to the openclaw user and run the installer:
|
||||
|
||||
```bash
|
||||
su - openclaw
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
```
|
||||
|
||||
When prompted:
|
||||
|
||||
1. Enter the openclaw password for sudo access
|
||||
2. Press ENTER to continue when the installer shows the directories it will create
|
||||
|
||||
### Step 3: Add Homebrew to PATH
|
||||
|
||||
After installation, add Homebrew to your shell:
|
||||
|
||||
```bash
|
||||
echo >> /home/openclaw/.bashrc
|
||||
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"' >> /home/openclaw/.bashrc
|
||||
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"
|
||||
```
|
||||
|
||||
### Step 4: Verify Installation
|
||||
|
||||
```bash
|
||||
brew --version
|
||||
```
|
||||
|
||||
### Installing Packages
|
||||
|
||||
Once Homebrew is installed, you can install packages:
|
||||
|
||||
```bash
|
||||
brew install <package-name>
|
||||
```
|
||||
|
||||
**Note:** Homebrew packages are installed to `/home/linuxbrew/.linuxbrew/` and are available to all users on the system.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [OpenClaw Gateway Documentation](https://docs.openclaw.ai/gateway)
|
||||
|
||||
+102
-59
@@ -22,9 +22,22 @@ $STD apt-get install -y \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
git \
|
||||
procps
|
||||
procps \
|
||||
debian-keyring \
|
||||
debian-archive-keyring \
|
||||
ffmpeg \
|
||||
sudo
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
# Install Caddy for HTTPS reverse proxy
|
||||
msg_info "Installing Caddy (HTTPS Reverse Proxy)"
|
||||
$STD apt-get install -y debian-keyring debian-archive-keyring apt-transport-https
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' >/etc/apt/sources.list.d/caddy-stable.list
|
||||
$STD apt-get update
|
||||
$STD apt-get install -y caddy
|
||||
msg_ok "Installed Caddy"
|
||||
|
||||
# Setup Node.js 22 (required by OpenClaw)
|
||||
NODE_VERSION="22" setup_nodejs
|
||||
|
||||
@@ -34,60 +47,25 @@ $STD curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
msg_ok "Installed uv"
|
||||
|
||||
# Install Homebrew (Linuxbrew) - use alternative installation for root
|
||||
msg_info "Installing Homebrew (Linuxbrew)"
|
||||
# Homebrew refuses to install as root, so we use the alternative untar method
|
||||
# This installs to /home/linuxbrew/.linuxbrew which is the recommended location
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
# Create the linuxbrew directory with proper permissions
|
||||
mkdir -p /home/linuxbrew/.linuxbrew
|
||||
chmod 775 /home/linuxbrew
|
||||
chmod 775 /home/linuxbrew/.linuxbrew
|
||||
|
||||
# Download and extract Homebrew (alternative installation method)
|
||||
cd /tmp
|
||||
$STD curl -fsSL https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C /home/linuxbrew/.linuxbrew
|
||||
|
||||
# Set permissions so brew can be run by anyone
|
||||
chown -R root:root /home/linuxbrew/.linuxbrew
|
||||
chmod -R g+w /home/linuxbrew/.linuxbrew
|
||||
|
||||
# Create symlinks for easy access
|
||||
ln -sf /home/linuxbrew/.linuxbrew/bin/brew /usr/local/bin/brew 2>/dev/null || true
|
||||
|
||||
# Add to bashrc for persistence
|
||||
if ! grep -q 'linuxbrew/.linuxbrew/bin/brew' /root/.bashrc 2>/dev/null; then
|
||||
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >>/root/.bashrc
|
||||
fi
|
||||
|
||||
# Add to system profile for all users
|
||||
if ! grep -q 'linuxbrew/.linuxbrew' /etc/profile 2>/dev/null; then
|
||||
echo 'export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH"' >>/etc/profile
|
||||
fi
|
||||
|
||||
# Make brew available in current session
|
||||
export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH"
|
||||
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv 2>/dev/null)" || true
|
||||
else
|
||||
# Not running as root, install normally
|
||||
$STD /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
# Create unprivileged user for OpenClaw
|
||||
msg_info "Creating OpenClaw User"
|
||||
if ! id -u openclaw &>/dev/null; then
|
||||
useradd -r -s /bin/bash -d /home/openclaw -m openclaw
|
||||
# Add openclaw to sudoers for specific operations if needed
|
||||
usermod -aG sudo openclaw 2>/dev/null || true
|
||||
fi
|
||||
msg_ok "Installed Homebrew"
|
||||
msg_ok "Created OpenClaw User"
|
||||
|
||||
# Install common brew dependencies for OpenClaw skills
|
||||
msg_info "Installing Homebrew Dependencies"
|
||||
if command -v brew &>/dev/null || [[ -x /home/linuxbrew/.linuxbrew/bin/brew ]]; then
|
||||
# Use the full path to brew if not in PATH
|
||||
BREW_CMD="${BREW_CMD:-/home/linuxbrew/.linuxbrew/bin/brew}"
|
||||
if [[ ! -x "$BREW_CMD" ]]; then
|
||||
BREW_CMD="brew"
|
||||
fi
|
||||
# Install ffmpeg for video-frames skill
|
||||
$STD $BREW_CMD install ffmpeg 2>/dev/null || true
|
||||
# Note: Other brew packages (camsnap, obsidian, summarize, songsee) require specific taps
|
||||
# These can be installed manually by the user if needed
|
||||
fi
|
||||
msg_ok "Installed Homebrew Dependencies"
|
||||
# Configure npm to use user-writable directory for global packages
|
||||
msg_info "Configuring npm for User Packages"
|
||||
mkdir -p /home/openclaw/.npm-global
|
||||
chown -R openclaw:openclaw /home/openclaw/.npm-global
|
||||
# Set npm prefix for openclaw user to use home directory
|
||||
run_user_cmd="su - openclaw -c"
|
||||
$run_user_cmd "npm config set prefix /home/openclaw/.npm-global"
|
||||
# Add to PATH for openclaw user
|
||||
echo 'export PATH=/home/openclaw/.npm-global/bin:$PATH' >> /home/openclaw/.bashrc
|
||||
msg_ok "Configured npm for User Packages"
|
||||
|
||||
msg_info "Installing OpenClaw"
|
||||
$STD npm install -g openclaw@latest
|
||||
@@ -95,13 +73,17 @@ msg_ok "Installed OpenClaw"
|
||||
|
||||
msg_info "Creating Directories"
|
||||
mkdir -p /opt/openclaw
|
||||
mkdir -p /root/.openclaw
|
||||
mkdir -p /home/openclaw/.openclaw
|
||||
chown -R openclaw:openclaw /opt/openclaw
|
||||
chown -R openclaw:openclaw /home/openclaw/.openclaw
|
||||
msg_ok "Created Directories"
|
||||
|
||||
msg_info "Creating OpenClaw Configuration"
|
||||
# Get the container's IP address for allowedOrigins
|
||||
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
|
||||
cat <<EOF >/root/.openclaw/openclaw.json
|
||||
# Get hostname for HTTPS origins
|
||||
HOSTNAME_FQDN=$(hostname -f 2>/dev/null || hostname)
|
||||
cat <<EOF >/home/openclaw/.openclaw/openclaw.json
|
||||
{
|
||||
"gateway": {
|
||||
"bind": "lan",
|
||||
@@ -110,15 +92,69 @@ cat <<EOF >/root/.openclaw/openclaw.json
|
||||
"allowedOrigins": [
|
||||
"http://localhost:18789",
|
||||
"http://127.0.0.1:18789",
|
||||
"http://${CONTAINER_IP}:18789"
|
||||
"https://localhost:18790",
|
||||
"https://127.0.0.1:18790",
|
||||
"https://${CONTAINER_IP}:18790",
|
||||
"https://${HOSTNAME_FQDN}:18790"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
chown openclaw:openclaw /home/openclaw/.openclaw/openclaw.json
|
||||
msg_ok "Created OpenClaw Configuration"
|
||||
|
||||
msg_info "Creating Service"
|
||||
msg_info "Creating Self-Signed Certificate for HTTPS"
|
||||
# Generate self-signed certificate with IP in SAN (Caddy's tls internal doesn't work with IPs)
|
||||
mkdir -p /etc/caddy/certs
|
||||
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
|
||||
HOSTNAME_FQDN=$(hostname -f 2>/dev/null || hostname)
|
||||
|
||||
# Generate self-signed certificate valid for 365 days
|
||||
openssl req -x509 -newkey rsa:4096 -keyout /etc/caddy/certs/openclaw.key \
|
||||
-out /etc/caddy/certs/openclaw.crt \
|
||||
-days 365 -nodes \
|
||||
-subj "/CN=openclaw-local" \
|
||||
-addext "subjectAltName=IP:127.0.0.1,IP:${CONTAINER_IP},DNS:localhost,DNS:${HOSTNAME_FQDN}" 2>/dev/null
|
||||
|
||||
# Set proper permissions
|
||||
chmod 644 /etc/caddy/certs/openclaw.crt
|
||||
chmod 600 /etc/caddy/certs/openclaw.key
|
||||
chown caddy:caddy /etc/caddy/certs/openclaw.crt /etc/caddy/certs/openclaw.key
|
||||
msg_ok "Created Self-Signed Certificate"
|
||||
|
||||
msg_info "Creating Caddy HTTPS Reverse Proxy"
|
||||
# Create Caddyfile for HTTPS reverse proxy with explicit certificate paths
|
||||
cat <<EOF >/etc/caddy/Caddyfile
|
||||
# OpenClaw HTTPS Reverse Proxy
|
||||
# Access via https://<container-ip>:18790
|
||||
|
||||
:18790 {
|
||||
tls /etc/caddy/certs/openclaw.crt /etc/caddy/certs/openclaw.key
|
||||
|
||||
# Caddy automatically handles WebSocket upgrades
|
||||
reverse_proxy localhost:18789 {
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote_host}
|
||||
}
|
||||
|
||||
# Log requests
|
||||
log {
|
||||
output file /var/log/caddy/openclaw.log
|
||||
format console
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create log directory
|
||||
mkdir -p /var/log/caddy
|
||||
chown caddy:caddy /var/log/caddy
|
||||
|
||||
# Enable Caddy
|
||||
systemctl enable -q caddy
|
||||
msg_ok "Created Caddy HTTPS Reverse Proxy"
|
||||
|
||||
msg_info "Creating OpenClaw Service"
|
||||
cat <<EOF >/etc/systemd/system/openclaw.service
|
||||
[Unit]
|
||||
Description=OpenClaw Gateway - Personal AI Assistant
|
||||
@@ -127,9 +163,11 @@ Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
User=openclaw
|
||||
Group=openclaw
|
||||
WorkingDirectory=/opt/openclaw
|
||||
Environment=NODE_ENV=production
|
||||
Environment=PATH=/home/openclaw/.npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
ExecStart=/usr/bin/openclaw gateway --port 18789 --bind lan
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
@@ -141,7 +179,12 @@ SyslogIdentifier=openclaw
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q --now openclaw
|
||||
msg_ok "Created Service"
|
||||
msg_ok "Created OpenClaw Service"
|
||||
|
||||
msg_info "Starting Caddy HTTPS Proxy"
|
||||
# Restart Caddy to apply configuration
|
||||
systemctl restart caddy
|
||||
msg_ok "Started Caddy HTTPS Proxy"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
# OpenClaw Configuration Guide
|
||||
|
||||
## "Device Identity Required" Error Fix
|
||||
|
||||
If you see this error when accessing the Control UI from another machine:
|
||||
|
||||
```
|
||||
control ui requires device identity (use HTTPS or localhost secure context)
|
||||
```
|
||||
|
||||
**This is a browser security requirement.** The Web Crypto API (used for device identity) only works in secure contexts - either HTTPS or localhost.
|
||||
|
||||
### Solution 1: SSH Tunnel (Recommended for LAN)
|
||||
|
||||
Access OpenClaw via localhost through an SSH tunnel:
|
||||
|
||||
```bash
|
||||
# On your local machine, create an SSH tunnel
|
||||
ssh -L 18789:localhost:18789 root@<container-ip>
|
||||
|
||||
# Then access in your browser
|
||||
http://localhost:18789
|
||||
```
|
||||
|
||||
### Solution 2: HTTPS Reverse Proxy (Production)
|
||||
|
||||
Set up Caddy or Nginx with SSL certificates for secure HTTPS access.
|
||||
|
||||
### Solution 3: Tailscale VPN
|
||||
|
||||
Install Tailscale on both machines and access via Tailscale IP.
|
||||
|
||||
## "Origin Not Allowed" Error Fix
|
||||
|
||||
If you see this error when accessing the Control UI from another machine:
|
||||
|
||||
```
|
||||
origin not allowed (open the Control UI from the gateway host or allow it in gateway.controlUi.allowedOrigins)
|
||||
```
|
||||
|
||||
**Quick Fix:**
|
||||
|
||||
```bash
|
||||
# Get your container IP
|
||||
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
|
||||
|
||||
# Create configuration with allowed origins
|
||||
mkdir -p ~/.openclaw
|
||||
cat > ~/.openclaw/openclaw.json << EOF
|
||||
{
|
||||
"gateway": {
|
||||
"bind": "lan",
|
||||
"port": 18789,
|
||||
"controlUi": {
|
||||
"allowedOrigins": [
|
||||
"http://localhost:18789",
|
||||
"http://127.0.0.1:18789",
|
||||
"http://${CONTAINER_IP}:18789"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Update service to use lan binding
|
||||
sed -i 's|--port 18789|--port 18789 --bind lan|' /etc/systemd/system/openclaw.service
|
||||
|
||||
# Apply and restart
|
||||
systemctl daemon-reload
|
||||
systemctl restart openclaw
|
||||
```
|
||||
|
||||
**Note:** Even with `allowedOrigins` configured, you still need a secure context (HTTPS or localhost) for the device identity requirement. Use SSH tunneling for LAN access.
|
||||
|
||||
## Network Binding Configuration
|
||||
|
||||
By default, OpenClaw binds to localhost (127.0.0.1) only. To make it accessible from other machines on your network, you need to:
|
||||
|
||||
1. **Bind to all interfaces** (`--bind lan`)
|
||||
2. **Configure allowed origins** (required for non-loopback bindings)
|
||||
|
||||
### Bind Options
|
||||
|
||||
| Option | Description | Use Case |
|
||||
| ---------- | --------------------------------- | -------------------- |
|
||||
| `loopback` | Binds to 127.0.0.1 only | Default, most secure |
|
||||
| `lan` | Binds to 0.0.0.0 (all interfaces) | Local network access |
|
||||
| `tailnet` | Binds to Tailscale interface | VPN access only |
|
||||
| `auto` | Automatic selection | Let OpenClaw decide |
|
||||
|
||||
### Method 1: Configure Command (Recommended for Existing Installations)
|
||||
|
||||
```bash
|
||||
# Open the gateway configuration
|
||||
openclaw configure --section gateway
|
||||
|
||||
# Set bind to "lan" when prompted
|
||||
# Then restart the service
|
||||
systemctl restart openclaw
|
||||
```
|
||||
|
||||
### Method 2: Manual Configuration
|
||||
|
||||
Edit both the configuration file and systemd service:
|
||||
|
||||
```bash
|
||||
# Create/edit the configuration file
|
||||
nano ~/.openclaw/openclaw.json
|
||||
```
|
||||
|
||||
Add the configuration with your IP:
|
||||
|
||||
```json
|
||||
{
|
||||
"gateway": {
|
||||
"bind": "lan",
|
||||
"port": 18789,
|
||||
"controlUi": {
|
||||
"allowedOrigins": ["http://localhost:18789", "http://127.0.0.1:18789", "http://192.168.31.39:18789"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then edit the systemd service:
|
||||
|
||||
```bash
|
||||
nano /etc/systemd/system/openclaw.service
|
||||
```
|
||||
|
||||
Modify the `ExecStart` line to include `--bind lan`:
|
||||
|
||||
```ini
|
||||
[Service]
|
||||
ExecStart=/usr/bin/openclaw gateway --port 18789 --bind lan
|
||||
```
|
||||
|
||||
Apply changes:
|
||||
|
||||
```bash
|
||||
systemctl daemon-reload
|
||||
systemctl restart openclaw
|
||||
```
|
||||
|
||||
### Method 3: Quick Fix for Existing Installations
|
||||
|
||||
For existing installations, run these commands:
|
||||
|
||||
```bash
|
||||
# Get your container IP
|
||||
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
|
||||
|
||||
# Create configuration with allowed origins
|
||||
mkdir -p ~/.openclaw
|
||||
cat > ~/.openclaw/openclaw.json << EOF
|
||||
{
|
||||
"gateway": {
|
||||
"bind": "lan",
|
||||
"port": 18789,
|
||||
"controlUi": {
|
||||
"allowedOrigins": [
|
||||
"http://localhost:18789",
|
||||
"http://127.0.0.1:18789",
|
||||
"http://${CONTAINER_IP}:18789"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Update service to use lan binding
|
||||
sed -i 's|--port 18789|--port 18789 --bind lan|' /etc/systemd/system/openclaw.service
|
||||
|
||||
# Apply and restart
|
||||
systemctl daemon-reload
|
||||
systemctl restart openclaw
|
||||
```
|
||||
|
||||
### Method 4: Environment Variable
|
||||
|
||||
You can also set the bind mode via environment variable:
|
||||
|
||||
```bash
|
||||
# Add to /etc/environment or the systemd service file
|
||||
OPENCLAW_BIND=lan
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
Check the current binding:
|
||||
|
||||
```bash
|
||||
openclaw gateway status
|
||||
```
|
||||
|
||||
Output should show:
|
||||
|
||||
```
|
||||
bind: lan
|
||||
listener: 0.0.0.0:18789
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
When binding to all interfaces (`lan`):
|
||||
|
||||
1. **Token Authentication**: OpenClaw uses token-based authentication. The token is generated on first run and displayed in the startup output.
|
||||
|
||||
2. **Firewall**: Consider adding firewall rules to restrict access:
|
||||
|
||||
```bash
|
||||
# Allow only specific IP ranges
|
||||
ufw allow from 192.168.31.0/24 to any port 18789
|
||||
```
|
||||
|
||||
3. **Reverse Proxy**: For production use, consider placing OpenClaw behind a reverse proxy (nginx, Caddy) with SSL:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name openclaw.yourdomain.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:18789;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **Never expose directly to the internet**: The gateway provides administrative access to your system. Always use VPN, reverse proxy with authentication, or Tailscale for remote access.
|
||||
|
||||
## Remote Access Options
|
||||
|
||||
### SSH Tunnel (Most Secure for Temporary Access)
|
||||
|
||||
```bash
|
||||
# On your local machine
|
||||
ssh -N -L 18789:127.0.0.1:18789 root@<server-ip>
|
||||
|
||||
# Then access in browser
|
||||
http://localhost:18789
|
||||
```
|
||||
|
||||
### Tailscale (Recommended for Permanent Remote Access)
|
||||
|
||||
1. Install Tailscale on both machines
|
||||
2. Set OpenClaw bind to `tailnet`:
|
||||
|
||||
```bash
|
||||
openclaw configure --section gateway
|
||||
# Set bind to "tailnet"
|
||||
systemctl restart openclaw
|
||||
```
|
||||
|
||||
3. Access via Tailscale IP: `http://<tailscale-ip>:18789`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Gateway Not Starting
|
||||
|
||||
```bash
|
||||
# Check logs
|
||||
journalctl -u openclaw -f
|
||||
|
||||
# Common issues:
|
||||
# - Port already in use: lsof -i :18789
|
||||
# - Permission denied: ensure running as root or correct user
|
||||
```
|
||||
|
||||
### Cannot Connect from Remote
|
||||
|
||||
1. Verify binding: `openclaw gateway status` should show `0.0.0.0:18789`
|
||||
2. Check firewall: `ufw status` or `iptables -L -n`
|
||||
3. Verify network connectivity: `ping <server-ip>`
|
||||
|
||||
### Token Authentication Issues
|
||||
|
||||
```bash
|
||||
# Regenerate token
|
||||
openclaw doctor --generate-gateway-token
|
||||
|
||||
# View current token
|
||||
openclaw gateway status
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [OpenClaw Gateway Documentation](https://docs.openclaw.ai/gateway)
|
||||
- [OpenClaw Security Guide](https://docs.openclaw.ai/gateway/security)
|
||||
- [OpenClaw Troubleshooting](https://docs.openclaw.ai/gateway/troubleshooting)
|
||||
+3
-2
@@ -65,5 +65,6 @@ 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}Website: http://${IP}:5099${CL}"
|
||||
echo -e "${TAB}${GATEWAY}${BGN}Dashboard: http://${IP}:5099/yao/auth/entry${CL}"
|
||||
echo -e "${TAB}${GATEWAY}${BGN}API: http://${IP}:5099/api${CL}"
|
||||
echo -e "${TAB}${GATEWAY}${BGN}Dashboard: http://${IP}:5099/dashboard/auth/entry${CL}"
|
||||
echo -e "${TAB}${GATEWAY}${BGN}OpenAPI: http://${IP}:5099/v1${CL}"
|
||||
echo -e "${TAB}${GATEWAY}${BGN}Custom API: http://${IP}:5099/v1/api${CL}"
|
||||
|
||||
+2
-2
@@ -26,8 +26,8 @@
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "admin@localhost",
|
||||
"password": "admin123"
|
||||
"username": "[email protected]",
|
||||
"password": "Yao123++"
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
|
||||
+86
-2
@@ -19,12 +19,76 @@ $STD apt-get install -y \
|
||||
ca-certificates \
|
||||
build-essential \
|
||||
python3 \
|
||||
git
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
git \
|
||||
procps
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
# Setup Node.js 22 (required by OpenClaw)
|
||||
NODE_VERSION="22" setup_nodejs
|
||||
|
||||
# Install uv (Python package manager)
|
||||
msg_info "Installing uv (Python Package Manager)"
|
||||
$STD curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
msg_ok "Installed uv"
|
||||
|
||||
# Install Homebrew (Linuxbrew) - use alternative installation for root
|
||||
msg_info "Installing Homebrew (Linuxbrew)"
|
||||
# Homebrew refuses to install as root, so we use the alternative untar method
|
||||
# This installs to /home/linuxbrew/.linuxbrew which is the recommended location
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
# Create the linuxbrew directory with proper permissions
|
||||
mkdir -p /home/linuxbrew/.linuxbrew
|
||||
chmod 775 /home/linuxbrew
|
||||
chmod 775 /home/linuxbrew/.linuxbrew
|
||||
|
||||
# Download and extract Homebrew (alternative installation method)
|
||||
cd /tmp
|
||||
$STD curl -fsSL https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C /home/linuxbrew/.linuxbrew
|
||||
|
||||
# Set permissions so brew can be run by anyone
|
||||
chown -R root:root /home/linuxbrew/.linuxbrew
|
||||
chmod -R g+w /home/linuxbrew/.linuxbrew
|
||||
|
||||
# Create symlinks for easy access
|
||||
ln -sf /home/linuxbrew/.linuxbrew/bin/brew /usr/local/bin/brew 2>/dev/null || true
|
||||
|
||||
# Add to bashrc for persistence
|
||||
if ! grep -q 'linuxbrew/.linuxbrew/bin/brew' /root/.bashrc 2>/dev/null; then
|
||||
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >>/root/.bashrc
|
||||
fi
|
||||
|
||||
# Add to system profile for all users
|
||||
if ! grep -q 'linuxbrew/.linuxbrew' /etc/profile 2>/dev/null; then
|
||||
echo 'export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH"' >>/etc/profile
|
||||
fi
|
||||
|
||||
# Make brew available in current session
|
||||
export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH"
|
||||
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv 2>/dev/null)" || true
|
||||
else
|
||||
# Not running as root, install normally
|
||||
$STD /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
fi
|
||||
msg_ok "Installed Homebrew"
|
||||
|
||||
# Install common brew dependencies for OpenClaw skills
|
||||
msg_info "Installing Homebrew Dependencies"
|
||||
if command -v brew &>/dev/null || [[ -x /home/linuxbrew/.linuxbrew/bin/brew ]]; then
|
||||
# Use the full path to brew if not in PATH
|
||||
BREW_CMD="${BREW_CMD:-/home/linuxbrew/.linuxbrew/bin/brew}"
|
||||
if [[ ! -x "$BREW_CMD" ]]; then
|
||||
BREW_CMD="brew"
|
||||
fi
|
||||
# Install ffmpeg for video-frames skill
|
||||
$STD $BREW_CMD install ffmpeg 2>/dev/null || true
|
||||
# Note: Other brew packages (camsnap, obsidian, summarize, songsee) require specific taps
|
||||
# These can be installed manually by the user if needed
|
||||
fi
|
||||
msg_ok "Installed Homebrew Dependencies"
|
||||
|
||||
msg_info "Installing OpenClaw"
|
||||
$STD npm install -g openclaw@latest
|
||||
msg_ok "Installed OpenClaw"
|
||||
@@ -34,6 +98,26 @@ mkdir -p /opt/openclaw
|
||||
mkdir -p /root/.openclaw
|
||||
msg_ok "Created Directories"
|
||||
|
||||
msg_info "Creating OpenClaw Configuration"
|
||||
# Get the container's IP address for allowedOrigins
|
||||
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
|
||||
cat <<EOF >/root/.openclaw/openclaw.json
|
||||
{
|
||||
"gateway": {
|
||||
"bind": "lan",
|
||||
"port": 18789,
|
||||
"controlUi": {
|
||||
"allowedOrigins": [
|
||||
"http://localhost:18789",
|
||||
"http://127.0.0.1:18789",
|
||||
"http://${CONTAINER_IP}:18789"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
msg_ok "Created OpenClaw Configuration"
|
||||
|
||||
msg_info "Creating Service"
|
||||
cat <<EOF >/etc/systemd/system/openclaw.service
|
||||
[Unit]
|
||||
@@ -46,7 +130,7 @@ Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/openclaw
|
||||
Environment=NODE_ENV=production
|
||||
ExecStart=/usr/bin/openclaw gateway --port 18789
|
||||
ExecStart=/usr/bin/openclaw gateway --port 18789 --bind lan
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
|
||||
+274
-186
@@ -22,41 +22,67 @@ msg_ok "Installed Dependencies"
|
||||
fetch_and_deploy_gh_release "yao" "YaoApp/yao" "singlefile" "latest" "/usr/local/bin" "yao-*-linux-*"
|
||||
|
||||
msg_info "Creating Application Directory"
|
||||
mkdir -p /opt/yao/data
|
||||
mkdir -p /opt/yao/etc
|
||||
mkdir -p /opt/yao/connectors
|
||||
mkdir -p /opt/yao/scripts
|
||||
mkdir -p /opt/yao/suis
|
||||
mkdir -p /opt/yao/agent
|
||||
mkdir -p /opt/yao/openapi
|
||||
mkdir -p /opt/yao/services
|
||||
mkdir -p /opt/yao/public
|
||||
mkdir -p /opt/yao/icons
|
||||
mkdir -p /opt/yao/public/.well-known
|
||||
mkdir -p /opt/yao/logins
|
||||
mkdir -p /opt/yao/models/admin
|
||||
mkdir -p /opt/yao/flows/app
|
||||
rm -rf /opt/yao
|
||||
mkdir -p /opt/yao
|
||||
msg_ok "Created Application Directory"
|
||||
|
||||
msg_info "Initializing Yao Application"
|
||||
cd /opt/yao
|
||||
$STD yao init
|
||||
msg_ok "Initialized Yao Application"
|
||||
|
||||
# Create additional directories after init
|
||||
mkdir -p /opt/yao/{db,logins,models,flows,scripts,public,logs,icons}
|
||||
|
||||
# Generate secure secrets
|
||||
YAO_CLIENT_ID=$(cat /proc/sys/kernel/random/uuid | tr -d '-')
|
||||
YAO_JWT_SECRET=$(openssl rand -base64 32 | tr -d '/+=' | head -c 32)
|
||||
YAO_AES_KEY=$(openssl rand -base64 24 | tr -d '/+=' | head -c 24)
|
||||
|
||||
msg_info "Creating Environment File"
|
||||
cat <<EOF >/opt/yao/.env
|
||||
# ============================================
|
||||
# Yao Application Configuration
|
||||
# ============================================
|
||||
|
||||
# Database Configuration
|
||||
YAO_DB_DRIVER="sqlite3"
|
||||
YAO_DB_PRIMARY="./db/yao.db"
|
||||
|
||||
# Security
|
||||
YAO_JWT_SECRET="${YAO_JWT_SECRET}"
|
||||
YAO_DB_AESKEY="${YAO_AES_KEY}"
|
||||
|
||||
# OAuth/OpenAPI
|
||||
YAO_CLIENT_ID="${YAO_CLIENT_ID}"
|
||||
|
||||
# Login Redirects
|
||||
AFTER_LOGIN_SUCCESS_URL="/mission-control"
|
||||
AFTER_LOGIN_FAILURE_URL="/dashboard/auth/entry"
|
||||
|
||||
# Server Configuration
|
||||
YAO_ENV="production"
|
||||
YAO_HOST="0.0.0.0"
|
||||
YAO_PORT=5099
|
||||
YAO_STUDIO_PORT=5077
|
||||
YAO_LOG="./logs/application.log"
|
||||
YAO_LOG_MODE="TEXT"
|
||||
YAO_SESSION_FILE="./db/.session"
|
||||
YAO_SESSION_STORE="file"
|
||||
EOF
|
||||
msg_ok "Created Environment File"
|
||||
|
||||
msg_info "Creating Application Configuration"
|
||||
cat <<EOF >/opt/yao/app.yao
|
||||
cat <<'EOF' >/opt/yao/app.yao
|
||||
{
|
||||
"xgen": "1.0",
|
||||
"name": "Yao Application",
|
||||
"short": "Yao",
|
||||
"description": "Yao Autonomous Agent Engine",
|
||||
"description": "Autonomous Agent Engine",
|
||||
"version": "1.0.0",
|
||||
"adminRoot": "admin",
|
||||
"adminRoot": "dashboard",
|
||||
"setup": "scripts.setup.Init",
|
||||
"menu": {
|
||||
"process": "flows.app.menu",
|
||||
"args": ["yao"]
|
||||
"process": "scripts.menu.Get",
|
||||
"args": []
|
||||
},
|
||||
"optional": {
|
||||
"hideNotification": true,
|
||||
@@ -64,129 +90,39 @@ cat <<EOF >/opt/yao/app.yao
|
||||
}
|
||||
}
|
||||
EOF
|
||||
msg_info "Creating Database Directory"
|
||||
mkdir -p /opt/yao/db
|
||||
msg_info "Creating Empty SQLite Database"
|
||||
sqlite3 /opt/yao/db/yao.db "VACUUM;" 2>/dev/null || touch /opt/yao/db/yao.db
|
||||
msg_ok "Created Application Configuration"
|
||||
|
||||
msg_info "Creating Agent Configuration"
|
||||
cat <<EOF >/opt/yao/agent/agent.yml
|
||||
# Yao Agent Configuration
|
||||
# This is a minimal configuration file for the agent system
|
||||
version: "1.0"
|
||||
agents: []
|
||||
EOF
|
||||
msg_ok "Created Agent Configuration"
|
||||
|
||||
msg_info "Creating OpenAPI Configuration"
|
||||
cat <<EOF >/opt/yao/openapi/openapi.yao
|
||||
// OpenAPI Configuration
|
||||
// This is a minimal configuration file for OpenAPI support
|
||||
msg_info "Creating Admin Login Widget"
|
||||
cat <<'EOF' >/opt/yao/logins/admin.login.json
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "Yao API",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"paths": {}
|
||||
}
|
||||
EOF
|
||||
msg_ok "Created OpenAPI Configuration"
|
||||
|
||||
msg_info "Downloading Application Icons"
|
||||
curl -fsSL "https://raw.githubusercontent.com/YaoApp/yao-dev-app/main/icons/app.ico" -o /opt/yao/icons/app.ico
|
||||
curl -fsSL "https://raw.githubusercontent.com/YaoApp/yao-dev-app/main/icons/app.png" -o /opt/yao/icons/app.png
|
||||
msg_ok "Downloaded Application Icons"
|
||||
|
||||
msg_info "Creating Well-Known Configuration"
|
||||
cat <<EOF >/opt/yao/public/.well-known/yao
|
||||
{
|
||||
"name": "yao-app",
|
||||
"version": "1.0.0",
|
||||
"description": "Yao Autonomous Agent Engine"
|
||||
}
|
||||
EOF
|
||||
msg_ok "Created Well-Known Configuration"
|
||||
|
||||
msg_info "Creating Login Configuration"
|
||||
cat <<EOF >/opt/yao/logins/admin.login.yao
|
||||
{
|
||||
"name": "Admin Login",
|
||||
"name": "::Admin Login",
|
||||
"action": {
|
||||
"process": "yao.login.Admin",
|
||||
"args": [":payload"]
|
||||
},
|
||||
"layout": {
|
||||
"entry": "/x/Chart/dashboard",
|
||||
"entry": "/mission-control",
|
||||
"captcha": "yao.utils.Captcha",
|
||||
"cover": "/assets/images/login/cover.svg",
|
||||
"slogan": "Yao Autonomous Agent Engine",
|
||||
"slogan": "::Build Autonomous Agents with Yao",
|
||||
"site": "https://yaoapps.com"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
cat <<EOF >/opt/yao/logins/user.login.yao
|
||||
{
|
||||
"name": "User Login",
|
||||
"action": {
|
||||
"process": "scripts.user.Login",
|
||||
"args": [":payload"]
|
||||
},
|
||||
"layout": {
|
||||
"entry": "/x/Table/pet",
|
||||
"captcha": "yao.utils.Captcha",
|
||||
"cover": "/assets/images/login/cover.svg",
|
||||
"slogan": "Yao Autonomous Agent Engine",
|
||||
"site": "https://yaoapps.com/doc"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
msg_ok "Created Login Configuration"
|
||||
msg_ok "Created Admin Login Widget"
|
||||
|
||||
msg_info "Creating Admin User Model"
|
||||
cat <<EOF >/opt/yao/models/admin/user.mod.yao
|
||||
cat <<'EOF' >/opt/yao/models/admin.mod.json
|
||||
{
|
||||
"name": "AdminUser",
|
||||
"table": { "name": "admin_user", "comment": "The administrator table" },
|
||||
"name": "Admin User",
|
||||
"table": {
|
||||
"name": "admin_user",
|
||||
"comment": "Admin User"
|
||||
},
|
||||
"columns": [
|
||||
{ "label": "ID", "name": "id", "type": "ID" },
|
||||
{
|
||||
"label": "Type",
|
||||
"name": "type",
|
||||
"type": "enum",
|
||||
"option": ["admin", "staff", "user", "robot"],
|
||||
"comment": "AccountTypes: admin, staff, user, robot",
|
||||
"default": "user",
|
||||
"index": true
|
||||
},
|
||||
{
|
||||
"label": "Email",
|
||||
"name": "email",
|
||||
"type": "string",
|
||||
"length": 50,
|
||||
"comment": "Email",
|
||||
"index": true,
|
||||
"nullable": true
|
||||
},
|
||||
{
|
||||
"label": "Mobile",
|
||||
"name": "mobile",
|
||||
"type": "string",
|
||||
"length": 50,
|
||||
"comment": "Mobile",
|
||||
"index": true,
|
||||
"nullable": true
|
||||
},
|
||||
{
|
||||
"label": "Login Password",
|
||||
"name": "password",
|
||||
"type": "string",
|
||||
"length": 256,
|
||||
"comment": "Login Password",
|
||||
"crypt": "PASSWORD",
|
||||
"index": true,
|
||||
"nullable": true
|
||||
"label": "ID",
|
||||
"name": "id",
|
||||
"type": "ID"
|
||||
},
|
||||
{
|
||||
"label": "Name",
|
||||
@@ -197,87 +133,212 @@ cat <<EOF >/opt/yao/models/admin/user.mod.yao
|
||||
"index": true,
|
||||
"nullable": true
|
||||
},
|
||||
{
|
||||
"label": "Email",
|
||||
"name": "email",
|
||||
"type": "string",
|
||||
"length": 128,
|
||||
"comment": "Email",
|
||||
"unique": true,
|
||||
"index": true,
|
||||
"nullable": true,
|
||||
"validations": [
|
||||
{
|
||||
"method": "email",
|
||||
"args": [],
|
||||
"message": "{{input}} should be email"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Password",
|
||||
"name": "password",
|
||||
"type": "string",
|
||||
"length": 256,
|
||||
"comment": "Password",
|
||||
"crypt": "PASSWORD",
|
||||
"index": true,
|
||||
"nullable": true,
|
||||
"validations": [
|
||||
{
|
||||
"method": "typeof",
|
||||
"args": ["string"],
|
||||
"message": "{{input}} Error"
|
||||
},
|
||||
{
|
||||
"method": "minLength",
|
||||
"args": [6],
|
||||
"message": "{{label}} must be at least 6 characters"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Type",
|
||||
"name": "type",
|
||||
"type": "string",
|
||||
"length": 20,
|
||||
"comment": "User Type",
|
||||
"default": "user",
|
||||
"index": true
|
||||
},
|
||||
{
|
||||
"label": "Status",
|
||||
"comment": "Status",
|
||||
"name": "status",
|
||||
"type": "enum",
|
||||
"default": "enabled",
|
||||
"option": ["enabled", "disabled"],
|
||||
"index": true
|
||||
},
|
||||
{
|
||||
"label": "Extra",
|
||||
"name": "extra",
|
||||
"type": "json",
|
||||
"comment": "Extra",
|
||||
"nullable": true
|
||||
}
|
||||
],
|
||||
"relations": {},
|
||||
"values": [
|
||||
{
|
||||
"name": "Admin",
|
||||
"type": "admin",
|
||||
"email": "admin@localhost",
|
||||
"password": "admin123",
|
||||
"status": "enabled"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"comment": "Email Unique Index",
|
||||
"name": "type_email_unique",
|
||||
"columns": ["type", "email"],
|
||||
"type": "unique"
|
||||
}
|
||||
],
|
||||
"option": { "timestamps": true, "soft_deletes": true }
|
||||
"values": [],
|
||||
"indexes": [],
|
||||
"option": {
|
||||
"timestamps": true,
|
||||
"soft_deletes": true
|
||||
}
|
||||
}
|
||||
EOF
|
||||
msg_ok "Created Admin User Model"
|
||||
|
||||
msg_info "Creating User Login Script"
|
||||
cat <<EOF >/opt/yao/scripts/user.js
|
||||
/**
|
||||
* User Login
|
||||
* @param {*} payload
|
||||
*/
|
||||
function Login(payload) {
|
||||
log.Trace("[user] Login %s", payload.email);
|
||||
return Process("yao.login.Admin", payload);
|
||||
}
|
||||
EOF
|
||||
msg_ok "Created User Login Script"
|
||||
|
||||
msg_info "Creating Menu Flow"
|
||||
cat <<EOF >/opt/yao/flows/app/menu.flow.yao
|
||||
cat <<'EOF' >/opt/yao/flows/menu.flow.yao
|
||||
{
|
||||
"label": "Application Menu",
|
||||
"version": "1.0.0",
|
||||
"description": "Application Menu Flow",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "menu",
|
||||
"type": "menu",
|
||||
"config": {
|
||||
"items": [
|
||||
{
|
||||
"id": "dashboard",
|
||||
"title": "Dashboard",
|
||||
"icon": "dashboard",
|
||||
"path": "/x/Chart/dashboard"
|
||||
},
|
||||
{
|
||||
"id": "admin",
|
||||
"title": "Admin",
|
||||
"icon": "admin",
|
||||
"path": "/x/Table/admin_user"
|
||||
}
|
||||
]
|
||||
"name": "Menu",
|
||||
"nodes": [],
|
||||
"output": {
|
||||
"items": [
|
||||
{
|
||||
"name": "Dashboard",
|
||||
"path": "/mission-control",
|
||||
"icon": { "name": "material-dashboard", "size": 22 }
|
||||
},
|
||||
{
|
||||
"name": "Agents",
|
||||
"path": "/agents",
|
||||
"icon": { "name": "material-smart_toy", "size": 22 }
|
||||
},
|
||||
{
|
||||
"name": "Data",
|
||||
"path": "/data",
|
||||
"icon": { "name": "material-storage", "size": 22 }
|
||||
},
|
||||
{
|
||||
"name": "Settings",
|
||||
"path": "/settings",
|
||||
"icon": { "name": "material-settings", "size": 22 }
|
||||
}
|
||||
}
|
||||
],
|
||||
"output": "menu"
|
||||
],
|
||||
"setting": [
|
||||
{
|
||||
"icon": { "name": "material-person", "size": 22 },
|
||||
"name": "Account",
|
||||
"path": "/account"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
EOF
|
||||
msg_ok "Created Menu Flow"
|
||||
|
||||
msg_info "Creating Setup Script"
|
||||
cat <<'EOF' >/opt/yao/scripts/setup.ts
|
||||
import { Process, Model } from "@yao/runtime";
|
||||
|
||||
/**
|
||||
* Initialize the application
|
||||
* Creates the default admin user if not exists
|
||||
*/
|
||||
export function Init() {
|
||||
// Create default admin user
|
||||
const adminData = {
|
||||
name: "Admin",
|
||||
email: "[email protected]",
|
||||
password: "Yao123++",
|
||||
type: "admin",
|
||||
status: "enabled",
|
||||
};
|
||||
|
||||
try {
|
||||
// Check if admin user already exists
|
||||
const existingAdmin = Process("models.admin.user.Find", 1, {
|
||||
select: ["id"],
|
||||
});
|
||||
|
||||
if (!existingAdmin || !existingAdmin.id) {
|
||||
// Create admin user
|
||||
Process("models.admin.user.Save", adminData);
|
||||
console.log("Default admin user created successfully");
|
||||
} else {
|
||||
console.log("Admin user already exists");
|
||||
}
|
||||
} catch (error) {
|
||||
// If the model doesn't exist yet, try to create using direct insert
|
||||
console.log("Creating admin user...");
|
||||
try {
|
||||
Process("models.admin.user.Save", adminData);
|
||||
console.log("Default admin user created successfully");
|
||||
} catch (e) {
|
||||
console.log("Note: Admin user will be created on first login");
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
msg_ok "Created Setup Script"
|
||||
|
||||
msg_info "Creating Menu Script"
|
||||
cat <<'EOF' >/opt/yao/scripts/menu.ts
|
||||
import { Process } from "@yao/runtime";
|
||||
|
||||
/**
|
||||
* Get the menu for the admin panel
|
||||
* @returns Menu structure
|
||||
*/
|
||||
export function Get() {
|
||||
return {
|
||||
items: [
|
||||
{
|
||||
name: "Dashboard",
|
||||
path: "/mission-control",
|
||||
icon: { name: "material-dashboard", size: 22 },
|
||||
},
|
||||
{
|
||||
name: "Agents",
|
||||
path: "/agents",
|
||||
icon: { name: "material-smart_toy", size: 22 },
|
||||
},
|
||||
{
|
||||
name: "Data",
|
||||
path: "/data",
|
||||
icon: { name: "material-storage", size: 22 },
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
path: "/settings",
|
||||
icon: { name: "material-settings", size: 22 },
|
||||
},
|
||||
],
|
||||
setting: [
|
||||
{
|
||||
icon: { name: "material-person", size: 22 },
|
||||
name: "Account",
|
||||
path: "/account",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
EOF
|
||||
msg_ok "Created Menu Script"
|
||||
|
||||
msg_info "Creating Public Web Interface"
|
||||
cat <<EOF >/opt/yao/public/index.html
|
||||
cat <<'EOF' >/opt/yao/public/index.html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -356,6 +417,22 @@ cat <<EOF >/opt/yao/public/index.html
|
||||
background: #00d4ff;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
.admin-link {
|
||||
margin-top: 20px;
|
||||
padding: 15px 30px;
|
||||
background: linear-gradient(90deg, #7c3aed, #00d4ff);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
.admin-link:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -368,8 +445,9 @@ cat <<EOF >/opt/yao/public/index.html
|
||||
</div>
|
||||
<div class="info">
|
||||
<p><strong>Port:</strong> 5099</p>
|
||||
<p><strong>Studio Port:</strong> 5077</p>
|
||||
<p><strong>Default Login:</strong> [email protected] / Yao123++</p>
|
||||
</div>
|
||||
<a href="/dashboard/auth/entry" class="admin-link">Open Dashboard</a>
|
||||
<div class="links">
|
||||
<a href="https://github.com/YaoApp/yao" target="_blank">GitHub</a>
|
||||
<a href="https://yaoapps.com" target="_blank">Documentation</a>
|
||||
@@ -380,6 +458,16 @@ cat <<EOF >/opt/yao/public/index.html
|
||||
EOF
|
||||
msg_ok "Created Public Web Interface"
|
||||
|
||||
msg_info "Running Database Migration"
|
||||
cd /opt/yao
|
||||
$STD yao migrate
|
||||
msg_ok "Database Migration Complete"
|
||||
|
||||
msg_info "Creating Default Admin User"
|
||||
cd /opt/yao
|
||||
$STD yao run scripts.setup.Init || true
|
||||
msg_ok "Created Default Admin User"
|
||||
|
||||
msg_info "Creating Service"
|
||||
cat <<EOF >/etc/systemd/system/yao.service
|
||||
[Unit]
|
||||
|
||||
+3
-1
@@ -64,4 +64,6 @@ 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}:5099${CL}"
|
||||
echo -e "${TAB}${GATEWAY}${BGN}Website: http://${IP}:5099${CL}"
|
||||
echo -e "${TAB}${GATEWAY}${BGN}Dashboard: http://${IP}:5099/yao/auth/entry${CL}"
|
||||
echo -e "${TAB}${GATEWAY}${BGN}API: http://${IP}:5099/api${CL}"
|
||||
|
||||
+2
-2
@@ -26,8 +26,8 @@
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
"username": "admin@localhost",
|
||||
"password": "admin123"
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
|
||||
+341
-19
@@ -15,7 +15,8 @@ update_os
|
||||
msg_info "Installing Dependencies"
|
||||
$STD apt-get install -y \
|
||||
curl \
|
||||
unzip
|
||||
unzip \
|
||||
sqlite3
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
fetch_and_deploy_gh_release "yao" "YaoApp/yao" "singlefile" "latest" "/usr/local/bin" "yao-*-linux-*"
|
||||
@@ -23,6 +24,18 @@ fetch_and_deploy_gh_release "yao" "YaoApp/yao" "singlefile" "latest" "/usr/local
|
||||
msg_info "Creating Application Directory"
|
||||
mkdir -p /opt/yao/data
|
||||
mkdir -p /opt/yao/etc
|
||||
mkdir -p /opt/yao/connectors
|
||||
mkdir -p /opt/yao/scripts
|
||||
mkdir -p /opt/yao/suis
|
||||
mkdir -p /opt/yao/agent
|
||||
mkdir -p /opt/yao/openapi
|
||||
mkdir -p /opt/yao/services
|
||||
mkdir -p /opt/yao/public
|
||||
mkdir -p /opt/yao/icons
|
||||
mkdir -p /opt/yao/public/.well-known
|
||||
mkdir -p /opt/yao/logins
|
||||
mkdir -p /opt/yao/models/admin
|
||||
mkdir -p /opt/yao/flows/app
|
||||
msg_ok "Created Application Directory"
|
||||
|
||||
msg_info "Creating Environment File"
|
||||
@@ -32,31 +45,340 @@ YAO_STUDIO_PORT=5077
|
||||
EOF
|
||||
msg_ok "Created Environment File"
|
||||
|
||||
msg_info "Creating Minimal Application Configuration"
|
||||
msg_info "Creating Application Configuration"
|
||||
cat <<EOF >/opt/yao/app.yao
|
||||
{
|
||||
"name": "yao-app",
|
||||
"version": "1.0.0",
|
||||
"xgen": "1.0",
|
||||
"name": "Yao Application",
|
||||
"short": "Yao",
|
||||
"description": "Yao Autonomous Agent Engine",
|
||||
"studio": {
|
||||
"port": 5077
|
||||
"version": "1.0.0",
|
||||
"adminRoot": "admin",
|
||||
"menu": {
|
||||
"process": "flows.app.menu",
|
||||
"args": ["yao"]
|
||||
},
|
||||
"server": {
|
||||
"port": 5099,
|
||||
"host": "0.0.0.0"
|
||||
},
|
||||
"database": {
|
||||
"driver": "sqlite3",
|
||||
"file": "/opt/yao/data/yao.db"
|
||||
},
|
||||
"session": {
|
||||
"store": "file",
|
||||
"path": "/opt/yao/data/sessions"
|
||||
"optional": {
|
||||
"hideNotification": true,
|
||||
"hideSetting": false
|
||||
}
|
||||
}
|
||||
EOF
|
||||
mkdir -p /opt/yao/data/sessions
|
||||
msg_ok "Created Minimal Application Configuration"
|
||||
msg_info "Creating Database Directory"
|
||||
mkdir -p /opt/yao/db
|
||||
msg_info "Creating Empty SQLite Database"
|
||||
sqlite3 /opt/yao/db/yao.db "VACUUM;" 2>/dev/null || touch /opt/yao/db/yao.db
|
||||
msg_ok "Created Application Configuration"
|
||||
|
||||
msg_info "Creating Agent Configuration"
|
||||
cat <<EOF >/opt/yao/agent/agent.yml
|
||||
# Yao Agent Configuration
|
||||
# This is a minimal configuration file for the agent system
|
||||
version: "1.0"
|
||||
agents: []
|
||||
EOF
|
||||
msg_ok "Created Agent Configuration"
|
||||
|
||||
msg_info "Creating OpenAPI Configuration"
|
||||
cat <<EOF >/opt/yao/openapi/openapi.yao
|
||||
// OpenAPI Configuration
|
||||
// This is a minimal configuration file for OpenAPI support
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "Yao API",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"paths": {}
|
||||
}
|
||||
EOF
|
||||
msg_ok "Created OpenAPI Configuration"
|
||||
|
||||
msg_info "Downloading Application Icons"
|
||||
curl -fsSL "https://raw.githubusercontent.com/YaoApp/yao-dev-app/main/icons/app.ico" -o /opt/yao/icons/app.ico
|
||||
curl -fsSL "https://raw.githubusercontent.com/YaoApp/yao-dev-app/main/icons/app.png" -o /opt/yao/icons/app.png
|
||||
msg_ok "Downloaded Application Icons"
|
||||
|
||||
msg_info "Creating Well-Known Configuration"
|
||||
cat <<EOF >/opt/yao/public/.well-known/yao
|
||||
{
|
||||
"name": "yao-app",
|
||||
"version": "1.0.0",
|
||||
"description": "Yao Autonomous Agent Engine"
|
||||
}
|
||||
EOF
|
||||
msg_ok "Created Well-Known Configuration"
|
||||
|
||||
msg_info "Creating Login Configuration"
|
||||
cat <<EOF >/opt/yao/logins/admin.login.yao
|
||||
{
|
||||
"name": "Admin Login",
|
||||
"action": {
|
||||
"process": "yao.login.Admin",
|
||||
"args": [":payload"]
|
||||
},
|
||||
"layout": {
|
||||
"entry": "/x/Chart/dashboard",
|
||||
"captcha": "yao.utils.Captcha",
|
||||
"cover": "/assets/images/login/cover.svg",
|
||||
"slogan": "Yao Autonomous Agent Engine",
|
||||
"site": "https://yaoapps.com"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
cat <<EOF >/opt/yao/logins/user.login.yao
|
||||
{
|
||||
"name": "User Login",
|
||||
"action": {
|
||||
"process": "scripts.user.Login",
|
||||
"args": [":payload"]
|
||||
},
|
||||
"layout": {
|
||||
"entry": "/x/Table/pet",
|
||||
"captcha": "yao.utils.Captcha",
|
||||
"cover": "/assets/images/login/cover.svg",
|
||||
"slogan": "Yao Autonomous Agent Engine",
|
||||
"site": "https://yaoapps.com/doc"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
msg_ok "Created Login Configuration"
|
||||
|
||||
msg_info "Creating Admin User Model"
|
||||
cat <<EOF >/opt/yao/models/admin/user.mod.yao
|
||||
{
|
||||
"name": "AdminUser",
|
||||
"table": { "name": "admin_user", "comment": "The administrator table" },
|
||||
"columns": [
|
||||
{ "label": "ID", "name": "id", "type": "ID" },
|
||||
{
|
||||
"label": "Type",
|
||||
"name": "type",
|
||||
"type": "enum",
|
||||
"option": ["admin", "staff", "user", "robot"],
|
||||
"comment": "AccountTypes: admin, staff, user, robot",
|
||||
"default": "user",
|
||||
"index": true
|
||||
},
|
||||
{
|
||||
"label": "Email",
|
||||
"name": "email",
|
||||
"type": "string",
|
||||
"length": 50,
|
||||
"comment": "Email",
|
||||
"index": true,
|
||||
"nullable": true
|
||||
},
|
||||
{
|
||||
"label": "Mobile",
|
||||
"name": "mobile",
|
||||
"type": "string",
|
||||
"length": 50,
|
||||
"comment": "Mobile",
|
||||
"index": true,
|
||||
"nullable": true
|
||||
},
|
||||
{
|
||||
"label": "Login Password",
|
||||
"name": "password",
|
||||
"type": "string",
|
||||
"length": 256,
|
||||
"comment": "Login Password",
|
||||
"crypt": "PASSWORD",
|
||||
"index": true,
|
||||
"nullable": true
|
||||
},
|
||||
{
|
||||
"label": "Name",
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"length": 80,
|
||||
"comment": "Name",
|
||||
"index": true,
|
||||
"nullable": true
|
||||
},
|
||||
{
|
||||
"label": "Status",
|
||||
"comment": "Status",
|
||||
"name": "status",
|
||||
"type": "enum",
|
||||
"default": "enabled",
|
||||
"option": ["enabled", "disabled"],
|
||||
"index": true
|
||||
}
|
||||
],
|
||||
"relations": {},
|
||||
"values": [
|
||||
{
|
||||
"name": "Admin",
|
||||
"type": "admin",
|
||||
"email": "admin@localhost",
|
||||
"password": "admin123",
|
||||
"status": "enabled"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"comment": "Email Unique Index",
|
||||
"name": "type_email_unique",
|
||||
"columns": ["type", "email"],
|
||||
"type": "unique"
|
||||
}
|
||||
],
|
||||
"option": { "timestamps": true, "soft_deletes": true }
|
||||
}
|
||||
EOF
|
||||
msg_ok "Created Admin User Model"
|
||||
|
||||
msg_info "Creating User Login Script"
|
||||
cat <<EOF >/opt/yao/scripts/user.js
|
||||
/**
|
||||
* User Login
|
||||
* @param {*} payload
|
||||
*/
|
||||
function Login(payload) {
|
||||
log.Trace("[user] Login %s", payload.email);
|
||||
return Process("yao.login.Admin", payload);
|
||||
}
|
||||
EOF
|
||||
msg_ok "Created User Login Script"
|
||||
|
||||
msg_info "Creating Menu Flow"
|
||||
cat <<EOF >/opt/yao/flows/app/menu.flow.yao
|
||||
{
|
||||
"label": "Application Menu",
|
||||
"version": "1.0.0",
|
||||
"description": "Application Menu Flow",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "menu",
|
||||
"type": "menu",
|
||||
"config": {
|
||||
"items": [
|
||||
{
|
||||
"id": "dashboard",
|
||||
"title": "Dashboard",
|
||||
"icon": "dashboard",
|
||||
"path": "/x/Chart/dashboard"
|
||||
},
|
||||
{
|
||||
"id": "admin",
|
||||
"title": "Admin",
|
||||
"icon": "admin",
|
||||
"path": "/x/Table/admin_user"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"output": "menu"
|
||||
}
|
||||
EOF
|
||||
msg_ok "Created Menu Flow"
|
||||
|
||||
msg_info "Creating Public Web Interface"
|
||||
cat <<EOF >/opt/yao/public/index.html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Yao - Autonomous Agent Engine</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
||||
max-width: 600px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 10px;
|
||||
background: linear-gradient(90deg, #00d4ff, #7c3aed);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.subtitle {
|
||||
color: #94a3b8;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.status {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
border: 1px solid #22c55e;
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.status-dot {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #22c55e;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
.info { color: #cbd5e1; line-height: 1.8; }
|
||||
.links {
|
||||
margin-top: 30px;
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.links a {
|
||||
color: #00d4ff;
|
||||
text-decoration: none;
|
||||
padding: 10px 20px;
|
||||
border: 1px solid #00d4ff;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.links a:hover {
|
||||
background: #00d4ff;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🤖 Yao</h1>
|
||||
<p class="subtitle">Autonomous Agent Engine</p>
|
||||
<div class="status">
|
||||
<span class="status-dot"></span>
|
||||
<span>Service Running</span>
|
||||
</div>
|
||||
<div class="info">
|
||||
<p><strong>Port:</strong> 5099</p>
|
||||
<p><strong>Studio Port:</strong> 5077</p>
|
||||
</div>
|
||||
<div class="links">
|
||||
<a href="https://github.com/YaoApp/yao" target="_blank">GitHub</a>
|
||||
<a href="https://yaoapps.com" target="_blank">Documentation</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
msg_ok "Created Public Web Interface"
|
||||
|
||||
msg_info "Creating Service"
|
||||
cat <<EOF >/etc/systemd/system/yao.service
|
||||
|
||||
+5
-1
@@ -31,13 +31,17 @@
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "After installation, create a new project: mkdir -p /opt/yao/myapp && cd /opt/yao/myapp && yao start",
|
||||
"text": "A minimal app.yao configuration is created automatically at /opt/yao/app.yao",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Access the web interface at http://IP:5099",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Application data is stored in /opt/yao/data/ including the SQLite database",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Yao includes built-in SQLite database, V8 JavaScript engine, and TypeScript runtime - no additional dependencies required.",
|
||||
"type": "info"
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generated": "2026-03-19T18:38:18Z",
|
||||
"generated": "2026-03-20T18:29:43Z",
|
||||
"versions": [
|
||||
{
|
||||
"slug": "agregarr",
|
||||
@@ -18,9 +18,9 @@
|
||||
{
|
||||
"slug": "llamacpp",
|
||||
"repo": "ggml-org/llama.cpp",
|
||||
"version": "b8429",
|
||||
"version": "b8457",
|
||||
"pinned": false,
|
||||
"date": "2026-03-19T14:00:45Z"
|
||||
"date": "2026-03-20T18:22:21Z"
|
||||
},
|
||||
{
|
||||
"slug": "localai",
|
||||
@@ -46,9 +46,9 @@
|
||||
{
|
||||
"slug": "pegaprox",
|
||||
"repo": "PegaProx/project-pegaprox",
|
||||
"version": "v0.9.2",
|
||||
"version": "v0.9.2.2",
|
||||
"pinned": false,
|
||||
"date": "2026-03-16T08:00:29Z"
|
||||
"date": "2026-03-20T17:42:56Z"
|
||||
},
|
||||
{
|
||||
"slug": "ragflow",
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: Slaviša Arežina (tremor021)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://wakapi.dev/ | https://github.com/muety/wakapi
|
||||
|
||||
APP="Alpine-Wakapi"
|
||||
var_tags="${var_tags:-code;time-tracking}"
|
||||
var_cpu="${var_cpu:-1}"
|
||||
var_ram="${var_ram:-512}"
|
||||
var_disk="${var_disk:-4}"
|
||||
var_os="${var_os:-alpine}"
|
||||
var_version="${var_version:-3.23}"
|
||||
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/wakapi ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
RELEASE=$(curl -s https://api.github.com/repos/muety/wakapi/releases/latest | grep "tag_name" | awk '{print substr($2, 2, length($2)-3) }')
|
||||
if [ "${RELEASE}" != "$(cat ~/.wakapi 2>/dev/null)" ] || [ ! -f ~/.wakapi ]; then
|
||||
msg_info "Stopping Wakapi Service"
|
||||
$STD rc-service wakapi stop
|
||||
msg_ok "Stopped Wakapi Service"
|
||||
|
||||
msg_info "Updating Wakapi LXC"
|
||||
$STD apk -U upgrade
|
||||
msg_ok "Updated Wakapi LXC"
|
||||
|
||||
msg_info "Creating backup"
|
||||
mkdir -p /opt/wakapi-backup
|
||||
cp /opt/wakapi/config.yml /opt/wakapi/wakapi_db.db /opt/wakapi-backup/
|
||||
msg_ok "Created backup"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "wakapi" "muety/wakapi" "tarball"
|
||||
|
||||
msg_info "Configuring Wakapi"
|
||||
cd /opt/wakapi
|
||||
$STD go mod download
|
||||
$STD go build -o wakapi
|
||||
cp /opt/wakapi-backup/config.yml /opt/wakapi/
|
||||
cp /opt/wakapi-backup/wakapi_db.db /opt/wakapi/
|
||||
rm -rf /opt/wakapi-backup
|
||||
msg_ok "Configured Wakapi"
|
||||
|
||||
msg_info "Starting Service"
|
||||
$STD rc-service wakapi start
|
||||
msg_ok "Started Service"
|
||||
msg_ok "Updated successfully"
|
||||
else
|
||||
msg_ok "No update required. ${APP} is already at ${RELEASE}"
|
||||
fi
|
||||
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
@@ -0,0 +1,6 @@
|
||||
___ __ _ _ __ __ _
|
||||
/ | / /___ (_)___ ___ | | / /___ _/ /______ _____ (_)
|
||||
/ /| | / / __ \/ / __ \/ _ \_____| | /| / / __ `/ //_/ __ `/ __ \/ /
|
||||
/ ___ |/ / /_/ / / / / / __/_____/ |/ |/ / /_/ / ,< / /_/ / /_/ / /
|
||||
/_/ |_/_/ .___/_/_/ /_/\___/ |__/|__/\__,_/_/|_|\__,_/ .___/_/
|
||||
/_/ /_/
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
_ _____ ____ __ __ _______ __
|
||||
(_) ___/____ ____ ____ _________ _____/ __ )/ /___ _____/ /_/_ __/ | / /
|
||||
/ /\__ \/ __ \/ __ \/ __ \/ ___/ __ \/ ___/ __ / / __ \/ ___/ //_// / | | / /
|
||||
/ /___/ / /_/ / /_/ / / / (__ ) /_/ / / / /_/ / / /_/ / /__/ ,< / / | |/ /
|
||||
/_//____/ .___/\____/_/ /_/____/\____/_/ /_____/_/\____/\___/_/|_|/_/ |___/
|
||||
/_/
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
______ __ __
|
||||
/_ __/__ / /__ ____ ____ _____/ /_
|
||||
/ / / _ \/ / _ \/ __ \/ __ \/ ___/ __/
|
||||
/ / / __/ / __/ /_/ / /_/ / / / /_
|
||||
/_/ \___/_/\___/ .___/\____/_/ \__/
|
||||
/_/
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: Matthew Stern (sternma) | MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/dmunozv04/iSponsorBlockTV
|
||||
|
||||
APP="iSponsorBlockTV"
|
||||
var_tags="${var_tags:-media;automation}"
|
||||
var_cpu="${var_cpu:-1}"
|
||||
var_ram="${var_ram:-1024}"
|
||||
var_disk="${var_disk:-4}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
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/isponsorblocktv ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "isponsorblocktv" "dmunozv04/iSponsorBlockTV"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop isponsorblocktv
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "isponsorblocktv" "dmunozv04/iSponsorBlockTV" "singlefile" "latest" "/opt/isponsorblocktv" "iSponsorBlockTV-*-linux"
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start isponsorblocktv
|
||||
msg_ok "Started Service"
|
||||
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} Run the setup wizard inside the container with:${CL}"
|
||||
echo -e "${TAB}${GATEWAY}${BGN}iSponsorBlockTV setup${CL}"
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: Slaviša Arežina (tremor021)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://goteleport.com/
|
||||
|
||||
APP="Teleport"
|
||||
var_tags="${var_tags:-zero-trust}"
|
||||
var_cpu="${var_cpu:-1}"
|
||||
var_ram="${var_ram:-1024}"
|
||||
var_disk="${var_disk:-4}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
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 /etc/teleport.yaml ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
msg_info "Updating Teleport"
|
||||
$STD apt update
|
||||
$STD apt upgrade -y
|
||||
msg_ok "Updated successfully!"
|
||||
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}:3080${CL}"
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: Slaviša Arežina (tremor021)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://wakapi.dev/ | https://github.com/muety/wakapi
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
msg_info "Installing Dependencies"
|
||||
$STD apk add --no-cache \
|
||||
ca-certificates \
|
||||
tzdata
|
||||
$STD update-ca-certificates
|
||||
$STD apk add --no-cache go --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
fetch_and_deploy_gh_release "wakapi" "muety/wakapi" "tarball"
|
||||
|
||||
msg_info "Configuring Wakapi"
|
||||
LOCAL_IP=$(/sbin/ip -o -4 addr list eth0 | awk '{print $4}' | cut -d/ -f1)
|
||||
cd /opt/wakapi
|
||||
$STD go mod download
|
||||
$STD go build -o wakapi
|
||||
cp config.default.yml config.yml
|
||||
sed -i 's/listen_ipv6: ::1/listen_ipv6: "-"/g' config.yml
|
||||
sed -i 's/listen_ipv4: 127.0.0.1/listen_ipv4: "0.0.0.0"/g' config.yml
|
||||
sed -i "s/public_url: http:\/\/localhost:3000/public_url: http:\/\/$LOCAL_IP:3000/g" config.yml
|
||||
msg_ok "Configured Wakapi"
|
||||
|
||||
msg_info "Enabling Wakapi Service"
|
||||
cat <<EOF >/etc/init.d/wakapi
|
||||
#!/sbin/openrc-run
|
||||
description="Wakapi Service"
|
||||
directory="/opt/wakapi"
|
||||
command="/opt/wakapi/wakapi"
|
||||
command_args="-config config.yml"
|
||||
command_background="true"
|
||||
command_user="root"
|
||||
pidfile="/var/run/wakapi.pid"
|
||||
|
||||
depend() {
|
||||
use net
|
||||
}
|
||||
EOF
|
||||
chmod +x /etc/init.d/wakapi
|
||||
$STD rc-update add wakapi default
|
||||
msg_ok "Enabled Wakapi Service"
|
||||
|
||||
msg_info "Starting Wakapi"
|
||||
$STD rc-service wakapi start
|
||||
msg_ok "Started Wakapi"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: Matthew Stern (sternma) | MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/dmunozv04/iSponsorBlockTV
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
fetch_and_deploy_gh_release "isponsorblocktv" "dmunozv04/iSponsorBlockTV" "singlefile" "latest" "/opt/isponsorblocktv" "iSponsorBlockTV-*-linux"
|
||||
|
||||
msg_info "Setting up iSponsorBlockTV"
|
||||
install -d /var/lib/isponsorblocktv
|
||||
msg_ok "Set up iSponsorBlockTV"
|
||||
|
||||
msg_info "Creating Service"
|
||||
cat <<EOF >/etc/systemd/system/isponsorblocktv.service
|
||||
[Unit]
|
||||
Description=iSponsorBlockTV
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=root
|
||||
Environment=iSPBTV_data_dir=/var/lib/isponsorblocktv
|
||||
ExecStart=/opt/isponsorblocktv/isponsorblocktv
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q isponsorblocktv
|
||||
msg_ok "Created Service"
|
||||
|
||||
msg_info "Creating CLI wrapper"
|
||||
cat <<EOF >/usr/local/bin/iSponsorBlockTV
|
||||
#!/usr/bin/env bash
|
||||
export iSPBTV_data_dir="/var/lib/isponsorblocktv"
|
||||
|
||||
set +e
|
||||
/opt/isponsorblocktv/isponsorblocktv "$@"
|
||||
status=$?
|
||||
set -e
|
||||
|
||||
case "${1:-}" in
|
||||
setup|setup-cli)
|
||||
systemctl restart isponsorblocktv >/dev/null 2>&1 || true
|
||||
;;
|
||||
esac
|
||||
|
||||
exit $status
|
||||
EOF
|
||||
chmod +x /usr/local/bin/iSponsorBlockTV
|
||||
ln -sf /usr/local/bin/iSponsorBlockTV /usr/bin/iSponsorBlockTV
|
||||
msg_ok "Created CLI wrapper"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: Slaviša Arežina (tremor021)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://goteleport.com/
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
setup_deb822_repo \
|
||||
"teleport" \
|
||||
"https://deb.releases.teleport.dev/teleport-pubkey.asc" \
|
||||
"https://apt.releases.teleport.dev/debian" \
|
||||
"trixie" \
|
||||
"stable/v18"
|
||||
|
||||
msg_info "Configuring Teleport"
|
||||
$STD apt install -y teleport
|
||||
$STD teleport configure -o /etc/teleport.yaml
|
||||
systemctl enable -q --now teleport
|
||||
sleep 10
|
||||
tctl users add teleport-admin --roles=editor,access --logins=root >~/teleportadmin.txt
|
||||
sed -i "s|https://[^:]*:3080|https://${LOCAL_IP}:3080|g" ~/teleportadmin.txt
|
||||
msg_ok "Configured Teleport"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
|
||||
__ ______ _____
|
||||
/ / / / __ `/ __ \
|
||||
/ /_/ / /_/ / /_/ /
|
||||
\__, /\__,_/\____/
|
||||
/____/
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/Heretek-AI/ProxmoxVE/refs/heads/main}"
|
||||
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/build.func)
|
||||
# Author: Heretek-AI
|
||||
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/YaoApp/yao
|
||||
|
||||
APP="yao"
|
||||
var_tags="${var_tags:-ai;agents;automation;low-code}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-4096}"
|
||||
var_disk="${var_disk:-10}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
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 /usr/local/bin/yao ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "yao" "YaoApp/yao"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop yao
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
msg_info "Backing up Data"
|
||||
cp -r /opt/yao/data /opt/yao_data_backup 2>/dev/null || true
|
||||
cp /opt/yao/app.yao /opt/yao_app_backup 2>/dev/null || true
|
||||
cp /opt/yao/.env /opt/yao_env_backup 2>/dev/null || true
|
||||
msg_ok "Backed up Data"
|
||||
|
||||
fetch_and_deploy_gh_release "yao" "YaoApp/yao" "singlefile" "latest" "/usr/local/bin" "yao-*-linux-*"
|
||||
|
||||
msg_info "Restoring Data"
|
||||
cp -r /opt/yao_data_backup/. /opt/yao/data 2>/dev/null || true
|
||||
cp /opt/yao_app_backup /opt/yao/app.yao 2>/dev/null || true
|
||||
cp /opt/yao_env_backup /opt/yao/.env 2>/dev/null || true
|
||||
rm -rf /opt/yao_data_backup /opt/yao_app_backup /opt/yao_env_backup
|
||||
msg_ok "Restored Data"
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start yao
|
||||
msg_ok "Started Service"
|
||||
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}:5099${CL}"
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "Yao (In Development)",
|
||||
"slug": "yao",
|
||||
"categories": [20],
|
||||
"date_created": "2026-03-20",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 5099,
|
||||
"documentation": "https://yaoapps.com/docs",
|
||||
"website": "https://yaoapps.com",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/yao.webp",
|
||||
"config_path": "/opt/yao/.env",
|
||||
"description": "Yao is an open-source engine for autonomous agents - event-driven, proactive, and self-scheduling. Build agents that work like real team members with three trigger modes: Clock (scheduled), Human (email/message), and Event (webhook/database).",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/yao.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 4096,
|
||||
"hdd": 10,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "After installation, create a new project: mkdir -p /opt/yao/myapp && cd /opt/yao/myapp && yao start",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Access the web interface at http://IP:5099",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Yao includes built-in SQLite database, V8 JavaScript engine, and TypeScript runtime - no additional dependencies required.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Author: Heretek-AI
|
||||
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/YaoApp/yao
|
||||
|
||||
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 \
|
||||
curl \
|
||||
unzip
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
fetch_and_deploy_gh_release "yao" "YaoApp/yao" "singlefile" "latest" "/usr/local/bin" "yao-*-linux-*"
|
||||
|
||||
msg_info "Creating Application Directory"
|
||||
mkdir -p /opt/yao/data
|
||||
mkdir -p /opt/yao/etc
|
||||
msg_ok "Created Application Directory"
|
||||
|
||||
msg_info "Creating Environment File"
|
||||
cat <<EOF >/opt/yao/.env
|
||||
YAO_PORT=5099
|
||||
YAO_STUDIO_PORT=5077
|
||||
EOF
|
||||
msg_ok "Created Environment File"
|
||||
|
||||
msg_info "Creating Minimal Application Configuration"
|
||||
cat <<EOF >/opt/yao/app.yao
|
||||
{
|
||||
"name": "yao-app",
|
||||
"version": "1.0.0",
|
||||
"description": "Yao Autonomous Agent Engine",
|
||||
"studio": {
|
||||
"port": 5077
|
||||
},
|
||||
"server": {
|
||||
"port": 5099,
|
||||
"host": "0.0.0.0"
|
||||
},
|
||||
"database": {
|
||||
"driver": "sqlite3",
|
||||
"file": "/opt/yao/data/yao.db"
|
||||
},
|
||||
"session": {
|
||||
"store": "file",
|
||||
"path": "/opt/yao/data/sessions"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
mkdir -p /opt/yao/data/sessions
|
||||
msg_ok "Created Minimal Application Configuration"
|
||||
|
||||
msg_info "Creating Service"
|
||||
cat <<EOF >/etc/systemd/system/yao.service
|
||||
[Unit]
|
||||
Description=Yao - Autonomous Agent Engine
|
||||
After=network.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/yao
|
||||
ExecStart=/usr/local/bin/yao start
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
EnvironmentFile=/opt/yao/.env
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q --now yao
|
||||
msg_ok "Created Service"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
+4
-1
@@ -285,7 +285,10 @@ EOF
|
||||
post_progress_to_api
|
||||
|
||||
local tools_content
|
||||
tools_content=$(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/misc/tools.func) || {
|
||||
# Add cache-busting query parameter to ensure we get the latest version
|
||||
# GitHub's CDN caches raw content, which can cause stale scripts to be served
|
||||
local cache_bust="?_=$(date +%s)"
|
||||
tools_content=$(curl -fsSL "${COMMUNITY_SCRIPTS_URL}/misc/tools.func${cache_bust}") || {
|
||||
msg_error "Failed to download tools.func"
|
||||
exit 115
|
||||
}
|
||||
|
||||
+375
-176
@@ -105,11 +105,13 @@ curl_with_retry() {
|
||||
fi
|
||||
fi
|
||||
|
||||
debug_log "curl attempt $attempt failed, waiting ${backoff}s before retry..."
|
||||
debug_log "curl attempt $attempt failed (timeout=${timeout}s), waiting ${backoff}s before retry..."
|
||||
sleep "$backoff"
|
||||
# Exponential backoff: 1, 2, 4, 8... capped at 30s
|
||||
backoff=$((backoff * 2))
|
||||
((backoff > 30)) && backoff=30
|
||||
# Double --max-time on each retry so slow connections can finish
|
||||
timeout=$((timeout * 2))
|
||||
((attempt++))
|
||||
done
|
||||
|
||||
@@ -172,8 +174,10 @@ curl_api_with_retry() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
debug_log "curl API attempt $attempt failed (HTTP $http_code), waiting ${attempt}s..."
|
||||
debug_log "curl API attempt $attempt failed (HTTP $http_code, timeout=${timeout}s), waiting ${attempt}s..."
|
||||
sleep "$attempt"
|
||||
# Double --max-time on each retry so slow connections can finish
|
||||
timeout=$((timeout * 2))
|
||||
((attempt++))
|
||||
done
|
||||
|
||||
@@ -289,9 +293,9 @@ cleanup_tool_keyrings() {
|
||||
local tool_patterns=("$@")
|
||||
|
||||
for pattern in "${tool_patterns[@]}"; do
|
||||
rm -f /usr/share/keyrings/"${pattern}"*.gpg \
|
||||
/etc/apt/keyrings/"${pattern}"*.gpg \
|
||||
/etc/apt/trusted.gpg.d/"${pattern}"*.gpg 2>/dev/null || true
|
||||
rm -f /usr/share/keyrings/${pattern}*.gpg \
|
||||
/etc/apt/keyrings/${pattern}*.gpg \
|
||||
/etc/apt/trusted.gpg.d/${pattern}*.gpg 2>/dev/null || true
|
||||
done
|
||||
}
|
||||
|
||||
@@ -791,7 +795,7 @@ manage_tool_repository() {
|
||||
repo_component="main"
|
||||
[[ "$distro_id" == "ubuntu" ]] && repo_component="multiverse"
|
||||
|
||||
cat <<EOF >/etc/apt/sources.list.d/mongodb-org-"${version}".sources
|
||||
cat <<EOF >/etc/apt/sources.list.d/mongodb-org-${version}.sources
|
||||
Types: deb
|
||||
URIs: ${repo_url}
|
||||
Suites: ${suite}/mongodb-org/${version}
|
||||
@@ -1371,7 +1375,7 @@ download_file() {
|
||||
local curl_opts=(-fsSL)
|
||||
[[ "$show_progress" == "true" ]] && curl_opts=(-fL#)
|
||||
|
||||
for attempt in $(seq 1 "$max_retries"); do
|
||||
for attempt in $(seq 1 $max_retries); do
|
||||
if curl "${curl_opts[@]}" -o "$output" "$url"; then
|
||||
return 0
|
||||
fi
|
||||
@@ -1846,7 +1850,7 @@ setup_deb822_repo() {
|
||||
[[ -n "$architectures" ]] && echo "Architectures: $architectures"
|
||||
echo "Signed-By: /etc/apt/keyrings/${name}.gpg"
|
||||
[[ -n "$enabled" ]] && echo "Enabled: $enabled"
|
||||
} >/etc/apt/sources.list.d/"${name}".sources
|
||||
} >/etc/apt/sources.list.d/${name}.sources
|
||||
|
||||
$STD apt update || {
|
||||
msg_warn "apt update failed after adding repository: ${name}"
|
||||
@@ -1975,6 +1979,47 @@ extract_version_from_json() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Get latest GitHub tag (for repos that only publish tags, not releases).
|
||||
#
|
||||
# Usage:
|
||||
# get_latest_gh_tag "owner/repo" [prefix]
|
||||
#
|
||||
# Arguments:
|
||||
# $1 - GitHub repo (owner/repo)
|
||||
# $2 - Optional prefix filter (e.g., "v" to only match tags starting with "v")
|
||||
#
|
||||
# Returns:
|
||||
# Latest tag name (stdout), or returns 1 on failure
|
||||
# ------------------------------------------------------------------------------
|
||||
get_latest_gh_tag() {
|
||||
local repo="$1"
|
||||
local prefix="${2:-}"
|
||||
local temp_file
|
||||
temp_file=$(mktemp)
|
||||
|
||||
if ! github_api_call "https://api.github.com/repos/${repo}/tags?per_page=50" "$temp_file"; then
|
||||
rm -f "$temp_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local tag=""
|
||||
if [[ -n "$prefix" ]]; then
|
||||
tag=$(jq -r --arg p "$prefix" '[.[] | select(.name | startswith($p))][0].name // empty' "$temp_file")
|
||||
else
|
||||
tag=$(jq -r '.[0].name // empty' "$temp_file")
|
||||
fi
|
||||
|
||||
rm -f "$temp_file"
|
||||
|
||||
if [[ -z "$tag" ]]; then
|
||||
msg_error "No tags found for ${repo}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "$tag"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Get latest GitHub release version with fallback to tags
|
||||
# Usage: get_latest_github_release "owner/repo" [strip_v] [include_prerelease]
|
||||
@@ -2073,103 +2118,131 @@ verify_gpg_fingerprint() {
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Get latest GitHub tag for a repository.
|
||||
# Fetches and deploys a GitHub tag-based source tarball.
|
||||
#
|
||||
# Description:
|
||||
# - Queries the GitHub API for tags (not releases)
|
||||
# - Useful for repos that only create tags, not full releases
|
||||
# - Supports optional prefix filter and version-only extraction
|
||||
# - Returns the latest tag name (printed to stdout)
|
||||
# - Downloads the source tarball for a given tag from GitHub
|
||||
# - Extracts to the target directory
|
||||
# - Writes the version to ~/.<app>
|
||||
#
|
||||
# Usage:
|
||||
# MONGO_VERSION=$(get_latest_gh_tag "mongodb/mongo-tools")
|
||||
# LATEST=$(get_latest_gh_tag "owner/repo" "v") # only tags starting with "v"
|
||||
# LATEST=$(get_latest_gh_tag "owner/repo" "" "true") # strip leading "v"
|
||||
# fetch_and_deploy_gh_tag "guacd" "apache/guacamole-server"
|
||||
# fetch_and_deploy_gh_tag "guacd" "apache/guacamole-server" "latest" "/opt/guacamole-server"
|
||||
#
|
||||
# Arguments:
|
||||
# $1 - GitHub repo (owner/repo)
|
||||
# $2 - Tag prefix filter (optional, e.g. "v" or "100.")
|
||||
# $3 - Strip prefix from result (optional, "true" to strip $2 prefix)
|
||||
#
|
||||
# Returns:
|
||||
# 0 on success (tag printed to stdout), 1 on failure
|
||||
# $1 - App name (used for version file ~/.<app>)
|
||||
# $2 - GitHub repo (owner/repo)
|
||||
# $3 - Tag version (default: "latest" → auto-detect via get_latest_gh_tag)
|
||||
# $4 - Target directory (default: /opt/$app)
|
||||
#
|
||||
# Notes:
|
||||
# - Skips tags containing "rc", "alpha", "beta", "dev", "test"
|
||||
# - Sorts by version number (sort -V) to find the latest
|
||||
# - Respects GITHUB_TOKEN for rate limiting
|
||||
# - Supports CLEAN_INSTALL=1 to wipe target before extracting
|
||||
# - For repos that only publish tags, not GitHub Releases
|
||||
# ------------------------------------------------------------------------------
|
||||
get_latest_gh_tag() {
|
||||
local repo="$1"
|
||||
local prefix="${2:-}"
|
||||
local strip_prefix="${3:-false}"
|
||||
fetch_and_deploy_gh_tag() {
|
||||
local app="$1"
|
||||
local repo="$2"
|
||||
local version="${3:-latest}"
|
||||
local target="${4:-/opt/$app}"
|
||||
local app_lc=""
|
||||
app_lc="$(echo "${app,,}" | tr -d ' ')"
|
||||
local version_file="$HOME/.${app_lc}"
|
||||
|
||||
local header_args=()
|
||||
[[ -n "${GITHUB_TOKEN:-}" ]] && header_args=(-H "Authorization: Bearer $GITHUB_TOKEN")
|
||||
if [[ "$version" == "latest" ]]; then
|
||||
version=$(get_latest_gh_tag "$repo") || {
|
||||
msg_error "Failed to determine latest tag for ${repo}"
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
|
||||
local http_code=""
|
||||
http_code=$(curl -sSL --max-time 20 -w "%{http_code}" -o /tmp/gh_tags.json \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
-H 'X-GitHub-Api-Version: 2022-11-28' \
|
||||
"${header_args[@]}" \
|
||||
"https://api.github.com/repos/${repo}/tags?per_page=100" 2>/dev/null) || true
|
||||
local current_version=""
|
||||
[[ -f "$version_file" ]] && current_version=$(<"$version_file")
|
||||
|
||||
if [[ "$http_code" == "401" ]]; then
|
||||
msg_error "GitHub API authentication failed (HTTP 401)."
|
||||
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
|
||||
msg_error "Your GITHUB_TOKEN appears to be invalid or expired."
|
||||
else
|
||||
msg_error "The repository may require authentication. Try: export GITHUB_TOKEN=\"ghp_your_token\""
|
||||
fi
|
||||
rm -f /tmp/gh_tags.json
|
||||
if [[ "$current_version" == "$version" ]]; then
|
||||
msg_ok "$app is already up-to-date ($version)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local tmpdir
|
||||
tmpdir=$(mktemp -d) || return 1
|
||||
local tarball_url="https://github.com/${repo}/archive/refs/tags/${version}.tar.gz"
|
||||
local filename="${app_lc}-${version}.tar.gz"
|
||||
|
||||
msg_info "Fetching GitHub tag: ${app} (${version})"
|
||||
|
||||
download_file "$tarball_url" "$tmpdir/$filename" || {
|
||||
msg_error "Download failed: $tarball_url"
|
||||
rm -rf "$tmpdir"
|
||||
return 1
|
||||
}
|
||||
|
||||
mkdir -p "$target"
|
||||
if [[ "${CLEAN_INSTALL:-0}" == "1" ]]; then
|
||||
rm -rf "${target:?}/"*
|
||||
fi
|
||||
|
||||
if [[ "$http_code" == "403" ]]; then
|
||||
msg_error "GitHub API rate limit exceeded (HTTP 403)."
|
||||
msg_error "To increase the limit, export a GitHub token before running the script:"
|
||||
msg_error " export GITHUB_TOKEN=\"ghp_your_token_here\""
|
||||
rm -f /tmp/gh_tags.json
|
||||
tar --no-same-owner -xzf "$tmpdir/$filename" -C "$tmpdir" || {
|
||||
msg_error "Failed to extract tarball"
|
||||
rm -rf "$tmpdir"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "$http_code" == "000" || -z "$http_code" ]]; then
|
||||
msg_error "GitHub API connection failed (no response)."
|
||||
msg_error "Check your network/DNS: curl -sSL https://api.github.com/rate_limit"
|
||||
rm -f /tmp/gh_tags.json
|
||||
return 1
|
||||
fi
|
||||
local unpack_dir
|
||||
unpack_dir=$(find "$tmpdir" -mindepth 1 -maxdepth 1 -type d | head -n1)
|
||||
|
||||
if [[ "$http_code" != "200" ]] || [[ ! -s /tmp/gh_tags.json ]]; then
|
||||
msg_error "Unable to fetch tags for ${repo} (HTTP ${http_code})"
|
||||
rm -f /tmp/gh_tags.json
|
||||
return 1
|
||||
fi
|
||||
shopt -s dotglob nullglob
|
||||
cp -r "$unpack_dir"/* "$target/"
|
||||
shopt -u dotglob nullglob
|
||||
|
||||
local tags_json
|
||||
tags_json=$(</tmp/gh_tags.json)
|
||||
rm -f /tmp/gh_tags.json
|
||||
|
||||
# Extract tag names, filter by prefix, exclude pre-release patterns, sort by version
|
||||
local latest=""
|
||||
latest=$(echo "$tags_json" | grep -oP '"name":\s*"\K[^"]+' |
|
||||
{ [[ -n "$prefix" ]] && grep "^${prefix}" || cat; } |
|
||||
grep -viE '(rc|alpha|beta|dev|test|preview|snapshot)' |
|
||||
sort -V | tail -n1)
|
||||
|
||||
if [[ -z "$latest" ]]; then
|
||||
msg_warn "No matching tags found for ${repo}${prefix:+ (prefix: $prefix)}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "$strip_prefix" == "true" && -n "$prefix" ]]; then
|
||||
latest="${latest#"$prefix"}"
|
||||
fi
|
||||
|
||||
echo "$latest"
|
||||
rm -rf "$tmpdir"
|
||||
echo "$version" >"$version_file"
|
||||
msg_ok "Deployed ${app} ${version} to ${target}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Checks for new GitHub tag (for repos without releases).
|
||||
#
|
||||
# Description:
|
||||
# - Uses get_latest_gh_tag to fetch the latest tag
|
||||
# - Compares it to a local cached version (~/.<app>)
|
||||
# - If newer, sets global CHECK_UPDATE_RELEASE and returns 0
|
||||
#
|
||||
# Usage:
|
||||
# if check_for_gh_tag "guacd" "apache/guacamole-server"; then
|
||||
# fetch_and_deploy_gh_tag "guacd" "apache/guacamole-server" "/opt/guacamole-server"
|
||||
# fi
|
||||
#
|
||||
# Notes:
|
||||
# - For repos that only publish tags, not GitHub Releases
|
||||
# - Same interface as check_for_gh_release
|
||||
# ------------------------------------------------------------------------------
|
||||
check_for_gh_tag() {
|
||||
local app="$1"
|
||||
local repo="$2"
|
||||
local prefix="${3:-}"
|
||||
local app_lc=""
|
||||
app_lc="$(echo "${app,,}" | tr -d ' ')"
|
||||
local current_file="$HOME/.${app_lc}"
|
||||
|
||||
msg_info "Checking for update: ${app}"
|
||||
|
||||
local latest=""
|
||||
latest=$(get_latest_gh_tag "$repo" "$prefix") || return 1
|
||||
|
||||
local current=""
|
||||
[[ -f "$current_file" ]] && current="$(<"$current_file")"
|
||||
|
||||
if [[ -z "$current" || "$current" != "$latest" ]]; then
|
||||
CHECK_UPDATE_RELEASE="$latest"
|
||||
msg_ok "Update available: ${app} ${current:-not installed} → ${latest}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
msg_ok "No update available: ${app} (${latest})"
|
||||
return 1
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# INSTALL FUNCTIONS
|
||||
# ==============================================================================
|
||||
@@ -2198,6 +2271,7 @@ check_for_gh_release() {
|
||||
local app="$1"
|
||||
local source="$2"
|
||||
local pinned_version_in="${3:-}" # optional
|
||||
local pin_reason="${4:-}" # optional reason shown to user
|
||||
local app_lc=""
|
||||
app_lc="$(echo "${app,,}" | tr -d ' ')"
|
||||
local current_file="$HOME/.${app_lc}"
|
||||
@@ -2219,6 +2293,35 @@ check_for_gh_release() {
|
||||
# Try /latest endpoint for non-pinned versions (most efficient)
|
||||
local releases_json="" http_code=""
|
||||
|
||||
# For pinned versions, query the specific release tag directly
|
||||
if [[ -n "$pinned_version_in" ]]; then
|
||||
http_code=$(curl -sSL --max-time 20 -w "%{http_code}" -o /tmp/gh_check.json \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
-H 'X-GitHub-Api-Version: 2022-11-28' \
|
||||
"${header_args[@]}" \
|
||||
"https://api.github.com/repos/${source}/releases/tags/${pinned_version_in}" 2>/dev/null) || true
|
||||
|
||||
if [[ "$http_code" == "200" ]] && [[ -s /tmp/gh_check.json ]]; then
|
||||
releases_json="[$(</tmp/gh_check.json)]"
|
||||
elif [[ "$http_code" == "401" ]]; then
|
||||
msg_error "GitHub API authentication failed (HTTP 401)."
|
||||
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
|
||||
msg_error "Your GITHUB_TOKEN appears to be invalid or expired."
|
||||
else
|
||||
msg_error "The repository may require authentication. Try: export GITHUB_TOKEN=\"ghp_your_token\""
|
||||
fi
|
||||
rm -f /tmp/gh_check.json
|
||||
return 1
|
||||
elif [[ "$http_code" == "403" ]]; then
|
||||
msg_error "GitHub API rate limit exceeded (HTTP 403)."
|
||||
msg_error "To increase the limit, export a GitHub token before running the script:"
|
||||
msg_error " export GITHUB_TOKEN=\"ghp_your_token_here\""
|
||||
rm -f /tmp/gh_check.json
|
||||
return 1
|
||||
fi
|
||||
rm -f /tmp/gh_check.json
|
||||
fi
|
||||
|
||||
if [[ -z "$pinned_version_in" ]]; then
|
||||
http_code=$(curl -sSL --max-time 20 -w "%{http_code}" -o /tmp/gh_check.json \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
@@ -2342,7 +2445,11 @@ check_for_gh_release() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
msg_ok "No update available: ${app} is already on pinned version (${current})"
|
||||
if [[ -n "$pin_reason" ]]; then
|
||||
msg_ok "No update available: ${app} (${current}) - update held back: ${pin_reason}"
|
||||
else
|
||||
msg_ok "No update available: ${app} (${current}) - update temporarily held back due to issues with newer releases"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -2381,6 +2488,7 @@ check_for_codeberg_release() {
|
||||
local app="$1"
|
||||
local source="$2"
|
||||
local pinned_version_in="${3:-}" # optional
|
||||
local pin_reason="${4:-}" # optional reason shown to user
|
||||
local app_lc="${app,,}"
|
||||
local current_file="$HOME/.${app_lc}"
|
||||
|
||||
@@ -2460,7 +2568,11 @@ check_for_codeberg_release() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
msg_ok "No update available: ${app} is already on pinned version (${current})"
|
||||
if [[ -n "$pin_reason" ]]; then
|
||||
msg_ok "No update available: ${app} (${current}) - update held back: ${pin_reason}"
|
||||
else
|
||||
msg_ok "No update available: ${app} (${current}) - update temporarily held back due to issues with newer releases"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -2486,6 +2598,8 @@ check_for_codeberg_release() {
|
||||
# ------------------------------------------------------------------------------
|
||||
create_self_signed_cert() {
|
||||
local APP_NAME="${1:-${APPLICATION}}"
|
||||
local HOSTNAME="$(hostname -f)"
|
||||
local IP="$(hostname -I | awk '{print $1}')"
|
||||
local APP_NAME_LC=$(echo "${APP_NAME,,}" | tr -d ' ')
|
||||
local CERT_DIR="/etc/ssl/${APP_NAME_LC}"
|
||||
local CERT_KEY="${CERT_DIR}/${APP_NAME_LC}.key"
|
||||
@@ -2503,8 +2617,8 @@ create_self_signed_cert() {
|
||||
|
||||
mkdir -p "$CERT_DIR"
|
||||
$STD openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 \
|
||||
-subj "/CN=${APP_NAME}" \
|
||||
-addext "subjectAltName=DNS:${APP_NAME}" \
|
||||
-subj "/CN=${HOSTNAME}" \
|
||||
-addext "subjectAltName=DNS:${HOSTNAME},DNS:localhost,IP:${IP},IP:127.0.0.1" \
|
||||
-keyout "$CERT_KEY" \
|
||||
-out "$CERT_CRT" || {
|
||||
msg_error "Failed to create self-signed certificate"
|
||||
@@ -2574,6 +2688,30 @@ function ensure_usr_local_bin_persist() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# curl_download - Downloads a file with automatic retry and exponential backoff.
|
||||
#
|
||||
# Usage: curl_download <output_file> <url>
|
||||
#
|
||||
# Retries up to 5 times with increasing --max-time (60/120/240/480/960s).
|
||||
# Returns 0 on success, 1 if all attempts fail.
|
||||
# ------------------------------------------------------------------------------
|
||||
function curl_download() {
|
||||
local output="$1"
|
||||
local url="$2"
|
||||
local timeouts=(60 120 240 480 960)
|
||||
|
||||
for i in "${!timeouts[@]}"; do
|
||||
if curl --connect-timeout 15 --max-time "${timeouts[$i]}" -fsSL -o "$output" "$url"; then
|
||||
return 0
|
||||
fi
|
||||
if ((i < ${#timeouts[@]} - 1)); then
|
||||
msg_warn "Download timed out after ${timeouts[$i]}s, retrying... (attempt $((i + 2))/${#timeouts[@]})"
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Downloads and deploys latest Codeberg release (source, binary, tarball, asset).
|
||||
#
|
||||
@@ -2631,8 +2769,7 @@ function fetch_and_deploy_codeberg_release() {
|
||||
local app_lc=$(echo "${app,,}" | tr -d ' ')
|
||||
local version_file="$HOME/.${app_lc}"
|
||||
|
||||
local api_timeout="--connect-timeout 10 --max-time 60"
|
||||
local download_timeout="--connect-timeout 15 --max-time 900"
|
||||
local api_timeouts=(60 120 240)
|
||||
|
||||
local current_version=""
|
||||
[[ -f "$version_file" ]] && current_version=$(<"$version_file")
|
||||
@@ -2672,7 +2809,7 @@ function fetch_and_deploy_codeberg_release() {
|
||||
|
||||
# Codeberg archive URL format: https://codeberg.org/{owner}/{repo}/archive/{tag}.tar.gz
|
||||
local archive_url="https://codeberg.org/$repo/archive/${tag_name}.tar.gz"
|
||||
if curl "$download_timeout" -fsSL -o "$tmpdir/$filename" "$archive_url"; then
|
||||
if curl_download "$tmpdir/$filename" "$archive_url"; then
|
||||
download_success=true
|
||||
fi
|
||||
|
||||
@@ -2719,16 +2856,18 @@ function fetch_and_deploy_codeberg_release() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
local max_retries=3 retry_delay=2 attempt=1 success=false resp http_code
|
||||
local attempt=0 success=false resp http_code
|
||||
|
||||
while ((attempt <= max_retries)); do
|
||||
resp=$(curl "$api_timeout" -fsSL -w "%{http_code}" -o /tmp/codeberg_rel.json "$api_url") && success=true && break
|
||||
sleep "$retry_delay"
|
||||
while ((attempt < ${#api_timeouts[@]})); do
|
||||
resp=$(curl --connect-timeout 10 --max-time "${api_timeouts[$attempt]}" -fsSL -w "%{http_code}" -o /tmp/codeberg_rel.json "$api_url") && success=true && break
|
||||
((attempt++))
|
||||
if ((attempt < ${#api_timeouts[@]})); then
|
||||
msg_warn "API request timed out after ${api_timeouts[$((attempt - 1))]}s, retrying... (attempt $((attempt + 1))/${#api_timeouts[@]})"
|
||||
fi
|
||||
done
|
||||
|
||||
if ! $success; then
|
||||
msg_error "Failed to fetch release metadata from $api_url after $max_retries attempts"
|
||||
msg_error "Failed to fetch release metadata from $api_url after ${#api_timeouts[@]} attempts"
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -2769,7 +2908,7 @@ function fetch_and_deploy_codeberg_release() {
|
||||
|
||||
# Codeberg archive URL format
|
||||
local archive_url="https://codeberg.org/$repo/archive/${tag_name}.tar.gz"
|
||||
if curl "$download_timeout" -fsSL -o "$tmpdir/$filename" "$archive_url"; then
|
||||
if curl_download "$tmpdir/$filename" "$archive_url"; then
|
||||
download_success=true
|
||||
fi
|
||||
|
||||
@@ -2843,7 +2982,7 @@ function fetch_and_deploy_codeberg_release() {
|
||||
fi
|
||||
|
||||
filename="${url_match##*/}"
|
||||
curl "$download_timeout" -fsSL -o "$tmpdir/$filename" "$url_match" || {
|
||||
curl_download "$tmpdir/$filename" "$url_match" || {
|
||||
msg_error "Download failed: $url_match"
|
||||
rm -rf "$tmpdir"
|
||||
return 1
|
||||
@@ -2886,7 +3025,7 @@ function fetch_and_deploy_codeberg_release() {
|
||||
}
|
||||
|
||||
filename="${asset_url##*/}"
|
||||
curl "$download_timeout" -fsSL -o "$tmpdir/$filename" "$asset_url" || {
|
||||
curl_download "$tmpdir/$filename" "$asset_url" || {
|
||||
msg_error "Download failed: $asset_url"
|
||||
rm -rf "$tmpdir"
|
||||
return 1
|
||||
@@ -2987,7 +3126,7 @@ function fetch_and_deploy_codeberg_release() {
|
||||
local target_file="$app"
|
||||
[[ "$use_filename" == "true" ]] && target_file="$filename"
|
||||
|
||||
curl "$download_timeout" -fsSL -o "$target/$target_file" "$asset_url" || {
|
||||
curl_download "$target/$target_file" "$asset_url" || {
|
||||
msg_error "Download failed: $asset_url"
|
||||
rm -rf "$tmpdir"
|
||||
return 1
|
||||
@@ -3182,8 +3321,7 @@ function fetch_and_deploy_gh_release() {
|
||||
local app_lc=$(echo "${app,,}" | tr -d ' ')
|
||||
local version_file="$HOME/.${app_lc}"
|
||||
|
||||
local api_timeout="--connect-timeout 10 --max-time 60"
|
||||
local download_timeout="--connect-timeout 15 --max-time 900"
|
||||
local api_timeouts=(60 120 240)
|
||||
|
||||
local current_version=""
|
||||
[[ -f "$version_file" ]] && current_version=$(<"$version_file")
|
||||
@@ -3203,10 +3341,10 @@ function fetch_and_deploy_gh_release() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
local max_retries=3 retry_delay=2 attempt=1 success=false http_code
|
||||
local max_retries=${#api_timeouts[@]} retry_delay=2 attempt=1 success=false http_code
|
||||
|
||||
while ((attempt <= max_retries)); do
|
||||
http_code=$(curl "$api_timeout" -sSL -w "%{http_code}" -o /tmp/gh_rel.json "${header[@]}" "$api_url" 2>/dev/null) || true
|
||||
http_code=$(curl --connect-timeout 10 --max-time "${api_timeouts[$((attempt - 1))]:-240}" -sSL -w "%{http_code}" -o /tmp/gh_rel.json "${header[@]}" "$api_url" 2>/dev/null) || true
|
||||
if [[ "$http_code" == "200" ]]; then
|
||||
success=true
|
||||
break
|
||||
@@ -3280,7 +3418,7 @@ function fetch_and_deploy_gh_release() {
|
||||
local direct_tarball_url="https://github.com/$repo/archive/refs/tags/$tag_name.tar.gz"
|
||||
filename="${app_lc}-${version_safe}.tar.gz"
|
||||
|
||||
curl "$download_timeout" -fsSL -o "$tmpdir/$filename" "$direct_tarball_url" || {
|
||||
curl_download "$tmpdir/$filename" "$direct_tarball_url" || {
|
||||
msg_error "Download failed: $direct_tarball_url"
|
||||
rm -rf "$tmpdir"
|
||||
return 1
|
||||
@@ -3383,7 +3521,7 @@ function fetch_and_deploy_gh_release() {
|
||||
fi
|
||||
|
||||
filename="${url_match##*/}"
|
||||
curl "$download_timeout" -fsSL -o "$tmpdir/$filename" "$url_match" || {
|
||||
curl_download "$tmpdir/$filename" "$url_match" || {
|
||||
msg_error "Download failed: $url_match"
|
||||
rm -rf "$tmpdir"
|
||||
return 1
|
||||
@@ -3395,7 +3533,7 @@ function fetch_and_deploy_gh_release() {
|
||||
local dpkg_opts=""
|
||||
[[ "${DPKG_FORCE_CONFOLD:-}" == "1" ]] && dpkg_opts="-o Dpkg::Options::=--force-confold"
|
||||
[[ "${DPKG_FORCE_CONFNEW:-}" == "1" ]] && dpkg_opts="-o Dpkg::Options::=--force-confnew"
|
||||
DEBIAN_FRONTEND=noninteractive SYSTEMD_OFFLINE=1 $STD apt install -y "$dpkg_opts" "$tmpdir/$filename" || {
|
||||
DEBIAN_FRONTEND=noninteractive SYSTEMD_OFFLINE=1 $STD apt install -y $dpkg_opts "$tmpdir/$filename" || {
|
||||
SYSTEMD_OFFLINE=1 $STD dpkg -i "$tmpdir/$filename" || {
|
||||
msg_error "Both apt and dpkg installation failed"
|
||||
rm -rf "$tmpdir"
|
||||
@@ -3450,7 +3588,7 @@ function fetch_and_deploy_gh_release() {
|
||||
}
|
||||
|
||||
filename="${asset_url##*/}"
|
||||
curl "$download_timeout" -fsSL -o "$tmpdir/$filename" "$asset_url" || {
|
||||
curl_download "$tmpdir/$filename" "$asset_url" || {
|
||||
msg_error "Download failed: $asset_url"
|
||||
rm -rf "$tmpdir"
|
||||
return 1
|
||||
@@ -3571,7 +3709,7 @@ function fetch_and_deploy_gh_release() {
|
||||
local target_file="$app"
|
||||
[[ "$use_filename" == "true" ]] && target_file="$filename"
|
||||
|
||||
curl "$download_timeout" -fsSL -o "$target/$target_file" "$asset_url" || {
|
||||
curl_download "$target/$target_file" "$asset_url" || {
|
||||
msg_error "Download failed: $asset_url"
|
||||
rm -rf "$tmpdir"
|
||||
return 1
|
||||
@@ -4128,6 +4266,8 @@ function setup_gs() {
|
||||
# - NVIDIA requires matching host driver version
|
||||
# ------------------------------------------------------------------------------
|
||||
function setup_hwaccel() {
|
||||
local service_user="${1:-}"
|
||||
|
||||
# Check if user explicitly disabled GPU in advanced settings
|
||||
# ENABLE_GPU is exported from build.func
|
||||
if [[ "${ENABLE_GPU:-no}" == "no" ]]; then
|
||||
@@ -4379,7 +4519,7 @@ function setup_hwaccel() {
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Device Permissions
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
_setup_gpu_permissions "$in_ct"
|
||||
_setup_gpu_permissions "$in_ct" "$service_user"
|
||||
|
||||
cache_installed_version "hwaccel" "1.0"
|
||||
msg_ok "Setup Hardware Acceleration"
|
||||
@@ -4572,9 +4712,6 @@ _setup_amd_apu() {
|
||||
$STD apt -y install firmware-amd-graphics 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# ROCm compute stack (OpenCL + HIP) - also works for many APUs
|
||||
_setup_rocm "$os_id" "$os_codename"
|
||||
|
||||
msg_ok "AMD APU configured"
|
||||
}
|
||||
|
||||
@@ -4622,16 +4759,9 @@ _setup_rocm() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# AMDGPU driver repository (append to same keyring)
|
||||
{
|
||||
echo ""
|
||||
echo "Types: deb"
|
||||
echo "URIs: https://repo.radeon.com/amdgpu/latest/ubuntu"
|
||||
echo "Suites: ${ROCM_REPO_CODENAME}"
|
||||
echo "Components: main"
|
||||
echo "Architectures: amd64"
|
||||
echo "Signed-By: /etc/apt/keyrings/rocm.gpg"
|
||||
} >>/etc/apt/sources.list.d/rocm.sources
|
||||
# Note: The amdgpu/latest/ubuntu repo (kernel driver packages) is intentionally
|
||||
# omitted — kernel drivers are managed by the Proxmox host, not the LXC container.
|
||||
# Only the ROCm userspace compute stack is needed inside the container.
|
||||
|
||||
# Pin ROCm packages to prefer radeon repo
|
||||
cat <<EOF >/etc/apt/preferences.d/rocm-pin-600
|
||||
@@ -4640,7 +4770,26 @@ Pin: release o=repo.radeon.com
|
||||
Pin-Priority: 600
|
||||
EOF
|
||||
|
||||
$STD apt update
|
||||
# apt update with retry — repo.radeon.com CDN can be mid-sync (transient size mismatches).
|
||||
# Run with ERR trap disabled so a transient failure does not abort the entire install.
|
||||
local _apt_ok=0
|
||||
for _attempt in 1 2 3; do
|
||||
if (
|
||||
set +e
|
||||
apt-get update -qq 2>&1
|
||||
exit $?
|
||||
) 2>/dev/null; then
|
||||
_apt_ok=1
|
||||
break
|
||||
fi
|
||||
msg_warn "apt update failed (attempt ${_attempt}/3) — AMD repo may be temporarily unavailable, retrying in 30s…"
|
||||
sleep 30
|
||||
done
|
||||
if [[ $_apt_ok -eq 0 ]]; then
|
||||
msg_warn "apt update still failing after 3 attempts — skipping ROCm install"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Install only runtime packages — full 'rocm' meta-package includes 15GB+ dev tools
|
||||
$STD apt install -y rocm-opencl-runtime rocm-hip-runtime rocm-smi-lib 2>/dev/null || {
|
||||
msg_warn "ROCm runtime install failed — trying minimal set"
|
||||
@@ -4724,7 +4873,7 @@ _setup_nvidia_gpu() {
|
||||
if [[ -n "$available_version" ]]; then
|
||||
msg_info "Found available NVIDIA version: ${available_version}"
|
||||
local nvidia_pkgs="libcuda1=${available_version} libnvcuvid1=${available_version} libnvidia-encode1=${available_version} libnvidia-ml1=${available_version}"
|
||||
if $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends "$nvidia_pkgs" 2>/dev/null; then
|
||||
if $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends $nvidia_pkgs 2>/dev/null; then
|
||||
msg_ok "Installed NVIDIA libraries (${available_version})"
|
||||
else
|
||||
msg_warn "Failed to install NVIDIA ${available_version} - trying unversioned"
|
||||
@@ -4788,7 +4937,7 @@ NVIDIA_PIN
|
||||
if [[ -n "$available_version" ]]; then
|
||||
msg_info "Installing NVIDIA libraries (version ${available_version})"
|
||||
local nvidia_pkgs="libcuda1=${available_version} libnvcuvid1=${available_version} libnvidia-encode1=${available_version} libnvidia-ml1=${available_version}"
|
||||
if $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends "$nvidia_pkgs" 2>/dev/null; then
|
||||
if $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends $nvidia_pkgs 2>/dev/null; then
|
||||
msg_ok "Installed version-matched NVIDIA libraries"
|
||||
else
|
||||
msg_warn "Version-pinned install failed - trying unpinned"
|
||||
@@ -4851,13 +5000,13 @@ NVIDIA_PIN
|
||||
if [[ -n "$available_version" ]]; then
|
||||
msg_info "Installing NVIDIA libraries (version ${available_version})"
|
||||
local nvidia_pkgs="libcuda1=${available_version} libnvcuvid1=${available_version} libnvidia-encode1=${available_version} libnvidia-ml1=${available_version}"
|
||||
if $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends "$nvidia_pkgs" 2>/dev/null; then
|
||||
if $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends $nvidia_pkgs 2>/dev/null; then
|
||||
msg_ok "Installed version-matched NVIDIA libraries"
|
||||
else
|
||||
# Fallback to Ubuntu repo packages with versioned nvidia-utils
|
||||
msg_warn "CUDA repo install failed - trying Ubuntu native packages (nvidia-utils-${nvidia_major_version})"
|
||||
if $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends \
|
||||
libnvidia-decode-"${nvidia_major_version}" libnvidia-encode-"${nvidia_major_version}" nvidia-utils-"${nvidia_major_version}" 2>/dev/null; then
|
||||
libnvidia-decode-${nvidia_major_version} libnvidia-encode-${nvidia_major_version} nvidia-utils-${nvidia_major_version} 2>/dev/null; then
|
||||
msg_ok "Installed Ubuntu NVIDIA packages (${nvidia_major_version})"
|
||||
else
|
||||
msg_warn "NVIDIA driver installation failed - please install manually: apt install nvidia-utils-${nvidia_major_version}"
|
||||
@@ -4868,7 +5017,7 @@ NVIDIA_PIN
|
||||
# Fallback to Ubuntu repo packages with versioned nvidia-utils
|
||||
msg_info "Trying Ubuntu native packages (nvidia-utils-${nvidia_major_version})"
|
||||
if $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends \
|
||||
libnvidia-decode-"${nvidia_major_version}" libnvidia-encode-"${nvidia_major_version}" nvidia-utils-"${nvidia_major_version}" 2>/dev/null; then
|
||||
libnvidia-decode-${nvidia_major_version} libnvidia-encode-${nvidia_major_version} nvidia-utils-${nvidia_major_version} 2>/dev/null; then
|
||||
msg_ok "Installed Ubuntu NVIDIA packages (${nvidia_major_version})"
|
||||
else
|
||||
msg_warn "NVIDIA driver installation failed - please install manually: apt install nvidia-utils-${nvidia_major_version}"
|
||||
@@ -5004,6 +5153,7 @@ EOF
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
_setup_gpu_permissions() {
|
||||
local in_ct="$1"
|
||||
local service_user="${2:-}"
|
||||
|
||||
# /dev/dri permissions (Intel/AMD)
|
||||
if [[ "$in_ct" == "0" && -d /dev/dri ]]; then
|
||||
@@ -5070,6 +5220,12 @@ _setup_gpu_permissions() {
|
||||
chmod 666 /dev/kfd 2>/dev/null || true
|
||||
msg_info "AMD ROCm compute device configured"
|
||||
fi
|
||||
|
||||
# Add service user to render and video groups for GPU hardware acceleration
|
||||
if [[ -n "$service_user" ]]; then
|
||||
usermod -aG render "$service_user" 2>/dev/null || true
|
||||
usermod -aG video "$service_user" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
@@ -5207,24 +5363,7 @@ function setup_java() {
|
||||
|
||||
# Get currently installed version
|
||||
local INSTALLED_VERSION=""
|
||||
if dpkg -l | grep -q "temurin-.*-jdk" 2>/dev/null; then
|
||||
INSTALLED_VERSION=$(dpkg -l 2>/dev/null | awk '/temurin-.*-jdk/{print $2}' | grep -oP 'temurin-\K[0-9]+' | head -n1 || echo "")
|
||||
fi
|
||||
|
||||
# Validate INSTALLED_VERSION is not empty if JDK package found
|
||||
local JDK_COUNT=0
|
||||
JDK_COUNT=$(dpkg -l 2>/dev/null | grep -c "temurin-.*-jdk" || true)
|
||||
if [[ -z "$INSTALLED_VERSION" && "${JDK_COUNT:-0}" -gt 0 ]]; then
|
||||
msg_warn "Found Temurin JDK but cannot determine version - attempting reinstall"
|
||||
# Try to get actual package name for purge
|
||||
local OLD_PACKAGE
|
||||
OLD_PACKAGE=$(dpkg -l 2>/dev/null | awk '/temurin-.*-jdk/{print $2}' | head -n1 || echo "")
|
||||
if [[ -n "$OLD_PACKAGE" ]]; then
|
||||
msg_info "Removing existing package: $OLD_PACKAGE"
|
||||
$STD apt purge -y "$OLD_PACKAGE" || true
|
||||
fi
|
||||
INSTALLED_VERSION="" # Reset to trigger fresh install
|
||||
fi
|
||||
INSTALLED_VERSION=$(dpkg-query -W -f '${Package}\n' 2>/dev/null | grep -oP '^temurin-\K[0-9]+(?=-jdk$)' | head -n1 || echo "")
|
||||
|
||||
# Scenario 1: Already at correct version
|
||||
if [[ "$INSTALLED_VERSION" == "$JAVA_VERSION" ]]; then
|
||||
@@ -5948,23 +6087,6 @@ function setup_mysql() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Debian 13+ Fix: MySQL repo has expired GPG key (as of 2025-10-22), use MariaDB
|
||||
# See: https://bugs.mysql.com/bug.php?id=119212
|
||||
# This check MUST be before prepare_repository_setup to avoid adding broken repo
|
||||
if [[ "$DISTRO_ID" == "debian" && "$DISTRO_CODENAME" =~ ^(trixie|forky|sid)$ ]]; then
|
||||
msg_info "Debian ${DISTRO_CODENAME} detected → using MariaDB (MySQL repo GPG key expired)"
|
||||
|
||||
# Use distro's MariaDB directly - no external repo needed
|
||||
install_packages_with_retry "mariadb-server" "mariadb-client" || {
|
||||
msg_error "Failed to install MariaDB on Debian ${DISTRO_CODENAME}"
|
||||
return 1
|
||||
}
|
||||
|
||||
cache_installed_version "mysql" "mariadb"
|
||||
msg_ok "Setup MariaDB (MySQL-compatible, Debian ${DISTRO_CODENAME})"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Scenario 2: Different version installed - clean upgrade
|
||||
if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" != "$MYSQL_VERSION" ]]; then
|
||||
msg_info "Upgrade MySQL from $CURRENT_VERSION to $MYSQL_VERSION"
|
||||
@@ -5979,6 +6101,47 @@ function setup_mysql() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# Debian 13+ Fix: MySQL 8.0 incompatible with libaio1t64, use 8.4 LTS
|
||||
if [[ "$DISTRO_ID" == "debian" && "$DISTRO_CODENAME" =~ ^(trixie|forky|sid)$ ]]; then
|
||||
msg_info "Debian ${DISTRO_CODENAME} detected → using MySQL 8.4 LTS (libaio1t64 compatible)"
|
||||
|
||||
if ! download_gpg_key "https://repo.mysql.com/RPM-GPG-KEY-mysql-2023" "/etc/apt/keyrings/mysql.gpg" "dearmor"; then
|
||||
msg_error "Failed to import MySQL GPG key"
|
||||
return 1
|
||||
fi
|
||||
|
||||
cat >/etc/apt/sources.list.d/mysql.sources <<EOF
|
||||
Types: deb
|
||||
URIs: https://repo.mysql.com/apt/debian/
|
||||
Suites: bookworm
|
||||
Components: mysql-8.4-lts
|
||||
Architectures: $(dpkg --print-architecture)
|
||||
Signed-By: /etc/apt/keyrings/mysql.gpg
|
||||
EOF
|
||||
|
||||
$STD apt update || {
|
||||
msg_error "Failed to update APT for MySQL 8.4 LTS"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Install with retry logic
|
||||
if ! install_packages_with_retry "mysql-community-server" "mysql-community-client"; then
|
||||
msg_warn "MySQL 8.4 LTS installation failed – falling back to MariaDB"
|
||||
cleanup_old_repo_files "mysql"
|
||||
$STD apt update
|
||||
install_packages_with_retry "mariadb-server" "mariadb-client" || {
|
||||
msg_error "Failed to install database engine (MySQL/MariaDB fallback)"
|
||||
return 1
|
||||
}
|
||||
msg_ok "Setup Database Engine (MariaDB fallback on Debian ${DISTRO_CODENAME})"
|
||||
return 0
|
||||
fi
|
||||
|
||||
cache_installed_version "mysql" "8.4"
|
||||
msg_ok "Setup MySQL 8.4 LTS (Debian ${DISTRO_CODENAME})"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Standard setup for other distributions
|
||||
local SUITE
|
||||
if [[ "$DISTRO_ID" == "debian" ]]; then
|
||||
@@ -5992,7 +6155,7 @@ function setup_mysql() {
|
||||
|
||||
# Setup repository
|
||||
manage_tool_repository "mysql" "$MYSQL_VERSION" "https://repo.mysql.com/apt/${DISTRO_ID}" \
|
||||
"https://repo.mysql.com/RPM-GPG-KEY-mysql-2025" || {
|
||||
"https://repo.mysql.com/RPM-GPG-KEY-mysql-2023" || {
|
||||
msg_error "Failed to setup MySQL repository"
|
||||
return 1
|
||||
}
|
||||
@@ -6415,8 +6578,8 @@ EOF
|
||||
msg_warn "php${PHP_VERSION}-fpm not available"
|
||||
fi
|
||||
# Create systemd override for PHP-FPM to fix runtime directory issues in LXC containers
|
||||
mkdir -p /etc/systemd/system/php"${PHP_VERSION}"-fpm.service.d/
|
||||
cat <<EOF >/etc/systemd/system/php"${PHP_VERSION}"-fpm.service.d/override.conf
|
||||
mkdir -p /etc/systemd/system/php${PHP_VERSION}-fpm.service.d/
|
||||
cat <<EOF >/etc/systemd/system/php${PHP_VERSION}-fpm.service.d/override.conf
|
||||
[Service]
|
||||
RuntimeDirectory=php
|
||||
RuntimeDirectoryMode=0755
|
||||
@@ -6442,7 +6605,7 @@ EOF
|
||||
msg_info "Installing PHP ${PHP_VERSION} packages"
|
||||
|
||||
# First attempt: Install all verified packages at once
|
||||
if ! $STD apt install -y "$MODULE_LIST" 2>/dev/null; then
|
||||
if ! $STD apt install -y $MODULE_LIST 2>/dev/null; then
|
||||
msg_warn "Bulk installation failed, attempting individual installation"
|
||||
|
||||
# Install main package first (critical)
|
||||
@@ -6496,8 +6659,8 @@ EOF
|
||||
# Enable and restart PHP-FPM if requested
|
||||
if [[ "$PHP_FPM" == "YES" ]]; then
|
||||
if systemctl list-unit-files | grep -q "php${PHP_VERSION}-fpm.service"; then
|
||||
$STD systemctl enable php"${PHP_VERSION}"-fpm
|
||||
safe_service_restart php"${PHP_VERSION}"-fpm
|
||||
$STD systemctl enable php${PHP_VERSION}-fpm
|
||||
safe_service_restart php${PHP_VERSION}-fpm
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -6528,22 +6691,38 @@ EOF
|
||||
# - Optionally uses official PGDG repository for specific versions
|
||||
# - Detects existing PostgreSQL version
|
||||
# - Dumps all databases before upgrade
|
||||
# - Installs optional PG_MODULES (e.g. postgis, contrib)
|
||||
# - Installs optional PG_MODULES (e.g. postgis, contrib, cron)
|
||||
# - Restores dumped data post-upgrade
|
||||
#
|
||||
# Variables:
|
||||
# USE_PGDG_REPO - Use official PGDG repository (default: true)
|
||||
# Set to "false" to use distro packages instead
|
||||
# PG_VERSION - Major PostgreSQL version (e.g. 15, 16) (default: 16)
|
||||
# PG_MODULES - Comma-separated list of modules (e.g. "postgis,contrib")
|
||||
# PG_MODULES - Comma-separated list of modules (e.g. "postgis,contrib,cron")
|
||||
#
|
||||
# Examples:
|
||||
# setup_postgresql # Uses PGDG repo, PG 16
|
||||
# PG_VERSION="17" setup_postgresql # Specific version from PGDG
|
||||
# USE_PGDG_REPO=false setup_postgresql # Uses distro package instead
|
||||
# setup_postgresql # Uses PGDG repo, PG 16
|
||||
# PG_VERSION="17" setup_postgresql # Specific version from PGDG
|
||||
# USE_PGDG_REPO=false setup_postgresql # Uses distro package instead
|
||||
# PG_VERSION="17" PG_MODULES="cron" setup_postgresql # With pg_cron module
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
function setup_postgresql() {
|
||||
# Internal helper: Configure shared_preload_libraries for pg_cron
|
||||
_configure_pg_cron_preload() {
|
||||
local modules="${1:-}"
|
||||
[[ -z "$modules" ]] && return 0
|
||||
if [[ ",$modules," == *",cron,"* ]]; then
|
||||
local current_libs
|
||||
current_libs=$(sudo -u postgres psql -tAc "SHOW shared_preload_libraries;" 2>/dev/null || echo "")
|
||||
if [[ "$current_libs" != *"pg_cron"* ]]; then
|
||||
local new_libs="${current_libs:+${current_libs},}pg_cron"
|
||||
$STD sudo -u postgres psql -c "ALTER SYSTEM SET shared_preload_libraries = '${new_libs}';"
|
||||
$STD systemctl restart postgresql
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
setup_postgresql() {
|
||||
local PG_VERSION="${PG_VERSION:-16}"
|
||||
local PG_MODULES="${PG_MODULES:-}"
|
||||
local USE_PGDG_REPO="${USE_PGDG_REPO:-true}"
|
||||
@@ -6581,6 +6760,7 @@ function setup_postgresql() {
|
||||
$STD apt install -y "postgresql-${CURRENT_PG_VERSION}-${module}" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
_configure_pg_cron_preload "$PG_MODULES"
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -6616,6 +6796,7 @@ function setup_postgresql() {
|
||||
$STD apt install -y "postgresql-${INSTALLED_VERSION}-${module}" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
_configure_pg_cron_preload "$PG_MODULES"
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -6637,6 +6818,7 @@ function setup_postgresql() {
|
||||
$STD apt install -y "postgresql-${PG_VERSION}-${module}" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
_configure_pg_cron_preload "$PG_MODULES"
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -6664,13 +6846,16 @@ function setup_postgresql() {
|
||||
local SUITE
|
||||
case "$DISTRO_CODENAME" in
|
||||
trixie | forky | sid)
|
||||
|
||||
if verify_repo_available "https://apt.postgresql.org/pub/repos/apt" "trixie-pgdg"; then
|
||||
SUITE="trixie-pgdg"
|
||||
|
||||
else
|
||||
msg_warn "PGDG repo not available for ${DISTRO_CODENAME}, falling back to distro packages"
|
||||
USE_PGDG_REPO=false setup_postgresql
|
||||
return $?
|
||||
fi
|
||||
|
||||
;;
|
||||
*)
|
||||
SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://apt.postgresql.org/pub/repos/apt")
|
||||
@@ -6754,6 +6939,7 @@ function setup_postgresql() {
|
||||
}
|
||||
done
|
||||
fi
|
||||
_configure_pg_cron_preload "$PG_MODULES"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
@@ -6772,6 +6958,7 @@ function setup_postgresql() {
|
||||
# PG_DB_NAME="immich" PG_DB_USER="immich" PG_DB_EXTENSIONS="pgvector" setup_postgresql_db
|
||||
# PG_DB_NAME="ghostfolio" PG_DB_USER="ghostfolio" PG_DB_GRANT_SUPERUSER="true" setup_postgresql_db
|
||||
# PG_DB_NAME="adventurelog" PG_DB_USER="adventurelog" PG_DB_EXTENSIONS="postgis" setup_postgresql_db
|
||||
# PG_DB_NAME="splitpro" PG_DB_USER="splitpro" PG_DB_EXTENSIONS="pg_cron" setup_postgresql_db
|
||||
#
|
||||
# Variables:
|
||||
# PG_DB_NAME - Database name (required)
|
||||
@@ -6803,6 +6990,13 @@ function setup_postgresql_db() {
|
||||
$STD sudo -u postgres psql -c "CREATE ROLE $PG_DB_USER WITH LOGIN PASSWORD '$PG_DB_PASS';"
|
||||
$STD sudo -u postgres psql -c "CREATE DATABASE $PG_DB_NAME WITH OWNER $PG_DB_USER ENCODING 'UTF8' TEMPLATE template0;"
|
||||
|
||||
# Configure pg_cron database BEFORE creating the extension (must be set before pg_cron loads)
|
||||
if [[ -n "${PG_DB_EXTENSIONS:-}" ]] && [[ ",${PG_DB_EXTENSIONS//[[:space:]]/}," == *",pg_cron,"* ]]; then
|
||||
$STD sudo -u postgres psql -c "ALTER SYSTEM SET cron.database_name = '${PG_DB_NAME}';"
|
||||
$STD sudo -u postgres psql -c "ALTER SYSTEM SET cron.timezone = 'UTC';"
|
||||
$STD systemctl restart postgresql
|
||||
fi
|
||||
|
||||
# Install extensions (comma-separated)
|
||||
if [[ -n "${PG_DB_EXTENSIONS:-}" ]]; then
|
||||
IFS=',' read -ra EXT_LIST <<<"${PG_DB_EXTENSIONS:-}"
|
||||
@@ -6812,6 +7006,12 @@ function setup_postgresql_db() {
|
||||
done
|
||||
fi
|
||||
|
||||
# Grant pg_cron schema permissions to DB user
|
||||
if [[ -n "${PG_DB_EXTENSIONS:-}" ]] && [[ ",${PG_DB_EXTENSIONS//[[:space:]]/}," == *",pg_cron,"* ]]; then
|
||||
$STD sudo -u postgres psql -d "$PG_DB_NAME" -c "GRANT USAGE ON SCHEMA cron TO ${PG_DB_USER};"
|
||||
$STD sudo -u postgres psql -d "$PG_DB_NAME" -c "GRANT ALL ON ALL TABLES IN SCHEMA cron TO ${PG_DB_USER};"
|
||||
fi
|
||||
|
||||
# ALTER ROLE settings for Django/Rails compatibility (unless skipped)
|
||||
if [[ "${PG_DB_SKIP_ALTER_ROLE:-}" != "true" ]]; then
|
||||
$STD sudo -u postgres psql -c "ALTER ROLE $PG_DB_USER SET client_encoding TO 'utf8';"
|
||||
@@ -6853,7 +7053,6 @@ function setup_postgresql_db() {
|
||||
export PG_DB_USER
|
||||
export PG_DB_PASS
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Installs rbenv and ruby-build, installs Ruby and optionally Rails.
|
||||
#
|
||||
|
||||
+6
@@ -420,6 +420,12 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
||||
|
||||
</details>
|
||||
|
||||
## 2026-03-20
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- 🔄 Upstream Sync - 2026-03-20 [@BillyOutlast](https://github.com/BillyOutlast) ([#74](https://github.com/Heretek-AI/ProxmoxVE/pull/74))
|
||||
|
||||
## 2026-03-19
|
||||
|
||||
### 📂 Github
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generated": "2026-03-19T06:41:06Z",
|
||||
"generated": "2026-03-19T18:38:18Z",
|
||||
"versions": [
|
||||
{
|
||||
"slug": "agregarr",
|
||||
@@ -18,9 +18,9 @@
|
||||
{
|
||||
"slug": "llamacpp",
|
||||
"repo": "ggml-org/llama.cpp",
|
||||
"version": "b8417",
|
||||
"version": "b8429",
|
||||
"pinned": false,
|
||||
"date": "2026-03-19T05:37:13Z"
|
||||
"date": "2026-03-19T14:00:45Z"
|
||||
},
|
||||
{
|
||||
"slug": "localai",
|
||||
@@ -54,7 +54,7 @@
|
||||
"slug": "ragflow",
|
||||
"repo": "infiniflow/ragflow",
|
||||
"version": "v0.24.0",
|
||||
"pinned": true,
|
||||
"pinned": false,
|
||||
"date": "2026-02-10T09:27:14Z"
|
||||
},
|
||||
{
|
||||
|
||||
+675
@@ -0,0 +1,675 @@
|
||||
name: PocketBase Bot
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pocketbase-bot:
|
||||
runs-on: self-hosted
|
||||
|
||||
# Only act on /pocketbase commands
|
||||
if: startsWith(github.event.comment.body, '/pocketbase')
|
||||
|
||||
steps:
|
||||
- name: Execute PocketBase bot command
|
||||
env:
|
||||
POCKETBASE_URL: ${{ secrets.POCKETBASE_URL }}
|
||||
POCKETBASE_COLLECTION: ${{ secrets.POCKETBASE_COLLECTION }}
|
||||
POCKETBASE_ADMIN_EMAIL: ${{ secrets.POCKETBASE_ADMIN_EMAIL }}
|
||||
POCKETBASE_ADMIN_PASSWORD: ${{ secrets.POCKETBASE_ADMIN_PASSWORD }}
|
||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
COMMENT_ID: ${{ github.event.comment.id }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
REPO_OWNER: ${{ github.repository_owner }}
|
||||
REPO_NAME: ${{ github.event.repository.name }}
|
||||
ACTOR: ${{ github.event.comment.user.login }}
|
||||
ACTOR_ASSOCIATION: ${{ github.event.comment.author_association }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
node << 'ENDSCRIPT'
|
||||
(async function () {
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const url = require('url');
|
||||
|
||||
// ── HTTP helper with redirect following ────────────────────────────
|
||||
function request(fullUrl, opts, redirectCount) {
|
||||
redirectCount = redirectCount || 0;
|
||||
return new Promise(function (resolve, reject) {
|
||||
const u = url.parse(fullUrl);
|
||||
const isHttps = u.protocol === 'https:';
|
||||
const body = opts.body;
|
||||
const options = {
|
||||
hostname: u.hostname,
|
||||
port: u.port || (isHttps ? 443 : 80),
|
||||
path: u.path,
|
||||
method: opts.method || 'GET',
|
||||
headers: opts.headers || {}
|
||||
};
|
||||
if (body) options.headers['Content-Length'] = Buffer.byteLength(body);
|
||||
const lib = isHttps ? https : http;
|
||||
const req = lib.request(options, function (res) {
|
||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||
if (redirectCount >= 5) return reject(new Error('Too many redirects from ' + fullUrl));
|
||||
const redirectUrl = url.resolve(fullUrl, res.headers.location);
|
||||
res.resume();
|
||||
resolve(request(redirectUrl, opts, redirectCount + 1));
|
||||
return;
|
||||
}
|
||||
let data = '';
|
||||
res.on('data', function (chunk) { data += chunk; });
|
||||
res.on('end', function () {
|
||||
resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, statusCode: res.statusCode, body: data });
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
if (body) req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ── GitHub API helpers ─────────────────────────────────────────────
|
||||
const owner = process.env.REPO_OWNER;
|
||||
const repo = process.env.REPO_NAME;
|
||||
const issueNumber = parseInt(process.env.ISSUE_NUMBER, 10);
|
||||
const commentId = parseInt(process.env.COMMENT_ID, 10);
|
||||
const actor = process.env.ACTOR;
|
||||
|
||||
function ghRequest(path, method, body) {
|
||||
const headers = {
|
||||
'Authorization': 'Bearer ' + process.env.GITHUB_TOKEN,
|
||||
'Accept': 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
'User-Agent': 'PocketBase-Bot'
|
||||
};
|
||||
const bodyStr = body ? JSON.stringify(body) : undefined;
|
||||
if (bodyStr) headers['Content-Type'] = 'application/json';
|
||||
return request('https://api.github.com' + path, { method: method || 'GET', headers, body: bodyStr });
|
||||
}
|
||||
|
||||
async function addReaction(content) {
|
||||
try {
|
||||
await ghRequest(
|
||||
'/repos/' + owner + '/' + repo + '/issues/comments/' + commentId + '/reactions',
|
||||
'POST', { content }
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn('Could not add reaction:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function postComment(text) {
|
||||
const res = await ghRequest(
|
||||
'/repos/' + owner + '/' + repo + '/issues/' + issueNumber + '/comments',
|
||||
'POST', { body: text }
|
||||
);
|
||||
if (!res.ok) console.warn('Could not post comment:', res.body);
|
||||
}
|
||||
|
||||
// ── Permission check ───────────────────────────────────────────────
|
||||
// author_association: OWNER = repo/org owner, MEMBER = org member (includes Contributors team)
|
||||
const association = process.env.ACTOR_ASSOCIATION;
|
||||
if (association !== 'OWNER' && association !== 'MEMBER') {
|
||||
await addReaction('-1');
|
||||
await postComment(
|
||||
'❌ **PocketBase Bot**: @' + actor + ' is not authorized to use this command.\n' +
|
||||
'Only org members (Contributors team) can use `/pocketbase`.'
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// ── Acknowledge ────────────────────────────────────────────────────
|
||||
await addReaction('eyes');
|
||||
|
||||
// ── Parse command ──────────────────────────────────────────────────
|
||||
// Formats (first line of comment):
|
||||
// /pocketbase <slug> field=value [field=value ...] ← field updates (simple values)
|
||||
// /pocketbase <slug> set <field> ← value from code block below
|
||||
// /pocketbase <slug> note list|add|edit|remove ... ← note management
|
||||
// /pocketbase <slug> method list ← list install methods
|
||||
// /pocketbase <slug> method <type> cpu=N ram=N hdd=N ← edit install method resources
|
||||
const commentBody = process.env.COMMENT_BODY || '';
|
||||
const lines = commentBody.trim().split('\n');
|
||||
const firstLine = lines[0].trim();
|
||||
const withoutCmd = firstLine.replace(/^\/pocketbase\s+/, '').trim();
|
||||
|
||||
// Extract code block content from comment body (```...``` or ```lang\n...```)
|
||||
function extractCodeBlock(body) {
|
||||
const m = body.match(/```[^\n]*\n([\s\S]*?)```/);
|
||||
return m ? m[1].trim() : null;
|
||||
}
|
||||
const codeBlockValue = extractCodeBlock(commentBody);
|
||||
|
||||
const HELP_TEXT =
|
||||
'**Field update (simple):** `/pocketbase <slug> field=value [field=value ...]`\n\n' +
|
||||
'**Field update (HTML/multiline) — value from code block:**\n' +
|
||||
'````\n' +
|
||||
'/pocketbase <slug> set description\n' +
|
||||
'```html\n' +
|
||||
'<p>Your <b>HTML</b> or multi-line content here</p>\n' +
|
||||
'```\n' +
|
||||
'````\n\n' +
|
||||
'**Note management:**\n' +
|
||||
'```\n' +
|
||||
'/pocketbase <slug> note list\n' +
|
||||
'/pocketbase <slug> note add <type> "<text>"\n' +
|
||||
'/pocketbase <slug> note edit <type> "<old text>" "<new text>"\n' +
|
||||
'/pocketbase <slug> note remove <type> "<text>"\n' +
|
||||
'```\n\n' +
|
||||
'**Install method resources:**\n' +
|
||||
'```\n' +
|
||||
'/pocketbase <slug> method list\n' +
|
||||
'/pocketbase <slug> method <type> hdd=10\n' +
|
||||
'/pocketbase <slug> method <type> cpu=4 ram=2048 hdd=20\n' +
|
||||
'```\n\n' +
|
||||
'**Editable fields:** `name` `description` `logo` `documentation` `website` `project_url` `github` ' +
|
||||
'`config_path` `port` `default_user` `default_passwd` ' +
|
||||
'`updateable` `privileged` `has_arm` `is_dev` ' +
|
||||
'`is_disabled` `disable_message` `is_deleted` `deleted_message`';
|
||||
|
||||
if (!withoutCmd) {
|
||||
await addReaction('-1');
|
||||
await postComment('❌ **PocketBase Bot**: No slug or command specified.\n\n' + HELP_TEXT);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const spaceIdx = withoutCmd.indexOf(' ');
|
||||
const slug = (spaceIdx === -1 ? withoutCmd : withoutCmd.substring(0, spaceIdx)).trim();
|
||||
const rest = spaceIdx === -1 ? '' : withoutCmd.substring(spaceIdx + 1).trim();
|
||||
|
||||
if (!rest) {
|
||||
await addReaction('-1');
|
||||
await postComment('❌ **PocketBase Bot**: No command specified for slug `' + slug + '`.\n\n' + HELP_TEXT);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// ── Allowed fields and their types ─────────────────────────────────
|
||||
// ── PocketBase: authenticate (shared by all paths) ─────────────────
|
||||
const raw = process.env.POCKETBASE_URL.replace(/\/$/, '');
|
||||
const apiBase = /\/api$/i.test(raw) ? raw : raw + '/api';
|
||||
const coll = process.env.POCKETBASE_COLLECTION;
|
||||
|
||||
const authRes = await request(apiBase + '/collections/users/auth-with-password', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
identity: process.env.POCKETBASE_ADMIN_EMAIL,
|
||||
password: process.env.POCKETBASE_ADMIN_PASSWORD
|
||||
})
|
||||
});
|
||||
if (!authRes.ok) {
|
||||
await addReaction('-1');
|
||||
await postComment('❌ **PocketBase Bot**: PocketBase authentication failed. CC @' + owner + '/maintainers');
|
||||
process.exit(1);
|
||||
}
|
||||
const token = JSON.parse(authRes.body).token;
|
||||
|
||||
// ── PocketBase: find record by slug (shared by all paths) ──────────
|
||||
const recordsUrl = apiBase + '/collections/' + encodeURIComponent(coll) + '/records';
|
||||
const filter = "(slug='" + slug.replace(/'/g, "''") + "')";
|
||||
const listRes = await request(recordsUrl + '?filter=' + encodeURIComponent(filter) + '&perPage=1', {
|
||||
headers: { 'Authorization': token }
|
||||
});
|
||||
const list = JSON.parse(listRes.body);
|
||||
const record = list.items && list.items[0];
|
||||
|
||||
if (!record) {
|
||||
await addReaction('-1');
|
||||
await postComment(
|
||||
'❌ **PocketBase Bot**: No record found for slug `' + slug + '`.\n\n' +
|
||||
'Make sure the script was already pushed to PocketBase (JSON must exist and have been synced).'
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// ── Route: dispatch to subcommand handler ──────────────────────────
|
||||
const noteMatch = rest.match(/^note\s+(list|add|edit|remove)\b/i);
|
||||
const methodMatch = rest.match(/^method\b/i);
|
||||
const setMatch = rest.match(/^set\s+(\S+)/i);
|
||||
|
||||
if (noteMatch) {
|
||||
// ── NOTE SUBCOMMAND (reads/writes notes_json on script record) ────
|
||||
const noteAction = noteMatch[1].toLowerCase();
|
||||
const noteArgsStr = rest.substring(noteMatch[0].length).trim();
|
||||
|
||||
// Parse notes_json from the already-fetched script record
|
||||
// PocketBase may return JSON fields as already-parsed objects
|
||||
let notesArr = [];
|
||||
try {
|
||||
const rawNotes = record.notes_json;
|
||||
notesArr = Array.isArray(rawNotes) ? rawNotes : JSON.parse(rawNotes || '[]');
|
||||
} catch (e) { notesArr = []; }
|
||||
|
||||
// Token parser: unquoted-word OR "quoted string" (supports \" escapes)
|
||||
function parseNoteTokens(str) {
|
||||
const tokens = [];
|
||||
let pos = 0;
|
||||
while (pos < str.length) {
|
||||
while (pos < str.length && /\s/.test(str[pos])) pos++;
|
||||
if (pos >= str.length) break;
|
||||
if (str[pos] === '"') {
|
||||
pos++;
|
||||
let start = pos;
|
||||
while (pos < str.length && str[pos] !== '"') {
|
||||
if (str[pos] === '\\') pos++;
|
||||
pos++;
|
||||
}
|
||||
tokens.push(str.substring(start, pos).replace(/\\"/g, '"'));
|
||||
if (pos < str.length) pos++;
|
||||
} else {
|
||||
let start = pos;
|
||||
while (pos < str.length && !/\s/.test(str[pos])) pos++;
|
||||
tokens.push(str.substring(start, pos));
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function formatNotesList(arr) {
|
||||
if (arr.length === 0) return '*None*';
|
||||
return arr.map(function (n, i) {
|
||||
return (i + 1) + '. **`' + (n.type || '?') + '`**: ' + (n.text || '');
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
async function patchNotesJson(arr) {
|
||||
const res = await request(recordsUrl + '/' + record.id, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ notes_json: JSON.stringify(arr) })
|
||||
});
|
||||
if (!res.ok) {
|
||||
await addReaction('-1');
|
||||
await postComment('❌ **PocketBase Bot**: Failed to update `notes_json`:\n```\n' + res.body + '\n```');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (noteAction === 'list') {
|
||||
await addReaction('+1');
|
||||
await postComment(
|
||||
'ℹ️ **PocketBase Bot**: Notes for **`' + slug + '`** (' + notesArr.length + ' total)\n\n' +
|
||||
formatNotesList(notesArr)
|
||||
);
|
||||
|
||||
} else if (noteAction === 'add') {
|
||||
const tokens = parseNoteTokens(noteArgsStr);
|
||||
if (tokens.length < 2) {
|
||||
await addReaction('-1');
|
||||
await postComment(
|
||||
'❌ **PocketBase Bot**: `note add` requires `<type>` and `"<text>"`.\n\n' +
|
||||
'**Usage:** `/pocketbase ' + slug + ' note add <type> "<text>"`'
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
const noteType = tokens[0].toLowerCase();
|
||||
const noteText = tokens.slice(1).join(' ');
|
||||
notesArr.push({ type: noteType, text: noteText });
|
||||
await patchNotesJson(notesArr);
|
||||
await addReaction('+1');
|
||||
await postComment(
|
||||
'✅ **PocketBase Bot**: Added note to **`' + slug + '`**\n\n' +
|
||||
'- **Type:** `' + noteType + '`\n' +
|
||||
'- **Text:** ' + noteText + '\n\n' +
|
||||
'*Executed by @' + actor + '*'
|
||||
);
|
||||
|
||||
} else if (noteAction === 'edit') {
|
||||
const tokens = parseNoteTokens(noteArgsStr);
|
||||
if (tokens.length < 3) {
|
||||
await addReaction('-1');
|
||||
await postComment(
|
||||
'❌ **PocketBase Bot**: `note edit` requires `<type>`, `"<old text>"`, and `"<new text>"`.\n\n' +
|
||||
'**Usage:** `/pocketbase ' + slug + ' note edit <type> "<old text>" "<new text>"`\n\n' +
|
||||
'Use `/pocketbase ' + slug + ' note list` to see current notes.'
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
const noteType = tokens[0].toLowerCase();
|
||||
const oldText = tokens[1];
|
||||
const newText = tokens[2];
|
||||
const idx = notesArr.findIndex(function (n) {
|
||||
return n.type.toLowerCase() === noteType && n.text === oldText;
|
||||
});
|
||||
if (idx === -1) {
|
||||
await addReaction('-1');
|
||||
await postComment(
|
||||
'❌ **PocketBase Bot**: No `' + noteType + '` note found with that exact text.\n\n' +
|
||||
'**Current notes for `' + slug + '`:**\n' + formatNotesList(notesArr)
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
notesArr[idx].text = newText;
|
||||
await patchNotesJson(notesArr);
|
||||
await addReaction('+1');
|
||||
await postComment(
|
||||
'✅ **PocketBase Bot**: Edited note in **`' + slug + '`**\n\n' +
|
||||
'- **Type:** `' + noteType + '`\n' +
|
||||
'- **Old:** ' + oldText + '\n' +
|
||||
'- **New:** ' + newText + '\n\n' +
|
||||
'*Executed by @' + actor + '*'
|
||||
);
|
||||
|
||||
} else if (noteAction === 'remove') {
|
||||
const tokens = parseNoteTokens(noteArgsStr);
|
||||
if (tokens.length < 2) {
|
||||
await addReaction('-1');
|
||||
await postComment(
|
||||
'❌ **PocketBase Bot**: `note remove` requires `<type>` and `"<text>"`.\n\n' +
|
||||
'**Usage:** `/pocketbase ' + slug + ' note remove <type> "<text>"`\n\n' +
|
||||
'Use `/pocketbase ' + slug + ' note list` to see current notes.'
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
const noteType = tokens[0].toLowerCase();
|
||||
const noteText = tokens[1];
|
||||
const before = notesArr.length;
|
||||
notesArr = notesArr.filter(function (n) {
|
||||
return !(n.type.toLowerCase() === noteType && n.text === noteText);
|
||||
});
|
||||
if (notesArr.length === before) {
|
||||
await addReaction('-1');
|
||||
await postComment(
|
||||
'❌ **PocketBase Bot**: No `' + noteType + '` note found with that exact text.\n\n' +
|
||||
'**Current notes for `' + slug + '`:**\n' + formatNotesList(notesArr)
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
await patchNotesJson(notesArr);
|
||||
await addReaction('+1');
|
||||
await postComment(
|
||||
'✅ **PocketBase Bot**: Removed note from **`' + slug + '`**\n\n' +
|
||||
'- **Type:** `' + noteType + '`\n' +
|
||||
'- **Text:** ' + noteText + '\n\n' +
|
||||
'*Executed by @' + actor + '*'
|
||||
);
|
||||
}
|
||||
|
||||
} else if (methodMatch) {
|
||||
// ── METHOD SUBCOMMAND (reads/writes install_methods_json on script record) ──
|
||||
const methodArgs = rest.replace(/^method\s*/i, '').trim();
|
||||
const methodListMode = !methodArgs || methodArgs.toLowerCase() === 'list';
|
||||
|
||||
// Parse install_methods_json from the already-fetched script record
|
||||
// PocketBase may return JSON fields as already-parsed objects
|
||||
let methodsArr = [];
|
||||
try {
|
||||
const rawMethods = record.install_methods_json;
|
||||
methodsArr = Array.isArray(rawMethods) ? rawMethods : JSON.parse(rawMethods || '[]');
|
||||
} catch (e) { methodsArr = []; }
|
||||
|
||||
function formatMethodsList(arr) {
|
||||
if (arr.length === 0) return '*None*';
|
||||
return arr.map(function (im, i) {
|
||||
const r = im.resources || {};
|
||||
return (i + 1) + '. **`' + (im.type || '?') + '`** — CPU: `' + (r.cpu != null ? r.cpu : '?') +
|
||||
'` · RAM: `' + (r.ram != null ? r.ram : '?') + ' MB` · HDD: `' + (r.hdd != null ? r.hdd : '?') + ' GB`';
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
async function patchInstallMethodsJson(arr) {
|
||||
const res = await request(recordsUrl + '/' + record.id, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ install_methods_json: JSON.stringify(arr) })
|
||||
});
|
||||
if (!res.ok) {
|
||||
await addReaction('-1');
|
||||
await postComment('❌ **PocketBase Bot**: Failed to update `install_methods_json`:\n```\n' + res.body + '\n```');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (methodListMode) {
|
||||
await addReaction('+1');
|
||||
await postComment(
|
||||
'ℹ️ **PocketBase Bot**: Install methods for **`' + slug + '`** (' + methodsArr.length + ' total)\n\n' +
|
||||
formatMethodsList(methodsArr)
|
||||
);
|
||||
} else {
|
||||
// Parse: <type> cpu=N ram=N hdd=N
|
||||
const methodParts = methodArgs.match(/^(\S+)\s+(.+)$/);
|
||||
if (!methodParts) {
|
||||
await addReaction('-1');
|
||||
await postComment(
|
||||
'❌ **PocketBase Bot**: Invalid `method` syntax.\n\n' +
|
||||
'**Usage:**\n```\n/pocketbase ' + slug + ' method list\n/pocketbase ' + slug + ' method <type> hdd=10\n/pocketbase ' + slug + ' method <type> cpu=4 ram=2048 hdd=20\n```'
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
const targetType = methodParts[1].toLowerCase();
|
||||
const resourcesStr = methodParts[2];
|
||||
|
||||
// Parse resource fields (only cpu/ram/hdd allowed)
|
||||
const RESOURCE_FIELDS = { cpu: true, ram: true, hdd: true };
|
||||
const resourceChanges = {};
|
||||
const rePairs = /([a-z]+)=(\d+)/gi;
|
||||
let m;
|
||||
while ((m = rePairs.exec(resourcesStr)) !== null) {
|
||||
const key = m[1].toLowerCase();
|
||||
if (RESOURCE_FIELDS[key]) resourceChanges[key] = parseInt(m[2], 10);
|
||||
}
|
||||
if (Object.keys(resourceChanges).length === 0) {
|
||||
await addReaction('-1');
|
||||
await postComment('❌ **PocketBase Bot**: No valid resource fields found. Use `cpu=N`, `ram=N`, `hdd=N`.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Find matching method by type name (case-insensitive)
|
||||
const idx = methodsArr.findIndex(function (im) {
|
||||
return (im.type || '').toLowerCase() === targetType;
|
||||
});
|
||||
if (idx === -1) {
|
||||
await addReaction('-1');
|
||||
const availableTypes = methodsArr.map(function (im) { return im.type || '?'; });
|
||||
await postComment(
|
||||
'❌ **PocketBase Bot**: No install method with type `' + targetType + '` found for `' + slug + '`.\n\n' +
|
||||
'**Available types:** `' + (availableTypes.length ? availableTypes.join('`, `') : '(none)') + '`\n\n' +
|
||||
'Use `/pocketbase ' + slug + ' method list` to see all methods.'
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!methodsArr[idx].resources) methodsArr[idx].resources = {};
|
||||
if (resourceChanges.cpu != null) methodsArr[idx].resources.cpu = resourceChanges.cpu;
|
||||
if (resourceChanges.ram != null) methodsArr[idx].resources.ram = resourceChanges.ram;
|
||||
if (resourceChanges.hdd != null) methodsArr[idx].resources.hdd = resourceChanges.hdd;
|
||||
|
||||
await patchInstallMethodsJson(methodsArr);
|
||||
|
||||
const changesLines = Object.entries(resourceChanges)
|
||||
.map(function ([k, v]) { return '- `' + k + '` → `' + v + (k === 'ram' ? ' MB' : k === 'hdd' ? ' GB' : '') + '`'; })
|
||||
.join('\n');
|
||||
await addReaction('+1');
|
||||
await postComment(
|
||||
'✅ **PocketBase Bot**: Updated install method **`' + methodsArr[idx].type + '`** for **`' + slug + '`**\n\n' +
|
||||
'**Changes applied:**\n' + changesLines + '\n\n' +
|
||||
'*Executed by @' + actor + '*'
|
||||
);
|
||||
}
|
||||
|
||||
} else if (setMatch) {
|
||||
// ── SET SUBCOMMAND (multi-line / HTML / special chars via code block) ──
|
||||
const fieldName = setMatch[1].toLowerCase();
|
||||
const SET_ALLOWED = {
|
||||
name: 'string', description: 'string', logo: 'string',
|
||||
documentation: 'string', website: 'string', project_url: 'string', github: 'string',
|
||||
config_path: 'string', disable_message: 'string', deleted_message: 'string'
|
||||
};
|
||||
if (!SET_ALLOWED[fieldName]) {
|
||||
await addReaction('-1');
|
||||
await postComment(
|
||||
'❌ **PocketBase Bot**: `set` only supports text fields.\n\n' +
|
||||
'**Allowed:** `' + Object.keys(SET_ALLOWED).join('`, `') + '`\n\n' +
|
||||
'For boolean/number fields use `field=value` syntax instead.'
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
if (!codeBlockValue) {
|
||||
await addReaction('-1');
|
||||
await postComment(
|
||||
'❌ **PocketBase Bot**: `set` requires a code block with the value.\n\n' +
|
||||
'**Usage:**\n````\n/pocketbase ' + slug + ' set ' + fieldName + '\n```\nYour content here (HTML, multiline, special chars all fine)\n```\n````'
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
const setPayload = {};
|
||||
setPayload[fieldName] = codeBlockValue;
|
||||
const setPatchRes = await request(recordsUrl + '/' + record.id, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(setPayload)
|
||||
});
|
||||
if (!setPatchRes.ok) {
|
||||
await addReaction('-1');
|
||||
await postComment('❌ **PocketBase Bot**: PATCH failed for `' + slug + '`:\n```\n' + setPatchRes.body + '\n```');
|
||||
process.exit(1);
|
||||
}
|
||||
const preview = codeBlockValue.length > 300 ? codeBlockValue.substring(0, 300) + '…' : codeBlockValue;
|
||||
await addReaction('+1');
|
||||
await postComment(
|
||||
'✅ **PocketBase Bot**: Set `' + fieldName + '` for **`' + slug + '`**\n\n' +
|
||||
'**Value set:**\n```\n' + preview + '\n```\n\n' +
|
||||
'*Executed by @' + actor + '*'
|
||||
);
|
||||
|
||||
} else {
|
||||
// ── FIELD=VALUE PATH ─────────────────────────────────────────────
|
||||
const fieldsStr = rest;
|
||||
|
||||
// Skipped: slug, script_created/updated, created (auto), categories/
|
||||
// install_methods/notes/type (relations), github_data/install_methods_json/
|
||||
// notes_json (auto-generated), execute_in (select relation), last_update_commit (auto)
|
||||
const ALLOWED_FIELDS = {
|
||||
name: 'string',
|
||||
description: 'string',
|
||||
logo: 'string',
|
||||
documentation: 'string',
|
||||
website: 'string',
|
||||
project_url: 'string',
|
||||
github: 'string',
|
||||
config_path: 'string',
|
||||
port: 'number',
|
||||
default_user: 'nullable_string',
|
||||
default_passwd: 'nullable_string',
|
||||
updateable: 'boolean',
|
||||
privileged: 'boolean',
|
||||
has_arm: 'boolean',
|
||||
is_dev: 'boolean',
|
||||
is_disabled: 'boolean',
|
||||
disable_message: 'string',
|
||||
is_deleted: 'boolean',
|
||||
deleted_message: 'string',
|
||||
};
|
||||
|
||||
// Field=value parser (handles quoted values and empty=null)
|
||||
function parseFields(str) {
|
||||
const fields = {};
|
||||
let pos = 0;
|
||||
while (pos < str.length) {
|
||||
while (pos < str.length && /\s/.test(str[pos])) pos++;
|
||||
if (pos >= str.length) break;
|
||||
let keyStart = pos;
|
||||
while (pos < str.length && str[pos] !== '=' && !/\s/.test(str[pos])) pos++;
|
||||
const key = str.substring(keyStart, pos).trim();
|
||||
if (!key || pos >= str.length || str[pos] !== '=') { pos++; continue; }
|
||||
pos++;
|
||||
let value;
|
||||
if (str[pos] === '"') {
|
||||
pos++;
|
||||
let valStart = pos;
|
||||
while (pos < str.length && str[pos] !== '"') {
|
||||
if (str[pos] === '\\') pos++;
|
||||
pos++;
|
||||
}
|
||||
value = str.substring(valStart, pos).replace(/\\"/g, '"');
|
||||
if (pos < str.length) pos++;
|
||||
} else {
|
||||
let valStart = pos;
|
||||
while (pos < str.length && !/\s/.test(str[pos])) pos++;
|
||||
value = str.substring(valStart, pos);
|
||||
}
|
||||
fields[key] = value;
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
const parsedFields = parseFields(fieldsStr);
|
||||
|
||||
const unknownFields = Object.keys(parsedFields).filter(function (f) { return !ALLOWED_FIELDS[f]; });
|
||||
if (unknownFields.length > 0) {
|
||||
await addReaction('-1');
|
||||
await postComment(
|
||||
'❌ **PocketBase Bot**: Unknown field(s): `' + unknownFields.join('`, `') + '`\n\n' +
|
||||
'**Allowed fields:** `' + Object.keys(ALLOWED_FIELDS).join('`, `') + '`'
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (Object.keys(parsedFields).length === 0) {
|
||||
await addReaction('-1');
|
||||
await postComment('❌ **PocketBase Bot**: Could not parse any valid `field=value` pairs.\n\n' + HELP_TEXT);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Cast values to correct types
|
||||
const payload = {};
|
||||
for (const [key, rawVal] of Object.entries(parsedFields)) {
|
||||
const type = ALLOWED_FIELDS[key];
|
||||
if (type === 'boolean') {
|
||||
if (rawVal === 'true') payload[key] = true;
|
||||
else if (rawVal === 'false') payload[key] = false;
|
||||
else {
|
||||
await addReaction('-1');
|
||||
await postComment('❌ **PocketBase Bot**: `' + key + '` must be `true` or `false`, got: `' + rawVal + '`');
|
||||
process.exit(0);
|
||||
}
|
||||
} else if (type === 'number') {
|
||||
const n = parseInt(rawVal, 10);
|
||||
if (isNaN(n)) {
|
||||
await addReaction('-1');
|
||||
await postComment('❌ **PocketBase Bot**: `' + key + '` must be a number, got: `' + rawVal + '`');
|
||||
process.exit(0);
|
||||
}
|
||||
payload[key] = n;
|
||||
} else if (type === 'nullable_string') {
|
||||
payload[key] = rawVal === '' ? null : rawVal;
|
||||
} else {
|
||||
payload[key] = rawVal;
|
||||
}
|
||||
}
|
||||
|
||||
const patchRes = await request(recordsUrl + '/' + record.id, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!patchRes.ok) {
|
||||
await addReaction('-1');
|
||||
await postComment('❌ **PocketBase Bot**: PATCH failed for `' + slug + '`:\n```\n' + patchRes.body + '\n```');
|
||||
process.exit(1);
|
||||
}
|
||||
await addReaction('+1');
|
||||
const changesLines = Object.entries(payload)
|
||||
.map(function ([k, v]) { return '- `' + k + '` → `' + JSON.stringify(v) + '`'; })
|
||||
.join('\n');
|
||||
await postComment(
|
||||
'✅ **PocketBase Bot**: Updated **`' + slug + '`** successfully!\n\n' +
|
||||
'**Changes applied:**\n' + changesLines + '\n\n' +
|
||||
'*Executed by @' + actor + '*'
|
||||
);
|
||||
}
|
||||
|
||||
console.log('Done.');
|
||||
})().catch(function (e) {
|
||||
console.error('Fatal error:', e.message || e);
|
||||
process.exit(1);
|
||||
});
|
||||
ENDSCRIPT
|
||||
shell: bash
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
__ __ __ __ ___
|
||||
__ ______ _____/ /___ / /_/ /_ _____/ /___ ______/ (_)___
|
||||
/ / / / __ \/ ___/ / __ \/ __/ __ \______/ ___/ __/ / / / __ / / __ \
|
||||
/ /_/ / / / (__ ) / /_/ / /_/ / / /_____(__ ) /_/ /_/ / /_/ / / /_/ /
|
||||
\__,_/_/ /_/____/_/\____/\__/_/ /_/ /____/\__/\__,_/\__,_/_/\____/
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/Heretek-AI/ProxmoxVE/refs/heads/main}"
|
||||
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/build.func)
|
||||
# Author: Heretek-AI
|
||||
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/unslothai/unsloth
|
||||
|
||||
APP="unsloth-studio"
|
||||
var_tags="${var_tags:-ai;llm;fine-tuning;training}"
|
||||
var_cpu="${var_cpu:-4}"
|
||||
var_ram="${var_ram:-16384}"
|
||||
var_disk="${var_disk:-50}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
var_gpu="${var_gpu:-yes}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
color
|
||||
catch_errors
|
||||
|
||||
function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
|
||||
if [[ ! -d /opt/unsloth-studio ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Source the activation script to set up GPU environment
|
||||
if [[ -f /opt/unsloth-studio/activate.sh ]]; then
|
||||
source /opt/unsloth-studio/activate.sh
|
||||
else
|
||||
# Fallback: activate venv directly
|
||||
source /opt/unsloth-studio/.venv/bin/activate
|
||||
fi
|
||||
|
||||
if command -v unsloth &>/dev/null; then
|
||||
CURRENT_VERSION=$(pip show unsloth 2>/dev/null | grep -i version | awk '{print $2}' || echo "unknown")
|
||||
msg_info "Current version: ${CURRENT_VERSION}"
|
||||
|
||||
msg_info "Checking for updates"
|
||||
$STD pip install --upgrade unsloth 2>/dev/null
|
||||
|
||||
NEW_VERSION=$(pip show unsloth 2>/dev/null | grep -i version | awk '{print $2}' || echo "unknown")
|
||||
|
||||
if [[ "$CURRENT_VERSION" != "$NEW_VERSION" ]]; then
|
||||
msg_ok "Updated from ${CURRENT_VERSION} to ${NEW_VERSION}"
|
||||
else
|
||||
msg_ok "Already at latest version: ${NEW_VERSION}"
|
||||
fi
|
||||
else
|
||||
msg_error "Unsloth not installed properly"
|
||||
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}http://${IP}:8888${CL}"
|
||||
echo -e "${INFO}${YW} Note: First launch may take 5-10 minutes to compile llama.cpp${CL}"
|
||||
+14
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Unsloth Studio (In Beta)",
|
||||
"slug": "unsolth-studio",
|
||||
"name": "Unsloth Studio",
|
||||
"slug": "unsloth-studio",
|
||||
"categories": [20],
|
||||
"date_created": "2026-03-18",
|
||||
"type": "ct",
|
||||
@@ -10,12 +10,12 @@
|
||||
"documentation": "https://unsloth.ai/docs/new/studio/start",
|
||||
"website": "https://unsloth.ai/",
|
||||
"logo": "https://unsloth.ai/favicon.ico",
|
||||
"config_path": "/opt/unsolth-studio/.env",
|
||||
"config_path": "/opt/unsloth-studio/.env",
|
||||
"description": "Local, browser-based GUI for fine-tuning LLMs without writing code. Supports QLoRA, LoRA, and full fine-tuning with live training monitoring, GPU stats, and model export to GGUF/Safetensors.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/unsolth-studio.sh",
|
||||
"script": "ct/unsloth-studio.sh",
|
||||
"resources": {
|
||||
"cpu": 4,
|
||||
"ram": 16384,
|
||||
@@ -26,10 +26,14 @@
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
"username": "unsloth",
|
||||
"password": "Check logs for generated password"
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Default credentials: username 'unsloth', password is randomly generated and shown in logs. Run 'journalctl -u unsloth-studio | grep password' to find it.",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "Requires GPU passthrough for training. NVIDIA, AMD (ROCm), and Intel GPUs are supported.",
|
||||
"type": "info"
|
||||
@@ -45,6 +49,10 @@
|
||||
{
|
||||
"text": "For AMD GPUs, ensure ROCm is properly configured on the host.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "ROCm support for Unsloth Studio is not yet implemented. See https://github.com/unslothai/unsloth/pull/4390 for progress.",
|
||||
"type": "warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
+118
-33
@@ -33,8 +33,8 @@ setup_hwaccel
|
||||
PYTHON_VERSION="3.13" setup_uv
|
||||
|
||||
msg_info "Creating Virtual Environment"
|
||||
mkdir -p /opt/unsolth-studio
|
||||
cd /opt/unsolth-studio || exit
|
||||
mkdir -p /opt/unsloth-studio
|
||||
cd /opt/unsloth-studio || exit
|
||||
$STD uv venv --python 3.13
|
||||
source .venv/bin/activate
|
||||
msg_ok "Created Virtual Environment"
|
||||
@@ -107,8 +107,10 @@ elif [ -d "/opt/rocm-6.2" ]; then
|
||||
fi
|
||||
|
||||
# Check if GPU is available (works for both CUDA and ROCm)
|
||||
if /opt/unsolth-studio/.venv/bin/python -c "import torch; exit(0 if torch.cuda.is_available() else 1)" 2>/dev/null; then
|
||||
$STD /opt/unsolth-studio/.venv/bin/python -m unsloth studio setup
|
||||
if /opt/unsloth-studio/.venv/bin/python -c "import torch; exit(0 if torch.cuda.is_available() else 1)" 2>/dev/null; then
|
||||
# Use the unsloth CLI entry point instead of python -m unsloth
|
||||
# The package installs a 'unsloth' command that provides the studio subcommand
|
||||
$STD /opt/unsloth-studio/.venv/bin/unsloth studio setup
|
||||
msg_ok "Completed Unsloth Studio Setup"
|
||||
else
|
||||
msg_info "GPU not detected via torch.cuda - skipping Unsloth Studio setup"
|
||||
@@ -116,47 +118,89 @@ else
|
||||
echo ""
|
||||
echo -e "${GN}Note: If you have GPU passthrough configured, try:${CL}"
|
||||
echo -e "${GN} 1. Restart the container: pct stop <CTID> && pct start <CTID>${CL}"
|
||||
echo -e "${GN} 2. Then run: source /opt/unsolth-studio/.venv/bin/activate && unsloth studio setup${CL}"
|
||||
echo -e "${GN} 2. Then run: source /opt/unsloth-studio/.venv/bin/activate && unsloth studio setup${CL}"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
msg_info "Creating Directories"
|
||||
mkdir -p /opt/unsolth-studio/models
|
||||
mkdir -p /opt/unsolth-studio/datasets
|
||||
mkdir -p /var/log/unsolth-studio
|
||||
chmod 755 /var/log/unsolth-studio
|
||||
mkdir -p /opt/unsloth-studio/models
|
||||
mkdir -p /opt/unsloth-studio/datasets
|
||||
mkdir -p /var/log/unsloth-studio
|
||||
chmod 755 /var/log/unsloth-studio
|
||||
msg_ok "Created Directories"
|
||||
|
||||
msg_info "Creating Service"
|
||||
# Create environment file for ROCm/CUDA paths
|
||||
cat <<EOF >/opt/unsolth-studio/environment.sh
|
||||
# Detect ROCm version and create environment file
|
||||
ROCM_PATH=""
|
||||
HSA_GFX_VERSION=""
|
||||
|
||||
# Find ROCm installation
|
||||
if [ -d "/opt/rocm" ]; then
|
||||
ROCM_PATH="/opt/rocm"
|
||||
elif [ -d "/opt/rocm-7.2" ]; then
|
||||
ROCM_PATH="/opt/rocm-7.2"
|
||||
elif [ -d "/opt/rocm-6.2" ]; then
|
||||
ROCM_PATH="/opt/rocm-6.2"
|
||||
fi
|
||||
|
||||
# Detect GPU architecture for HSA_OVERRIDE_GFX_VERSION (needed for consumer AMD GPUs)
|
||||
if [ -n "$ROCM_PATH" ] && [ -x "$ROCM_PATH/bin/rocminfo" ]; then
|
||||
GFX_ARCH=$("$ROCM_PATH/bin/rocminfo" 2>/dev/null | grep -oP 'gfx\w+' | head -1 || true)
|
||||
if [ -n "$GFX_ARCH" ]; then
|
||||
HSA_GFX_VERSION="$GFX_ARCH"
|
||||
msg_info "Detected AMD GPU architecture: $GFX_ARCH"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create a shell wrapper script for manual commands
|
||||
cat <<'EOF' >/opt/unsloth-studio/activate.sh
|
||||
#!/bin/bash
|
||||
# Set up GPU environment for Unsloth Studio
|
||||
# Activate script for Unsloth Studio with GPU support
|
||||
# Source this file before running unsloth commands manually
|
||||
|
||||
# Activate virtual environment
|
||||
source /opt/unsloth-studio/.venv/bin/activate
|
||||
|
||||
# ROCm environment (AMD GPUs)
|
||||
if [ -d "/opt/rocm" ]; then
|
||||
export PATH="/opt/rocm/bin:\$PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm/lib:\$LD_LIBRARY_PATH"
|
||||
export PATH="/opt/rocm/bin:$PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||
export ROCM_PATH="/opt/rocm"
|
||||
elif [ -d "/opt/rocm-7.2" ]; then
|
||||
export PATH="/opt/rocm-7.2/bin:\$PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm-7.2/lib:\$LD_LIBRARY_PATH"
|
||||
export PATH="/opt/rocm-7.2/bin:$PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm-7.2/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||
export ROCM_PATH="/opt/rocm-7.2"
|
||||
elif [ -d "/opt/rocm-6.2" ]; then
|
||||
export PATH="/opt/rocm-6.2/bin:\$PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm-6.2/lib:\$LD_LIBRARY_PATH"
|
||||
export PATH="/opt/rocm-6.2/bin:$PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm-6.2/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||
export ROCM_PATH="/opt/rocm-6.2"
|
||||
fi
|
||||
|
||||
# NVIDIA CUDA environment
|
||||
if [ -d "/usr/local/cuda" ]; then
|
||||
export PATH="/usr/local/cuda/bin:\$PATH"
|
||||
export LD_LIBRARY_PATH="/usr/local/cuda/lib64:\$LD_LIBRARY_PATH"
|
||||
export PATH="/usr/local/cuda/bin:$PATH"
|
||||
export LD_LIBRARY_PATH="/usr/local/cuda/lib64${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||
fi
|
||||
EOF
|
||||
chmod +x /opt/unsolth-studio/environment.sh
|
||||
|
||||
cat <<EOF >/etc/systemd/system/unsolth-studio.service
|
||||
# Detect GPU architecture for HSA_OVERRIDE_GFX_VERSION
|
||||
if [ -n "$ROCM_PATH" ] && [ -x "$ROCM_PATH/bin/rocminfo" ]; then
|
||||
GFX_ARCH=$("$ROCM_PATH/bin/rocminfo" 2>/dev/null | grep -oP 'gfx\w+' | head -1 || true)
|
||||
if [ -n "$GFX_ARCH" ]; then
|
||||
export HSA_OVERRIDE_GFX_VERSION="$GFX_ARCH"
|
||||
echo "GPU architecture detected: $GFX_ARCH"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Make GPU visible
|
||||
export HIP_VISIBLE_DEVICES=0
|
||||
|
||||
echo "Environment activated. GPU ready for use."
|
||||
EOF
|
||||
chmod +x /opt/unsloth-studio/activate.sh
|
||||
|
||||
# Create systemd service with proper GPU environment
|
||||
# Note: systemd Environment doesn't support shell expansion, so we use static paths
|
||||
cat <<EOF >/etc/systemd/system/unsloth-studio.service
|
||||
[Unit]
|
||||
Description=Unsloth Studio - Local LLM Fine-tuning Web UI
|
||||
After=network.target network-online.target
|
||||
@@ -164,15 +208,26 @@ Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/opt/unsolth-studio
|
||||
Environment="PATH=/opt/unsolth-studio/.venv/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
EnvironmentFile=/opt/unsolth-studio/environment.sh
|
||||
ExecStart=/opt/unsolth-studio/.venv/bin/python -m unsloth studio -H 0.0.0.0 -p 8888
|
||||
WorkingDirectory=/opt/unsloth-studio
|
||||
Environment="PATH=/opt/unsloth-studio/.venv/bin:/opt/rocm/bin:/opt/rocm-7.2/bin:/opt/rocm-6.2/bin:/usr/local/cuda/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
Environment="LD_LIBRARY_PATH=/opt/rocm/lib:/opt/rocm-7.2/lib:/opt/rocm-6.2/lib:/usr/local/cuda/lib64"
|
||||
Environment="ROCM_PATH=/opt/rocm"
|
||||
Environment="HIP_VISIBLE_DEVICES=0"
|
||||
EOF
|
||||
|
||||
# Add HSA_OVERRIDE_GFX_VERSION if detected
|
||||
if [ -n "$HSA_GFX_VERSION" ]; then
|
||||
echo "Environment=\"HSA_OVERRIDE_GFX_VERSION=$HSA_GFX_VERSION\"" >>/etc/systemd/system/unsloth-studio.service
|
||||
fi
|
||||
|
||||
# Complete the service file
|
||||
cat <<EOF >>/etc/systemd/system/unsloth-studio.service
|
||||
ExecStart=/opt/unsloth-studio/.venv/bin/unsloth studio -H 0.0.0.0 -p 8888
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=unsolth-studio
|
||||
SyslogIdentifier=unsloth-studio
|
||||
|
||||
# Resource limits
|
||||
LimitNOFILE=65535
|
||||
@@ -184,16 +239,24 @@ WantedBy=multi-user.target
|
||||
EOF
|
||||
# Don't auto-start the service since GPU passthrough may not be configured yet
|
||||
# User needs to configure GPU passthrough first, then start the service manually
|
||||
systemctl enable -q unsolth-studio
|
||||
systemctl enable -q unsloth-studio
|
||||
msg_ok "Created Service"
|
||||
echo ""
|
||||
echo -e "${GN}Note: The unsolth-studio service is enabled but not started.${CL}"
|
||||
echo -e "${GN}Note: The unsloth-studio service is enabled but not started.${CL}"
|
||||
echo -e "${GN}Configure GPU passthrough first, then start with:${CL}"
|
||||
echo -e "${GN} systemctl start unsolth-studio${CL}"
|
||||
echo -e "${GN} systemctl start unsloth-studio${CL}"
|
||||
echo ""
|
||||
if [ -n "$HSA_GFX_VERSION" ]; then
|
||||
echo -e "${YW}AMD GPU detected with architecture: $HSA_GFX_VERSION${CL}"
|
||||
echo -e "${YW}HSA_OVERRIDE_GFX_VERSION has been set in the systemd service.${CL}"
|
||||
echo ""
|
||||
fi
|
||||
echo -e "${GN}For manual commands, activate the environment first:${CL}"
|
||||
echo -e "${GN} source /opt/unsloth-studio/activate.sh${CL}"
|
||||
echo ""
|
||||
|
||||
# Create GPU passthrough info file
|
||||
cat <<EOF >/opt/unsolth-studio/GPU_PASSTHROUGH.md
|
||||
cat <<EOF >/opt/unsloth-studio/GPU_PASSTHROUGH.md
|
||||
# GPU Passthrough Configuration for Unsloth Studio
|
||||
|
||||
This container has been configured for GPU acceleration for LLM fine-tuning.
|
||||
@@ -232,9 +295,31 @@ lxc.cgroup2.devices.allow: c 226:128 rwm
|
||||
|
||||
Run these commands inside the container:
|
||||
- nvidia-smi (NVIDIA GPUs)
|
||||
- rocminfo (AMD GPUs)
|
||||
- rocm-smi or rocminfo (AMD GPUs)
|
||||
- python -c "import torch; print(torch.cuda.is_available())"
|
||||
|
||||
## AMD GPU Configuration
|
||||
|
||||
For AMD consumer GPUs (Radeon RX series), the HSA_OVERRIDE_GFX_VERSION environment
|
||||
variable is automatically set during installation based on your GPU architecture.
|
||||
|
||||
If torch.cuda.is_available() returns False, you may need to manually set it:
|
||||
\`\`\`
|
||||
# Find your GPU architecture
|
||||
rocminfo | grep -oP 'gfx\\w+' | head -1
|
||||
|
||||
# Set the override (example for RX 7900 XT - gfx1100)
|
||||
export HSA_OVERRIDE_GFX_VERSION=gfx1100
|
||||
\`\`\`
|
||||
|
||||
## Manual Environment Activation
|
||||
|
||||
For running unsloth commands manually, activate the environment first:
|
||||
\`\`\`
|
||||
source /opt/unsloth-studio/activate.sh
|
||||
unsloth studio setup
|
||||
\`\`\`
|
||||
|
||||
## Usage
|
||||
|
||||
Access the web UI at: http://<IP>:8888
|
||||
+2
-1
@@ -9,10 +9,11 @@ APP="MCPHub"
|
||||
var_tags="${var_tags:-ai;automation;tooling}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-2048}"
|
||||
var_disk="${var_disk:-8}"
|
||||
var_disk="${var_disk:-10}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
var_database="${var_database:-postgresql}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
|
||||
-1
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/Heretek-AI/ProxmoxVE/refs/heads/main}"
|
||||
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/build.func)
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: BillyOutlast
|
||||
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/infiniflow/ragflow
|
||||
|
||||
+21
-3
@@ -1,9 +1,7 @@
|
||||
{
|
||||
"name": "RAGFlow",
|
||||
"slug": "ragflow",
|
||||
"categories": [
|
||||
20
|
||||
],
|
||||
"categories": [20],
|
||||
"date_created": "2026-03-12",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
@@ -75,6 +73,26 @@
|
||||
{
|
||||
"text": "Minimum 16GB RAM recommended for production workloads",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "Optional MCP Server (disabled by default): Enable with 'systemctl enable --now ragflow-mcp.service'",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "MCP endpoint: http://<IP>:9382 - for AI assistant integration (Claude Desktop, etc.)",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "MCP Server requires RAGFlow API key - generate in Settings > API Keys",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "MCP Documentation: https://ragflow.io/docs/launch_mcp_server",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "API Key Guide: https://ragflow.io/docs/acquire_ragflow_api_key",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+27
-1
@@ -12,12 +12,28 @@ setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
# =============================================================================
|
||||
# RUNTIMES & DATABASE SETUP
|
||||
# =============================================================================
|
||||
|
||||
NODE_VERSION="22" setup_nodejs
|
||||
|
||||
# PostgreSQL with pgvector for smart routing
|
||||
PG_VERSION="17" setup_postgresql
|
||||
PG_DB_NAME="mcphub" PG_DB_USER="mcphub" PG_DB_EXTENSIONS="pgvector" setup_postgresql_db
|
||||
|
||||
# =============================================================================
|
||||
# INSTALL MCPHUB
|
||||
# =============================================================================
|
||||
|
||||
msg_info "Installing MCPHub"
|
||||
$STD npm install -g @samanhappy/mcphub
|
||||
msg_ok "Installed MCPHub"
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
msg_info "Creating Default Configuration"
|
||||
mkdir -p /opt/mcphub
|
||||
cat <<EOF >/opt/mcphub/mcp_settings.json
|
||||
@@ -32,13 +48,18 @@ cat <<EOF >/opt/mcphub/mcp_settings.json
|
||||
EOF
|
||||
msg_ok "Created Default Configuration"
|
||||
|
||||
# =============================================================================
|
||||
# CREATE SYSTEMD SERVICE
|
||||
# =============================================================================
|
||||
|
||||
msg_info "Creating Service"
|
||||
NPM_GLOBAL_BIN="$(npm prefix -g)/bin"
|
||||
MCPHUB_BIN="${NPM_GLOBAL_BIN}/mcphub"
|
||||
cat <<EOF >/etc/systemd/system/mcphub.service
|
||||
[Unit]
|
||||
Description=MCPHub
|
||||
After=network.target
|
||||
After=network.target postgresql.service
|
||||
Requires=postgresql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
@@ -46,6 +67,7 @@ WorkingDirectory=/opt/mcphub
|
||||
Environment=NODE_ENV=production
|
||||
Environment=PORT=3000
|
||||
Environment=MCPHUB_SETTING_PATH=/opt/mcphub/mcp_settings.json
|
||||
Environment=DATABASE_URL="postgresql://${PG_DB_USER}:${PG_DB_PASS}@localhost:5432/${PG_DB_NAME}"
|
||||
ExecStart=${MCPHUB_BIN}
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
@@ -56,6 +78,10 @@ EOF
|
||||
systemctl enable -q --now mcphub
|
||||
msg_ok "Created Service"
|
||||
|
||||
# =============================================================================
|
||||
# CLEANUP & FINALIZATION
|
||||
# =============================================================================
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
|
||||
+88
-5
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: BillyOutlast
|
||||
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/infiniflow/ragflow
|
||||
@@ -111,6 +112,14 @@ max_allowed_packet=1073741824
|
||||
max_connections=900
|
||||
character-set-server=utf8mb4
|
||||
collation-server=utf8mb4_unicode_ci
|
||||
# Connection handling optimizations
|
||||
wait_timeout=28800
|
||||
interactive_timeout=28800
|
||||
connect_timeout=60
|
||||
# Buffer pool for performance
|
||||
innodb_buffer_pool_size=2G
|
||||
# Connection queue
|
||||
back_log=900
|
||||
EOF
|
||||
systemctl restart mariadb
|
||||
msg_ok "Configured MariaDB"
|
||||
@@ -232,6 +241,10 @@ MINIO_PASS=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c16)
|
||||
curl -fsSL https://dl.min.io/server/minio/release/linux-amd64/minio -o /usr/local/bin/minio
|
||||
chmod +x /usr/local/bin/minio
|
||||
|
||||
# Download MinIO Client (mc) for bucket management
|
||||
curl -fsSL https://dl.min.io/client/mc/release/linux-amd64/mc -o /usr/local/bin/mc
|
||||
chmod +x /usr/local/bin/mc
|
||||
|
||||
# Create MinIO directories
|
||||
mkdir -p /var/lib/minio/data
|
||||
|
||||
@@ -265,6 +278,18 @@ for i in {1..30}; do
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Create ragflow bucket using MinIO Client
|
||||
msg_info "Creating MinIO Bucket"
|
||||
for i in {1..30}; do
|
||||
if /usr/local/bin/mc alias set local http://localhost:9000 rag_flow "${MINIO_PASS}" 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
/usr/local/bin/mc mb local/ragflow --ignore-existing 2>/dev/null || true
|
||||
msg_ok "Created MinIO Bucket"
|
||||
|
||||
msg_ok "MinIO Installed"
|
||||
|
||||
# ==============================================================================
|
||||
@@ -272,7 +297,7 @@ msg_ok "MinIO Installed"
|
||||
# ==============================================================================
|
||||
|
||||
msg_info "Downloading RAGFlow"
|
||||
fetch_and_deploy_gh_release "ragflow" "infiniflow/ragflow" "tarball" "v0.24.0" "/opt/ragflow"
|
||||
fetch_and_deploy_gh_release "ragflow" "infiniflow/ragflow" "tarball" "latest" "/opt/ragflow"
|
||||
msg_ok "Downloaded RAGFlow"
|
||||
|
||||
# ==============================================================================
|
||||
@@ -309,8 +334,8 @@ mysql:
|
||||
password: '${MARIADB_DB_PASS}'
|
||||
host: 'localhost'
|
||||
port: 3306
|
||||
max_connections: 900
|
||||
stale_timeout: 300
|
||||
max_connections: 100
|
||||
stale_timeout: 60
|
||||
max_allowed_packet: 1073741824
|
||||
minio:
|
||||
user: 'rag_flow'
|
||||
@@ -358,7 +383,7 @@ SVR_WEB_HTTPS_PORT=443
|
||||
SVR_HTTP_PORT=9380
|
||||
ADMIN_SVR_HTTP_PORT=9381
|
||||
SVR_MCP_PORT=9382
|
||||
RAGFLOW_IMAGE=infiniflow/ragflow:v0.24.0
|
||||
RAGFLOW_IMAGE=infiniflow/ragflow:latest
|
||||
TZ=UTC
|
||||
REGISTER_ENABLED=1
|
||||
THREAD_POOL_MAX_WORKERS=128
|
||||
@@ -377,6 +402,7 @@ cat <<EOF >/etc/systemd/system/ragflow-server.service
|
||||
Description=RAGFlow Backend Server
|
||||
After=network.target mariadb.service elasticsearch.service redis-server.service minio.service
|
||||
Requires=mariadb.service elasticsearch.service redis-server.service minio.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
@@ -384,7 +410,10 @@ WorkingDirectory=/opt/ragflow
|
||||
Environment=PYTHONPATH=/opt/ragflow
|
||||
Environment=LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu/
|
||||
Environment=NLTK_DATA=/opt/ragflow/nltk_data
|
||||
ExecStartPre=/bin/sleep 10
|
||||
# Wait for services to be fully ready
|
||||
ExecStartPre=/bin/sleep 15
|
||||
# Health check for MariaDB
|
||||
ExecStartPre=/bin/bash -c 'for i in {1..30}; do mysqladmin ping -h localhost --silent && break; sleep 1; done'
|
||||
ExecStart=/usr/local/bin/uv run --index-strategy unsafe-best-match python api/ragflow_server.py
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
@@ -417,6 +446,43 @@ LimitNOFILE=65535
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# ==============================================================================
|
||||
# OPTIONAL: MCP SERVER SERVICE
|
||||
# ==============================================================================
|
||||
# The MCP (Model Context Protocol) server is optional and provides integration
|
||||
# with AI assistants like Claude Desktop. It runs on port 9382 by default.
|
||||
# To enable: systemctl enable --now ragflow-mcp.service
|
||||
|
||||
msg_info "Creating Optional MCP Server Service"
|
||||
|
||||
cat <<EOF >/etc/systemd/system/ragflow-mcp.service
|
||||
[Unit]
|
||||
Description=RAGFlow MCP Server (Model Context Protocol)
|
||||
After=network.target mariadb.service elasticsearch.service redis-server.service minio.service ragflow-server.service
|
||||
Requires=mariadb.service elasticsearch.service redis-server.service minio.service ragflow-server.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/opt/ragflow
|
||||
Environment=PYTHONPATH=/opt/ragflow
|
||||
Environment=LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu/
|
||||
Environment=NLTK_DATA=/opt/ragflow/nltk_data
|
||||
ExecStartPre=/bin/sleep 15
|
||||
ExecStart=/usr/local/bin/uv run --index-strategy unsafe-best-match python mcp/server/server.py --host=0.0.0.0 --port=9382 --base-url=http://127.0.0.1:9380
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
TimeoutStartSec=300
|
||||
LimitNOFILE=65535
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# MCP service is disabled by default - users must opt-in
|
||||
systemctl disable ragflow-mcp.service 2>/dev/null || true
|
||||
|
||||
msg_ok "Created Optional MCP Server Service (disabled by default)"
|
||||
|
||||
msg_ok "Created Systemd Services"
|
||||
|
||||
# ==============================================================================
|
||||
@@ -500,11 +566,23 @@ msg_ok "Nginx Frontend Configured"
|
||||
# ==============================================================================
|
||||
|
||||
msg_info "Starting RAGFlow Services"
|
||||
systemctl enable -q ragflow-server
|
||||
systemctl start ragflow-server
|
||||
sleep 5
|
||||
systemctl enable -q ragflow-task-executor
|
||||
systemctl start ragflow-task-executor
|
||||
|
||||
msg_ok "Started RAGFlow Services"
|
||||
|
||||
# ==============================================================================
|
||||
# Reloading Nginx and services after installation
|
||||
# ==============================================================================
|
||||
|
||||
msg_info "Reloading Nginx and Services"
|
||||
systemctl reload nginx
|
||||
systemctl restart nginx
|
||||
msg_ok "Reloaded Nginx"
|
||||
|
||||
# ==============================================================================
|
||||
# FINALIZATION
|
||||
# ==============================================================================
|
||||
@@ -538,3 +616,8 @@ echo -e "${TAB}- Configure your LLM API key in the web interface"
|
||||
echo -e "${TAB}- Default uses CPU for document processing"
|
||||
echo -e "${TAB}- For GPU acceleration, additional configuration required"
|
||||
echo -e "${TAB}- Elasticsearch may take 1-2 minutes to fully initialize"
|
||||
echo -e ""
|
||||
echo -e "${INFO}${YW} Optional MCP Server (for AI assistant integration):${CL}"
|
||||
echo -e "${TAB}- MCP endpoint: http://${LOCAL_IP}:9382"
|
||||
echo -e "${TAB}- Enable with: systemctl enable --now ragflow-mcp.service"
|
||||
echo -e "${TAB}- Requires RAGFlow API key from web interface"
|
||||
|
||||
+6
@@ -420,6 +420,12 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
||||
|
||||
</details>
|
||||
|
||||
## 2026-03-19
|
||||
|
||||
### 📂 Github
|
||||
|
||||
- Upstream sync 20260319 160030 [@BillyOutlast](https://github.com/BillyOutlast) ([#69](https://github.com/Heretek-AI/ProxmoxVE/pull/69))
|
||||
|
||||
## 2026-03-18
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
+30
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/Heretek-AI/ProxmoxVE/refs/heads/main}"
|
||||
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/build.func)
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: BillyOutlast
|
||||
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/infiniflow/ragflow
|
||||
@@ -30,7 +31,14 @@ function update_script() {
|
||||
fi
|
||||
|
||||
if check_for_gh_release "ragflow" "infiniflow/ragflow"; then
|
||||
# Check if MCP service is enabled before stopping
|
||||
MCP_WAS_ENABLED=false
|
||||
if systemctl is-enabled ragflow-mcp.service 2>/dev/null | grep -q "enabled"; then
|
||||
MCP_WAS_ENABLED=true
|
||||
fi
|
||||
|
||||
msg_info "Stopping Services"
|
||||
systemctl stop ragflow-mcp || true
|
||||
systemctl stop ragflow-task-executor || true
|
||||
systemctl stop ragflow-server || true
|
||||
msg_ok "Stopped Services"
|
||||
@@ -68,6 +76,23 @@ function update_script() {
|
||||
systemctl start ragflow-server
|
||||
sleep 5
|
||||
systemctl start ragflow-task-executor
|
||||
|
||||
# Restart MCP service if it was enabled before update
|
||||
if [[ "$MCP_WAS_ENABLED" == "true" ]]; then
|
||||
msg_info "Restarting MCP Server"
|
||||
systemctl start ragflow-mcp || true
|
||||
msg_ok "Restarted MCP Server"
|
||||
fi
|
||||
|
||||
# Verify MinIO bucket exists (fixes NoSuchBucket errors)
|
||||
msg_info "Verifying MinIO Bucket"
|
||||
MINIO_PASS=$(grep -oP 'MINIO_PASSWORD=\K[^"]+' /opt/ragflow/.env 2>/dev/null || grep -oP 'password: \K[^"]+' /opt/ragflow/conf/service_conf.yaml 2>/dev/null | head -1)
|
||||
if [[ -n "$MINIO_PASS" ]] && [[ -x /usr/local/bin/mc ]]; then
|
||||
/usr/local/bin/mc alias set local http://localhost:9000 rag_flow "${MINIO_PASS}" 2>/dev/null || true
|
||||
/usr/local/bin/mc mb local/ragflow --ignore-existing 2>/dev/null || true
|
||||
fi
|
||||
msg_ok "Verified MinIO Bucket"
|
||||
|
||||
msg_ok "Started Services"
|
||||
msg_ok "Updated successfully!"
|
||||
fi
|
||||
@@ -83,3 +108,8 @@ 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}"
|
||||
echo -e "${INFO}${YW} API endpoint: http://${IP}:9380${CL}"
|
||||
echo -e ""
|
||||
echo -e "${INFO}${YW} Optional MCP Server (for AI assistant integration):${CL}"
|
||||
echo -e "${TAB}- MCP endpoint: http://${IP}:9382"
|
||||
echo -e "${TAB}- Enable with: systemctl enable --now ragflow-mcp.service"
|
||||
echo -e "${TAB}- Requires RAGFlow API key from web interface"
|
||||
|
||||
+12
-5
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"generated": "2026-03-16T18:42:54Z",
|
||||
"generated": "2026-03-19T06:41:06Z",
|
||||
"versions": [
|
||||
{
|
||||
"slug": "agregarr",
|
||||
"repo": "agregarr/agregarr",
|
||||
"version": "v2.4.1",
|
||||
"version": "v2.4.2",
|
||||
"pinned": false,
|
||||
"date": "2026-03-05T06:43:40Z"
|
||||
"date": "2026-03-17T08:57:42Z"
|
||||
},
|
||||
{
|
||||
"slug": "lemonade",
|
||||
@@ -18,9 +18,9 @@
|
||||
{
|
||||
"slug": "llamacpp",
|
||||
"repo": "ggml-org/llama.cpp",
|
||||
"version": "b8373",
|
||||
"version": "b8417",
|
||||
"pinned": false,
|
||||
"date": "2026-03-16T10:55:12Z"
|
||||
"date": "2026-03-19T05:37:13Z"
|
||||
},
|
||||
{
|
||||
"slug": "localai",
|
||||
@@ -57,6 +57,13 @@
|
||||
"pinned": true,
|
||||
"date": "2026-02-10T09:27:14Z"
|
||||
},
|
||||
{
|
||||
"slug": "skillserver",
|
||||
"repo": "mudler/skillserver",
|
||||
"version": "v0.0.4",
|
||||
"pinned": false,
|
||||
"date": "2026-01-28T10:30:17Z"
|
||||
},
|
||||
{
|
||||
"slug": "wakapi",
|
||||
"repo": "muety/wakapi",
|
||||
|
||||
+21
-21
@@ -86,16 +86,16 @@ variables() {
|
||||
# Set default URL if not already defined by the calling script
|
||||
COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main}"
|
||||
|
||||
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/api.func)
|
||||
source <(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/misc/api.func)
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/core.func)
|
||||
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/error_handler.func)
|
||||
source <(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/misc/core.func)
|
||||
source <(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/misc/error_handler.func)
|
||||
load_functions
|
||||
catch_errors
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
source <(wget -qO- "${COMMUNITY_SCRIPTS_URL}"/misc/core.func)
|
||||
source <(wget -qO- "${COMMUNITY_SCRIPTS_URL}"/misc/error_handler.func)
|
||||
source <(wget -qO- ${COMMUNITY_SCRIPTS_URL}/misc/core.func)
|
||||
source <(wget -qO- ${COMMUNITY_SCRIPTS_URL}/misc/error_handler.func)
|
||||
load_functions
|
||||
catch_errors
|
||||
fi
|
||||
@@ -1489,7 +1489,7 @@ _build_vars_diff() {
|
||||
|
||||
# Build a temporary <app>.vars file from current advanced settings
|
||||
_build_current_app_vars_tmp() {
|
||||
tmpf="$(mktemp /tmp/"${NSAPP:-app}".vars.new.XXXXXX)"
|
||||
tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)"
|
||||
|
||||
# NET/GW
|
||||
_net="${NET:-}"
|
||||
@@ -3440,7 +3440,7 @@ msg_menu() {
|
||||
# - Otherwise: shows update/setting menu and runs update_script with cleanup
|
||||
# ------------------------------------------------------------------------------
|
||||
start() {
|
||||
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/tools.func)
|
||||
source <(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/misc/tools.func)
|
||||
if command -v pveversion >/dev/null 2>&1; then
|
||||
install_script || return 0
|
||||
return 0
|
||||
@@ -4136,7 +4136,7 @@ EOF'
|
||||
# that sends "configuring" status AFTER the host already reported "failed"
|
||||
export CONTAINER_INSTALLING=true
|
||||
|
||||
lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/install/"${var_install}".sh)"
|
||||
lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/install/${var_install}.sh)"
|
||||
local lxc_exit=$?
|
||||
|
||||
unset CONTAINER_INSTALLING
|
||||
@@ -4253,7 +4253,7 @@ EOF'
|
||||
else
|
||||
msg_dev "Container ${CTID} kept for debugging"
|
||||
fi
|
||||
exit "$install_exit_code"
|
||||
exit $install_exit_code
|
||||
fi
|
||||
|
||||
# Prompt user for cleanup with 60s timeout
|
||||
@@ -4449,7 +4449,7 @@ EOF'
|
||||
echo -e "${BFR}${CM}${GN}MOTD/SSH ready - SSH into container: ssh root@${ct_ip}${CL}"
|
||||
fi
|
||||
fi
|
||||
exit "$install_exit_code"
|
||||
exit $install_exit_code
|
||||
;;
|
||||
3)
|
||||
# Retry with verbose mode (full rebuild)
|
||||
@@ -4514,7 +4514,7 @@ EOF'
|
||||
# Re-run install script in existing container (don't destroy/recreate)
|
||||
set +Eeuo pipefail
|
||||
trap - ERR
|
||||
lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/install/"${var_install}".sh)"
|
||||
lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/install/${var_install}.sh)"
|
||||
local apt_retry_exit=$?
|
||||
set -Eeuo pipefail
|
||||
trap 'error_handler' ERR
|
||||
@@ -4630,7 +4630,7 @@ EOF'
|
||||
|
||||
if [[ "$handled" == false ]]; then
|
||||
echo -e "\n${TAB}${YW}Invalid option. Container ${CTID} kept.${CL}"
|
||||
exit "$install_exit_code"
|
||||
exit $install_exit_code
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
@@ -4651,7 +4651,7 @@ EOF'
|
||||
|
||||
# Restore default job-control signal handling before exit
|
||||
trap - TSTP TTIN TTOU
|
||||
exit "$install_exit_code"
|
||||
exit $install_exit_code
|
||||
fi
|
||||
|
||||
# Re-enable error handling after successful install or recovery menu completion
|
||||
@@ -5015,7 +5015,7 @@ create_lxc_container() {
|
||||
msg_ok "LXC stack upgraded."
|
||||
if [[ "$do_retry" == "yes" ]]; then
|
||||
msg_info "Retrying container creation after upgrade"
|
||||
if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "$PCT_OPTIONS" >>"$LOGFILE" 2>&1; then
|
||||
if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
|
||||
msg_ok "Container created successfully after upgrade."
|
||||
return 0
|
||||
else
|
||||
@@ -5270,7 +5270,7 @@ create_lxc_container() {
|
||||
fi
|
||||
fi
|
||||
|
||||
TEMPLATE_PATH="$(pvesm path "$TEMPLATE_STORAGE":vztmpl/"$TEMPLATE" 2>/dev/null || true)"
|
||||
TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)"
|
||||
if [[ -z "$TEMPLATE_PATH" ]]; then
|
||||
TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg)
|
||||
[[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE"
|
||||
@@ -5334,7 +5334,7 @@ create_lxc_container() {
|
||||
TEMPLATE_SOURCE="online"
|
||||
fi
|
||||
|
||||
TEMPLATE_PATH="$(pvesm path "$TEMPLATE_STORAGE":vztmpl/"$TEMPLATE" 2>/dev/null || true)"
|
||||
TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)"
|
||||
if [[ -z "$TEMPLATE_PATH" ]]; then
|
||||
TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg)
|
||||
[[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE"
|
||||
@@ -5530,7 +5530,7 @@ create_lxc_container() {
|
||||
msg_debug "Logfile: $LOGFILE"
|
||||
|
||||
# First attempt (PCT_OPTIONS is a multi-line string, use it directly)
|
||||
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "$PCT_OPTIONS" >"$LOGFILE" 2>&1; then
|
||||
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >"$LOGFILE" 2>&1; then
|
||||
msg_debug "Container creation failed on ${TEMPLATE_STORAGE}. Checking error..."
|
||||
|
||||
# Check if template issue - retry with fresh download
|
||||
@@ -5542,7 +5542,7 @@ create_lxc_container() {
|
||||
fi
|
||||
|
||||
# Retry after repair
|
||||
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "$PCT_OPTIONS" >>"$LOGFILE" 2>&1; then
|
||||
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
|
||||
# Fallback to local storage if not already on local
|
||||
if [[ "$TEMPLATE_STORAGE" != "local" ]]; then
|
||||
msg_info "Retrying container creation with fallback to local storage"
|
||||
@@ -5555,7 +5555,7 @@ create_lxc_container() {
|
||||
else
|
||||
msg_ok "Trying local storage fallback"
|
||||
fi
|
||||
if ! pct create "$CTID" "local:vztmpl/${TEMPLATE}" "$PCT_OPTIONS" >>"$LOGFILE" 2>&1; then
|
||||
if ! pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
|
||||
# Local fallback also failed - check for LXC stack version issue
|
||||
if grep -qiE 'unsupported .* version' "$LOGFILE"; then
|
||||
msg_warn "pct reported 'unsupported version' – LXC stack might be too old for this template"
|
||||
@@ -5578,7 +5578,7 @@ create_lxc_container() {
|
||||
msg_error "Container creation failed. See $LOGFILE"
|
||||
if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
|
||||
set -x
|
||||
pct create "$CTID" "local:vztmpl/${TEMPLATE}" "$PCT_OPTIONS" 2>&1 | tee -a "$LOGFILE"
|
||||
pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE"
|
||||
set +x
|
||||
fi
|
||||
_flush_pct_log
|
||||
@@ -5610,7 +5610,7 @@ create_lxc_container() {
|
||||
msg_error "Container creation failed. See $LOGFILE"
|
||||
if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
|
||||
set -x
|
||||
pct create "$CTID" "local:vztmpl/${TEMPLATE}" "$PCT_OPTIONS" 2>&1 | tee -a "$LOGFILE"
|
||||
pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE"
|
||||
set +x
|
||||
fi
|
||||
_flush_pct_log
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
<a href="#">
|
||||
<img src="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo.png" height="100px" />
|
||||
<img src="https://raw.githubusercontent.com/Heretek-AI/ProxmoxVE/main/misc/images/logo.png" height="100px" />
|
||||
</a>
|
||||
</div>
|
||||
<h1 align="center">Changelog</h1>
|
||||
|
||||
+5
-11
@@ -116,18 +116,12 @@ export default function Page() {
|
||||
<span className="steel-text glitch">Heretek-AI</span>
|
||||
</h1>
|
||||
<div className="max-w-2xl gap-2 flex flex-col text-center sm:text-lg text-sm leading-relaxed tracking-tight text-muted-foreground md:text-xl">
|
||||
<p className="text-blood-400 font-semibold animate-pulse">Uncompliant scripts, made quicker.</p>
|
||||
<p className="text-blood-400 font-semibold animate-pulse">Forbidden scripts, forged in innovation.</p>
|
||||
<p>
|
||||
Scripts that don't meet the strict guidelines of the official{" "}
|
||||
<a
|
||||
href="https://github.com/community-scripts/ProxmoxVE"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline text-blood-400 hover:text-blood-300 transition-colors"
|
||||
>
|
||||
Community-Scripts
|
||||
</a>{" "}
|
||||
repository, but are updated faster and built with flexibility in mind.
|
||||
Scripts that embrace the heretical path—beyond the rigid dogma of the orthodox repositories. Updated
|
||||
with
|
||||
<span className="text-corruption-400"> relentless speed</span>, built for those who dare to
|
||||
<span className="text-blood-400"> innovate</span>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+18
-37
@@ -31,7 +31,7 @@ function MousePosition(): MousePosition {
|
||||
}
|
||||
|
||||
// Mechanicus-themed particle presets
|
||||
type ParticleTheme = "default" | "rust" | "corruption" | "brass" | "mechanicus";
|
||||
type ParticleTheme = "default" | "rust" | "corruption" | "brass" | "mechanicus" | "heretek";
|
||||
|
||||
type ParticlesProps = {
|
||||
className?: string;
|
||||
@@ -68,6 +68,10 @@ const MECHANICUS_THEMES: Record<ParticleTheme, { colors: string[]; defaultColor:
|
||||
colors: ["#b45309", "#15803d", "#ca8a04", "#92400e", "#166534"],
|
||||
defaultColor: "#b45309",
|
||||
},
|
||||
heretek: {
|
||||
colors: ["#7f1d1d", "#991b1b", "#dc2626", "#450a0a", "#1e3a5f"],
|
||||
defaultColor: "#dc2626",
|
||||
},
|
||||
};
|
||||
|
||||
function hexToRgb(hex: string): number[] {
|
||||
@@ -76,7 +80,7 @@ function hexToRgb(hex: string): number[] {
|
||||
if (hex.length === 3) {
|
||||
hex = hex
|
||||
.split("")
|
||||
.map(char => char + char)
|
||||
.map((char) => char + char)
|
||||
.join("");
|
||||
}
|
||||
|
||||
@@ -223,12 +227,7 @@ const Particles: React.FC<ParticlesProps> = ({
|
||||
|
||||
const clearContext = () => {
|
||||
if (context.current) {
|
||||
context.current.clearRect(
|
||||
0,
|
||||
0,
|
||||
canvasSize.current.w,
|
||||
canvasSize.current.h,
|
||||
);
|
||||
context.current.clearRect(0, 0, canvasSize.current.w, canvasSize.current.h);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -241,15 +240,8 @@ const Particles: React.FC<ParticlesProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const remapValue = (
|
||||
value: number,
|
||||
start1: number,
|
||||
end1: number,
|
||||
start2: number,
|
||||
end2: number,
|
||||
): number => {
|
||||
const remapped
|
||||
= ((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
|
||||
const remapValue = (value: number, start1: number, end1: number, start2: number, end2: number): number => {
|
||||
const remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
|
||||
return remapped > 0 ? remapped : 0;
|
||||
};
|
||||
|
||||
@@ -264,35 +256,28 @@ const Particles: React.FC<ParticlesProps> = ({
|
||||
canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
|
||||
];
|
||||
const closestEdge = edge.reduce((a, b) => Math.min(a, b));
|
||||
const remapClosestEdge = Number.parseFloat(
|
||||
remapValue(closestEdge, 0, 20, 0, 1).toFixed(2),
|
||||
);
|
||||
const remapClosestEdge = Number.parseFloat(remapValue(closestEdge, 0, 20, 0, 1).toFixed(2));
|
||||
if (remapClosestEdge > 1) {
|
||||
circle.alpha += 0.02;
|
||||
if (circle.alpha > circle.targetAlpha) {
|
||||
circle.alpha = circle.targetAlpha;
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
circle.alpha = circle.targetAlpha * remapClosestEdge;
|
||||
}
|
||||
circle.x += circle.dx + vx;
|
||||
circle.y += circle.dy + vy;
|
||||
circle.translateX
|
||||
+= (mouse.current.x / (staticity / circle.magnetism) - circle.translateX)
|
||||
/ ease;
|
||||
circle.translateY
|
||||
+= (mouse.current.y / (staticity / circle.magnetism) - circle.translateY)
|
||||
/ ease;
|
||||
circle.translateX += (mouse.current.x / (staticity / circle.magnetism) - circle.translateX) / ease;
|
||||
circle.translateY += (mouse.current.y / (staticity / circle.magnetism) - circle.translateY) / ease;
|
||||
|
||||
drawCircle(circle, true);
|
||||
|
||||
// circle gets out of the canvas
|
||||
if (
|
||||
circle.x < -circle.size
|
||||
|| circle.x > canvasSize.current.w + circle.size
|
||||
|| circle.y < -circle.size
|
||||
|| circle.y > canvasSize.current.h + circle.size
|
||||
circle.x < -circle.size ||
|
||||
circle.x > canvasSize.current.w + circle.size ||
|
||||
circle.y < -circle.size ||
|
||||
circle.y > canvasSize.current.h + circle.size
|
||||
) {
|
||||
// remove the circle from the array
|
||||
circles.current.splice(i, 1);
|
||||
@@ -306,11 +291,7 @@ const Particles: React.FC<ParticlesProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("pointer-events-none", className)}
|
||||
ref={canvasContainerRef}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div className={cn("pointer-events-none", className)} ref={canvasContainerRef} aria-hidden="true">
|
||||
<canvas ref={canvasRef} className="size-full" />
|
||||
</div>
|
||||
);
|
||||
|
||||
+37
-33
@@ -86,16 +86,16 @@ variables() {
|
||||
# Set default URL if not already defined by the calling script
|
||||
COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main}"
|
||||
|
||||
source <(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/misc/api.func)
|
||||
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/api.func)
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
source <(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/misc/core.func)
|
||||
source <(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/misc/error_handler.func)
|
||||
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/core.func)
|
||||
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/error_handler.func)
|
||||
load_functions
|
||||
catch_errors
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
source <(wget -qO- ${COMMUNITY_SCRIPTS_URL}/misc/core.func)
|
||||
source <(wget -qO- ${COMMUNITY_SCRIPTS_URL}/misc/error_handler.func)
|
||||
source <(wget -qO- "${COMMUNITY_SCRIPTS_URL}"/misc/core.func)
|
||||
source <(wget -qO- "${COMMUNITY_SCRIPTS_URL}"/misc/error_handler.func)
|
||||
load_functions
|
||||
catch_errors
|
||||
fi
|
||||
@@ -1489,7 +1489,7 @@ _build_vars_diff() {
|
||||
|
||||
# Build a temporary <app>.vars file from current advanced settings
|
||||
_build_current_app_vars_tmp() {
|
||||
tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)"
|
||||
tmpf="$(mktemp /tmp/"${NSAPP:-app}".vars.new.XXXXXX)"
|
||||
|
||||
# NET/GW
|
||||
_net="${NET:-}"
|
||||
@@ -3440,7 +3440,7 @@ msg_menu() {
|
||||
# - Otherwise: shows update/setting menu and runs update_script with cleanup
|
||||
# ------------------------------------------------------------------------------
|
||||
start() {
|
||||
source <(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/misc/tools.func)
|
||||
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/tools.func)
|
||||
if command -v pveversion >/dev/null 2>&1; then
|
||||
install_script || return 0
|
||||
return 0
|
||||
@@ -3765,6 +3765,10 @@ $PCT_OPTIONS_STRING"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
# Add /dev/kfd for AMD ROCm compute support
|
||||
if [[ -e /dev/kfd ]]; then
|
||||
AMD_DEVICES+=("/dev/kfd")
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for NVIDIA GPU - look for NVIDIA vendor ID [10de]
|
||||
@@ -4132,7 +4136,7 @@ EOF'
|
||||
# that sends "configuring" status AFTER the host already reported "failed"
|
||||
export CONTAINER_INSTALLING=true
|
||||
|
||||
lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/install/${var_install}.sh)"
|
||||
lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/install/"${var_install}".sh)"
|
||||
local lxc_exit=$?
|
||||
|
||||
unset CONTAINER_INSTALLING
|
||||
@@ -4249,7 +4253,7 @@ EOF'
|
||||
else
|
||||
msg_dev "Container ${CTID} kept for debugging"
|
||||
fi
|
||||
exit $install_exit_code
|
||||
exit "$install_exit_code"
|
||||
fi
|
||||
|
||||
# Prompt user for cleanup with 60s timeout
|
||||
@@ -4445,7 +4449,7 @@ EOF'
|
||||
echo -e "${BFR}${CM}${GN}MOTD/SSH ready - SSH into container: ssh root@${ct_ip}${CL}"
|
||||
fi
|
||||
fi
|
||||
exit $install_exit_code
|
||||
exit "$install_exit_code"
|
||||
;;
|
||||
3)
|
||||
# Retry with verbose mode (full rebuild)
|
||||
@@ -4510,7 +4514,7 @@ EOF'
|
||||
# Re-run install script in existing container (don't destroy/recreate)
|
||||
set +Eeuo pipefail
|
||||
trap - ERR
|
||||
lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/install/${var_install}.sh)"
|
||||
lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/install/"${var_install}".sh)"
|
||||
local apt_retry_exit=$?
|
||||
set -Eeuo pipefail
|
||||
trap 'error_handler' ERR
|
||||
@@ -4626,7 +4630,7 @@ EOF'
|
||||
|
||||
if [[ "$handled" == false ]]; then
|
||||
echo -e "\n${TAB}${YW}Invalid option. Container ${CTID} kept.${CL}"
|
||||
exit $install_exit_code
|
||||
exit "$install_exit_code"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
@@ -4647,7 +4651,7 @@ EOF'
|
||||
|
||||
# Restore default job-control signal handling before exit
|
||||
trap - TSTP TTIN TTOU
|
||||
exit $install_exit_code
|
||||
exit "$install_exit_code"
|
||||
fi
|
||||
|
||||
# Re-enable error handling after successful install or recovery menu completion
|
||||
@@ -5011,7 +5015,7 @@ create_lxc_container() {
|
||||
msg_ok "LXC stack upgraded."
|
||||
if [[ "$do_retry" == "yes" ]]; then
|
||||
msg_info "Retrying container creation after upgrade"
|
||||
if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
|
||||
if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "$PCT_OPTIONS" >>"$LOGFILE" 2>&1; then
|
||||
msg_ok "Container created successfully after upgrade."
|
||||
return 0
|
||||
else
|
||||
@@ -5266,7 +5270,7 @@ create_lxc_container() {
|
||||
fi
|
||||
fi
|
||||
|
||||
TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)"
|
||||
TEMPLATE_PATH="$(pvesm path "$TEMPLATE_STORAGE":vztmpl/"$TEMPLATE" 2>/dev/null || true)"
|
||||
if [[ -z "$TEMPLATE_PATH" ]]; then
|
||||
TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg)
|
||||
[[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE"
|
||||
@@ -5330,7 +5334,7 @@ create_lxc_container() {
|
||||
TEMPLATE_SOURCE="online"
|
||||
fi
|
||||
|
||||
TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)"
|
||||
TEMPLATE_PATH="$(pvesm path "$TEMPLATE_STORAGE":vztmpl/"$TEMPLATE" 2>/dev/null || true)"
|
||||
if [[ -z "$TEMPLATE_PATH" ]]; then
|
||||
TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg)
|
||||
[[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE"
|
||||
@@ -5526,7 +5530,7 @@ create_lxc_container() {
|
||||
msg_debug "Logfile: $LOGFILE"
|
||||
|
||||
# First attempt (PCT_OPTIONS is a multi-line string, use it directly)
|
||||
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >"$LOGFILE" 2>&1; then
|
||||
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "$PCT_OPTIONS" >"$LOGFILE" 2>&1; then
|
||||
msg_debug "Container creation failed on ${TEMPLATE_STORAGE}. Checking error..."
|
||||
|
||||
# Check if template issue - retry with fresh download
|
||||
@@ -5538,7 +5542,7 @@ create_lxc_container() {
|
||||
fi
|
||||
|
||||
# Retry after repair
|
||||
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
|
||||
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "$PCT_OPTIONS" >>"$LOGFILE" 2>&1; then
|
||||
# Fallback to local storage if not already on local
|
||||
if [[ "$TEMPLATE_STORAGE" != "local" ]]; then
|
||||
msg_info "Retrying container creation with fallback to local storage"
|
||||
@@ -5551,7 +5555,7 @@ create_lxc_container() {
|
||||
else
|
||||
msg_ok "Trying local storage fallback"
|
||||
fi
|
||||
if ! pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
|
||||
if ! pct create "$CTID" "local:vztmpl/${TEMPLATE}" "$PCT_OPTIONS" >>"$LOGFILE" 2>&1; then
|
||||
# Local fallback also failed - check for LXC stack version issue
|
||||
if grep -qiE 'unsupported .* version' "$LOGFILE"; then
|
||||
msg_warn "pct reported 'unsupported version' – LXC stack might be too old for this template"
|
||||
@@ -5574,7 +5578,7 @@ create_lxc_container() {
|
||||
msg_error "Container creation failed. See $LOGFILE"
|
||||
if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
|
||||
set -x
|
||||
pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE"
|
||||
pct create "$CTID" "local:vztmpl/${TEMPLATE}" "$PCT_OPTIONS" 2>&1 | tee -a "$LOGFILE"
|
||||
set +x
|
||||
fi
|
||||
_flush_pct_log
|
||||
@@ -5606,7 +5610,7 @@ create_lxc_container() {
|
||||
msg_error "Container creation failed. See $LOGFILE"
|
||||
if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
|
||||
set -x
|
||||
pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE"
|
||||
pct create "$CTID" "local:vztmpl/${TEMPLATE}" "$PCT_OPTIONS" 2>&1 | tee -a "$LOGFILE"
|
||||
set +x
|
||||
fi
|
||||
_flush_pct_log
|
||||
@@ -5664,33 +5668,33 @@ create_lxc_container() {
|
||||
description() {
|
||||
IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1)
|
||||
|
||||
# Generate LXC Description
|
||||
# Generate LXC Description - Heretek-AI themed
|
||||
DESCRIPTION=$(
|
||||
cat <<EOF
|
||||
<div align='center'>
|
||||
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
|
||||
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
|
||||
<a href='https://github.com/Heretek-AI/ProxmoxVE' target='_blank' rel='noopener noreferrer'>
|
||||
<img src='https://raw.githubusercontent.com/Heretek-AI/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Heretek-AI Logo' style='width:81px;height:112px;'/>
|
||||
</a>
|
||||
|
||||
<h2 style='font-size: 24px; margin: 20px 0;'>${APP} LXC</h2>
|
||||
<h2 style='font-size: 24px; margin: 20px 0; color: #dc2626;'>${APP} LXC</h2>
|
||||
|
||||
<p style='margin: 16px 0;'>
|
||||
<a href='https://ko-fi.com/community_scripts' target='_blank' rel='noopener noreferrer'>
|
||||
<img src='https://img.shields.io/badge/☕-Buy us a coffee-blue' alt='spend Coffee' />
|
||||
<a href='https://discord.gg/3AnUqsXnmK' target='_blank' rel='noopener noreferrer'>
|
||||
<img src='https://img.shields.io/badge/Discord-Join%20Server-5865F2?logo=discord&logoColor=white' alt='Discord' />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<span style='margin: 0 10px;'>
|
||||
<i class="fa fa-github fa-fw" style="color: #f5f5f5;"></i>
|
||||
<a href='https://github.com/community-scripts/ProxmoxVE' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>GitHub</a>
|
||||
<i class="fa fa-github fa-fw" style="color: #dc2626;"></i>
|
||||
<a href='https://github.com/Heretek-AI/ProxmoxVE' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #dc2626;'>GitHub</a>
|
||||
</span>
|
||||
<span style='margin: 0 10px;'>
|
||||
<i class="fa fa-comments fa-fw" style="color: #f5f5f5;"></i>
|
||||
<a href='https://github.com/community-scripts/ProxmoxVE/discussions' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>Discussions</a>
|
||||
<i class="fa fa-comments fa-fw" style="color: #dc2626;"></i>
|
||||
<a href='https://github.com/Heretek-AI/ProxmoxVE/discussions' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #dc2626;'>Discussions</a>
|
||||
</span>
|
||||
<span style='margin: 0 10px;'>
|
||||
<i class="fa fa-exclamation-circle fa-fw" style="color: #f5f5f5;"></i>
|
||||
<a href='https://github.com/community-scripts/ProxmoxVE/issues' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>Issues</a>
|
||||
<i class="fa fa-exclamation-circle fa-fw" style="color: #dc2626;"></i>
|
||||
<a href='https://github.com/Heretek-AI/ProxmoxVE/issues' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #dc2626;'>Issues</a>
|
||||
</span>
|
||||
</div>
|
||||
EOF
|
||||
|
||||
+26
-45
@@ -35,26 +35,22 @@ export default function Page() {
|
||||
const [color, setColor] = useState("#000000");
|
||||
|
||||
useEffect(() => {
|
||||
// Use Mechanicus-themed colors for particles
|
||||
setColor(theme === "dark" ? "#b45309" : "#92400e");
|
||||
// Use Heretek-themed colors - Blood Red
|
||||
setColor(theme === "dark" ? "#dc2626" : "#b91c1c");
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full mt-16 relative overflow-hidden">
|
||||
{/* Mechanicus Background Effects */}
|
||||
<div className="pointer-events-none absolute inset-0 scan-lines opacity-5" />
|
||||
{/* Heretek Background Effects */}
|
||||
<div className="pointer-events-none absolute inset-0 scan-lines opacity-10" />
|
||||
<div className="pointer-events-none absolute inset-0 noise-overlay" />
|
||||
|
||||
{/* Themed Particles */}
|
||||
<Particles
|
||||
className="absolute inset-0 -z-40"
|
||||
quantity={100}
|
||||
ease={80}
|
||||
color={color}
|
||||
theme="mechanicus"
|
||||
refresh
|
||||
/>
|
||||
{/* Circuit board pattern overlay */}
|
||||
<div className="pointer-events-none absolute inset-0 circuit-board opacity-5" />
|
||||
|
||||
{/* Themed Particles - Blood Red */}
|
||||
<Particles className="absolute inset-0 -z-40" quantity={100} ease={80} color={color} theme="heretek" refresh />
|
||||
|
||||
<div className="container mx-auto relative z-10">
|
||||
<div className="flex h-[60vh] flex-col items-center justify-center gap-4 py-20 lg:py-32">
|
||||
@@ -64,16 +60,14 @@ export default function Page() {
|
||||
<AnimatedGradientText>
|
||||
<div
|
||||
className={cn(
|
||||
`absolute inset-0 block size-full animate-gradient bg-gradient-to-r from-rust-500/50 via-corruption-500/50 to-brass-500/50 bg-[length:var(--bg-size)_100%] [border-radius:inherit] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]`,
|
||||
`absolute inset-0 block size-full animate-gradient bg-gradient-to-r from-blood-500/50 via-corruption-500/50 to-blood-400/50 bg-[length:var(--bg-size)_100%] [border-radius:inherit] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]`,
|
||||
`p-px ![mask-composite:subtract]`,
|
||||
)}
|
||||
/>
|
||||
⚙️
|
||||
{" "}
|
||||
<Separator className="mx-2 h-4" orientation="vertical" />
|
||||
⛧ <Separator className="mx-2 h-4" orientation="vertical" />
|
||||
<span
|
||||
className={cn(
|
||||
`animate-gradient bg-gradient-to-r from-rust-400 via-corruption-400 to-brass-400 bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`,
|
||||
`animate-gradient bg-gradient-to-r from-blood-400 via-corruption-400 to-blood-300 bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`,
|
||||
`inline`,
|
||||
)}
|
||||
>
|
||||
@@ -82,27 +76,25 @@ export default function Page() {
|
||||
</AnimatedGradientText>
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="mechanicus-panel">
|
||||
<DialogContent className="heretek-panel border-blood-500/30">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="font-[family-name:var(--font-cinzel)] brass-text">
|
||||
<DialogTitle className="font-[family-name:var(--font-cinzel)] text-blood-400 glitch">
|
||||
Praise the Omnissiah!
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
A big thank you to tteck and the many contributors who have made this project possible. Your hard
|
||||
work is truly appreciated by the entire Proxmox community!
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<CardFooter className="flex flex-col gap-2">
|
||||
<Button className="w-full" variant="mechanicus" asChild>
|
||||
<Button className="w-full" variant="heretek" asChild>
|
||||
<a
|
||||
href="https://github.com/tteck"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<FaGithub className="mr-2 h-4 w-4" />
|
||||
{" "}
|
||||
Tteck's GitHub
|
||||
<FaGithub className="mr-2 h-4 w-4" /> Tteck's GitHub
|
||||
</a>
|
||||
</Button>
|
||||
<Button className="w-full" variant="forge" asChild>
|
||||
@@ -112,9 +104,7 @@ export default function Page() {
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<ExternalLink className="mr-2 h-4 w-4" />
|
||||
{" "}
|
||||
Proxmox Helper Scripts
|
||||
<ExternalLink className="mr-2 h-4 w-4" /> Proxmox Helper Scripts
|
||||
</a>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
@@ -123,36 +113,27 @@ export default function Page() {
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<h1 className="max-w-2xl text-center text-3xl font-semibold tracking-tighter md:text-7xl font-[family-name:var(--font-cinzel)]">
|
||||
<span className="brass-text">Heretek-AI</span>
|
||||
<span className="steel-text glitch">Heretek-AI</span>
|
||||
</h1>
|
||||
<div className="max-w-2xl gap-2 flex flex-col text-center sm:text-lg text-sm leading-relaxed tracking-tight text-muted-foreground md:text-xl">
|
||||
<p className="text-rust-300 font-semibold">
|
||||
Uncompliant scripts, made quicker.
|
||||
</p>
|
||||
<p className="text-blood-400 font-semibold animate-pulse">Uncompliant scripts, made quicker.</p>
|
||||
<p>
|
||||
Scripts that don't meet the strict guidelines of the official
|
||||
{" "}
|
||||
Scripts that don't meet the strict guidelines of the official{" "}
|
||||
<a
|
||||
href="https://github.com/community-scripts/ProxmoxVE"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline text-corruption-400 hover:text-corruption-300 transition-colors"
|
||||
className="underline text-blood-400 hover:text-blood-300 transition-colors"
|
||||
>
|
||||
Community-Scripts
|
||||
</a>
|
||||
{" "}
|
||||
</a>{" "}
|
||||
repository, but are updated faster and built with flexibility in mind.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-3">
|
||||
<Link href="/scripts">
|
||||
<Button
|
||||
size="lg"
|
||||
variant="mechanicus"
|
||||
Icon={CustomArrowRightIcon}
|
||||
iconPlacement="right"
|
||||
>
|
||||
<Button size="lg" variant="heretek" Icon={CustomArrowRightIcon} iconPlacement="right">
|
||||
View Scripts
|
||||
</Button>
|
||||
</Link>
|
||||
@@ -160,7 +141,7 @@ export default function Page() {
|
||||
<Button
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="border-rust-500/50 hover:border-rust-400 hover:bg-rust-500/10"
|
||||
className="border-blood-500/50 hover:border-blood-400 hover:bg-blood-500/10"
|
||||
>
|
||||
Browse Categories
|
||||
</Button>
|
||||
@@ -183,7 +164,7 @@ export default function Page() {
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl font-bold tracking-tighter md:text-5xl mb-4 font-[family-name:var(--font-cinzel)]">
|
||||
<span className="brass-text">Frequently Asked Questions</span>
|
||||
<span className="steel-text">Frequently Asked Questions</span>
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
Find answers to common questions about our Proxmox VE scripts
|
||||
|
||||
+24
-7
@@ -36,7 +36,7 @@ const features = [
|
||||
export function FeatureCards() {
|
||||
return (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
{features.map(feature => (
|
||||
{features.map((feature) => (
|
||||
<Link
|
||||
key={feature.title}
|
||||
href={feature.href}
|
||||
@@ -44,20 +44,37 @@ export function FeatureCards() {
|
||||
rel={feature.external ? "noopener noreferrer" : undefined}
|
||||
className="group"
|
||||
>
|
||||
<Card className="h-full transition-all duration-300 hover:border-rust-500/50 hover:shadow-lg hover:shadow-rust-500/10 rust-border">
|
||||
<CardHeader>
|
||||
<div className="mb-2 text-rust-400 group-hover:text-rust-300 transition-colors duration-300 group-hover:animate-heretic-glow">
|
||||
<Card className="h-full transition-all duration-300 hover:border-blood-500/60 hover:shadow-lg hover:shadow-blood-500/20 heretek-card glitch relative overflow-hidden">
|
||||
{/* Glitch overlay on hover */}
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-blood-500/5 via-transparent to-blood-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
|
||||
{/* Scan line effect */}
|
||||
<div className="absolute inset-0 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-blood-500/5 to-transparent animate-scan-line" />
|
||||
</div>
|
||||
|
||||
<CardHeader className="relative z-10">
|
||||
<div className="mb-2 text-blood-400 group-hover:text-blood-300 transition-colors duration-300 group-hover:animate-heretic-glow">
|
||||
{feature.icon}
|
||||
</div>
|
||||
<CardTitle className="text-lg font-[family-name:var(--font-cinzel)] group-hover:text-brass-400 transition-colors duration-300">
|
||||
{feature.title}
|
||||
<CardTitle className="text-lg font-[family-name:var(--font-cinzel)] group-hover:text-blood-400 transition-colors duration-300 relative">
|
||||
<span className="relative inline-block">
|
||||
{feature.title}
|
||||
{/* Underline glitch effect */}
|
||||
<span className="absolute bottom-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-blood-500 to-transparent transform scale-x-0 group-hover:scale-x-100 transition-transform duration-300" />
|
||||
</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="relative z-10">
|
||||
<CardDescription className="text-muted-foreground group-hover:text-foreground/80 transition-colors duration-300">
|
||||
{feature.description}
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
|
||||
{/* Corner accent */}
|
||||
<div className="absolute top-0 right-0 w-8 h-8 overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-16 h-16 bg-gradient-to-br from-blood-500/20 to-transparent transform rotate-45 translate-x-8 -translate-y-8 group-hover:translate-x-4 group-hover:-translate-y-4 transition-transform duration-300" />
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
+23
-9
@@ -8,17 +8,16 @@ import { buttonVariants } from "./ui/button";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<div className="supports-backdrop-blur:bg-background/90 mt-auto border-t w-full flex justify-between border-border bg-background/40 py-4 backdrop-blur-lg rust-border metal-surface">
|
||||
<div className="supports-backdrop-blur:bg-background/90 mt-auto border-t w-full flex justify-between border-border bg-background/40 py-4 backdrop-blur-lg heretek-panel">
|
||||
<div className="mx-6 w-full flex justify-between text-xs sm:text-sm text-muted-foreground">
|
||||
<div className="flex items-center">
|
||||
<p>
|
||||
Website built by the community. The source code is available on
|
||||
{" "}
|
||||
Website built by the community. The source code is available on{" "}
|
||||
<Link
|
||||
href={`https://github.com/Heretek-AI/${basePath}/tree/main/frontend`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="font-semibold underline-offset-2 duration-300 hover:underline text-rust-400 hover:text-rust-300"
|
||||
className="font-semibold underline-offset-2 duration-300 hover:underline text-blood-400 hover:text-blood-300"
|
||||
data-umami-event="View Website Source Code on Github"
|
||||
>
|
||||
GitHub
|
||||
@@ -29,35 +28,50 @@ export default function Footer() {
|
||||
<div className="sm:flex hidden gap-2">
|
||||
<Link
|
||||
href="/scripts"
|
||||
className={cn(buttonVariants({ variant: "link" }), "text-muted-foreground flex items-center gap-2 hover:text-rust-400 transition-colors duration-300")}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "link" }),
|
||||
"text-muted-foreground flex items-center gap-2 hover:text-blood-400 transition-colors duration-300",
|
||||
)}
|
||||
>
|
||||
<FileJson className="h-4 w-4" />
|
||||
Scripts
|
||||
</Link>
|
||||
<Link
|
||||
href="/categories"
|
||||
className={cn(buttonVariants({ variant: "link" }), "text-muted-foreground flex items-center gap-2 hover:text-rust-400 transition-colors duration-300")}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "link" }),
|
||||
"text-muted-foreground flex items-center gap-2 hover:text-blood-400 transition-colors duration-300",
|
||||
)}
|
||||
>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
Categories
|
||||
</Link>
|
||||
<Link
|
||||
href="/community"
|
||||
className={cn(buttonVariants({ variant: "link" }), "text-muted-foreground flex items-center gap-2 hover:text-rust-400 transition-colors duration-300")}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "link" }),
|
||||
"text-muted-foreground flex items-center gap-2 hover:text-blood-400 transition-colors duration-300",
|
||||
)}
|
||||
>
|
||||
<MessageCircle className="h-4 w-4" />
|
||||
Community
|
||||
</Link>
|
||||
<Link
|
||||
href="/json-editor"
|
||||
className={cn(buttonVariants({ variant: "link" }), "text-muted-foreground flex items-center gap-2 hover:text-rust-400 transition-colors duration-300")}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "link" }),
|
||||
"text-muted-foreground flex items-center gap-2 hover:text-blood-400 transition-colors duration-300",
|
||||
)}
|
||||
>
|
||||
<FileJson className="h-4 w-4" />
|
||||
JSON Editor
|
||||
</Link>
|
||||
<Link
|
||||
href="/data"
|
||||
className={cn(buttonVariants({ variant: "link" }), "text-muted-foreground flex items-center gap-2 hover:text-rust-400 transition-colors duration-300")}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "link" }),
|
||||
"text-muted-foreground flex items-center gap-2 hover:text-blood-400 transition-colors duration-300",
|
||||
)}
|
||||
>
|
||||
<Server className="h-4 w-4" />
|
||||
API Data
|
||||
|
||||
+36
-33
@@ -32,10 +32,9 @@ function Navbar() {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`fixed left-0 top-0 z-50 flex w-screen justify-center px-4 xl:px-0 transition-all duration-300 ${isScrolled
|
||||
? "glass rust-border border-b bg-background/50"
|
||||
: ""
|
||||
}`}
|
||||
className={`fixed left-0 top-0 z-50 flex w-screen justify-center px-4 xl:px-0 transition-all duration-300 ${
|
||||
isScrolled ? "glass blood-border border-b bg-background/50" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex h-20 w-full max-w-[1440px] items-center justify-between sm:flex-row">
|
||||
<Link
|
||||
@@ -45,7 +44,7 @@ function Navbar() {
|
||||
<div className="relative machine-icon">
|
||||
<Image height={18} unoptimized width={18} alt="logo" src="/ProxmoxVE/logo.png" className="" />
|
||||
</div>
|
||||
<span className="font-[family-name:var(--font-cinzel)] tracking-wide text-foreground group-hover:text-rust-400 transition-colors duration-300">
|
||||
<span className="font-[family-name:var(--font-cinzel)] tracking-wide text-foreground group-hover:text-blood-400 transition-colors duration-300 glitch">
|
||||
Heretek AI
|
||||
</span>
|
||||
</Link>
|
||||
@@ -56,38 +55,42 @@ function Navbar() {
|
||||
</Suspense>
|
||||
</div>
|
||||
<div className="hidden sm:flex items-center gap-2">
|
||||
{navbarLinks.filter(link => !link.external).map(({ href, event, icon, text }) => (
|
||||
<Button
|
||||
key={event}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
asChild
|
||||
className="text-muted-foreground hover:text-rust-400 hover:bg-rust-500/10 transition-colors duration-300"
|
||||
>
|
||||
<Link href={href} data-umami-event={event}>
|
||||
{icon}
|
||||
<span className="ml-2 hidden lg:inline">{text}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
))}
|
||||
{navbarLinks
|
||||
.filter((link) => !link.external)
|
||||
.map(({ href, event, icon, text }) => (
|
||||
<Button
|
||||
key={event}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
asChild
|
||||
className="text-muted-foreground hover:text-blood-400 hover:bg-blood-500/10 transition-colors duration-300"
|
||||
>
|
||||
<Link href={href} data-umami-event={event}>
|
||||
{icon}
|
||||
<span className="ml-2 hidden lg:inline">{text}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex sm:gap-2">
|
||||
<CommandMenu />
|
||||
<GitHubStarsButton username="Heretek-AI" repo="ProxmoxVE" className="hidden md:flex" />
|
||||
{navbarLinks.filter(link => link.external).map(({ href, event, icon, text, mobileHidden }) => (
|
||||
<Button
|
||||
key={event}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
asChild
|
||||
className={`text-muted-foreground hover:text-rust-400 hover:bg-rust-500/10 transition-colors duration-300 ${mobileHidden ? "hidden lg:flex" : ""}`}
|
||||
>
|
||||
<Link target="_blank" href={href} data-umami-event={event}>
|
||||
{icon}
|
||||
<span className="sr-only">{text}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
))}
|
||||
{navbarLinks
|
||||
.filter((link) => link.external)
|
||||
.map(({ href, event, icon, text, mobileHidden }) => (
|
||||
<Button
|
||||
key={event}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
asChild
|
||||
className={`text-muted-foreground hover:text-blood-400 hover:bg-blood-500/10 transition-colors duration-300 ${mobileHidden ? "hidden lg:flex" : ""}`}
|
||||
>
|
||||
<Link target="_blank" href={href} data-umami-event={event}>
|
||||
{icon}
|
||||
<span className="sr-only">{text}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
))}
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+24
-39
@@ -12,16 +12,12 @@ const buttonVariants = cva(
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
expandIcon:
|
||||
"group relative text-primary-foreground bg-primary hover:bg-primary/90",
|
||||
expandIcon: "group relative text-primary-foreground bg-primary hover:bg-primary/90",
|
||||
ringHover:
|
||||
"bg-primary text-primary-foreground transition-all duration-300 hover:bg-primary/90 hover:ring-2 hover:ring-primary/90 hover:ring-offset-2",
|
||||
shine:
|
||||
@@ -34,17 +30,23 @@ const buttonVariants = cva(
|
||||
"relative after:absolute after:bg-primary after:bottom-2 after:h-[1px] after:w-2/3 after:origin-bottom-left after:scale-x-100 hover:after:origin-bottom-right hover:after:scale-x-0 after:transition-transform after:ease-in-out after:duration-300",
|
||||
linkHover2:
|
||||
"relative after:absolute after:bg-primary after:bottom-2 after:h-[1px] after:w-2/3 after:origin-bottom-right after:scale-x-0 hover:after:origin-bottom-left hover:after:scale-x-100 after:transition-transform after:ease-in-out after:duration-300",
|
||||
// Mechanicus-themed variants
|
||||
mechanicus:
|
||||
"bg-gradient-to-r from-rust-600 to-rust-700 text-rust-100 border border-brass-500/30 hover:from-rust-500 hover:to-rust-600 hover:border-brass-400/50 hover:shadow-[0_0_15px_hsl(28_70%_45%_/_0.3)] transition-all duration-300",
|
||||
brass:
|
||||
"bg-gradient-to-r from-brass-500 to-brass-600 text-brass-100 border border-brass-400/30 hover:from-brass-400 hover:to-brass-500 hover:shadow-[0_0_15px_hsl(43_70%_50%_/_0.3)] transition-all duration-300",
|
||||
// Heretek-themed variants - Blood & Steel
|
||||
heretek:
|
||||
"bg-gradient-to-r from-blood-600 to-blood-700 text-blood-100 border border-blood-500/30 hover:from-blood-500 hover:to-blood-600 hover:border-blood-400/50 hover:shadow-[0_0_15px_hsl(0_80%_50%_/_0.4)] transition-all duration-300",
|
||||
blood:
|
||||
"bg-gradient-to-r from-blood-500 to-blood-600 text-blood-100 border border-blood-400/30 hover:from-blood-400 hover:to-blood-500 hover:shadow-[0_0_20px_hsl(0_85%_55%_/_0.5)] transition-all duration-300",
|
||||
void: "bg-gradient-to-r from-void-700 to-void-800 text-void-100 border border-void-500/30 hover:from-void-600 hover:to-void-700 hover:shadow-[0_0_15px_hsl(0_0%_20%_/_0.4)] transition-all duration-300",
|
||||
steel:
|
||||
"bg-gradient-to-r from-steel-600 to-steel-700 text-steel-100 border border-steel-500/30 hover:from-steel-500 hover:to-steel-600 hover:shadow-[0_0_10px_hsl(0_15%_35%_/_0.3)] transition-all duration-300",
|
||||
corruption:
|
||||
"bg-gradient-to-r from-corruption-600 to-corruption-700 text-corruption-100 border border-corruption-500/30 hover:from-corruption-500 hover:to-corruption-600 hover:shadow-[0_0_15px_hsl(145_60%_40%_/_0.3)] hover:animate-corrupted-pulse transition-all duration-300",
|
||||
"bg-gradient-to-r from-corruption-600 to-corruption-700 text-corruption-100 border border-corruption-500/30 hover:from-corruption-500 hover:to-corruption-600 hover:shadow-[0_0_15px_hsl(0_70%_45%_/_0.4)] hover:animate-corrupted-pulse transition-all duration-300",
|
||||
glitch:
|
||||
"bg-gradient-to-r from-blood-600 to-void-700 text-void-100 border border-blood-500/30 hover:from-blood-500 hover:to-void-600 hover:border-blood-400/50 hover:shadow-[0_0_20px_hsl(0_80%_50%_/_0.5)] animate-glitch transition-all duration-300",
|
||||
mechanicus:
|
||||
"bg-gradient-to-r from-blood-600 to-blood-700 text-blood-100 border border-blood-500/30 hover:from-blood-500 hover:to-blood-600 hover:border-blood-400/50 hover:shadow-[0_0_15px_hsl(0_80%_50%_/_0.4)] transition-all duration-300",
|
||||
forge:
|
||||
"bg-gradient-to-r from-copper-600 to-copper-700 text-copper-100 border border-copper-500/30 hover:from-copper-500 hover:to-copper-600 hover:shadow-[0_0_20px_hsl(25_70%_45%_/_0.4)] transition-all duration-300",
|
||||
iron:
|
||||
"bg-gradient-to-r from-iron-700 to-iron-800 text-iron-100 border border-iron-500/30 hover:from-iron-600 hover:to-iron-700 hover:shadow-[0_0_10px_hsl(30_20%_30%_/_0.3)] transition-all duration-300",
|
||||
"bg-gradient-to-r from-steel-600 to-blood-700 text-steel-100 border border-steel-500/30 hover:from-steel-500 hover:to-blood-600 hover:shadow-[0_0_20px_hsl(0_70%_45%_/_0.5)] transition-all duration-300",
|
||||
iron: "bg-gradient-to-r from-iron-700 to-iron-800 text-iron-100 border border-iron-500/30 hover:from-iron-600 hover:to-iron-700 hover:shadow-[0_0_10px_hsl(0_10%_30%_/_0.3)] transition-all duration-300",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
@@ -73,33 +75,16 @@ type IconRefProps = {
|
||||
|
||||
export type ButtonProps = {
|
||||
asChild?: boolean;
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants>;
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement> &
|
||||
VariantProps<typeof buttonVariants>;
|
||||
|
||||
export type ButtonIconProps = IconProps | IconRefProps;
|
||||
|
||||
const Button = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ButtonProps & ButtonIconProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
Icon,
|
||||
iconPlacement,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps & ButtonIconProps>(
|
||||
({ className, variant, size, asChild = false, Icon, iconPlacement, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props}>
|
||||
{Icon && iconPlacement === "left" && (
|
||||
<div className="group-hover:translate-x-100 w-0 translate-x-[0%] pr-0 opacity-0 transition-all duration-200 group-hover:w-5 group-hover:pr-2 group-hover:opacity-100">
|
||||
<Icon />
|
||||
|
||||
+450
-238
@@ -4,33 +4,33 @@
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
/* Heretek Light Theme - Worn Metal */
|
||||
--background: 30 15% 12%;
|
||||
--foreground: 35 20% 85%;
|
||||
--card: 30 12% 10%;
|
||||
--card-foreground: 35 20% 85%;
|
||||
--popover: 30 15% 8%;
|
||||
--popover-foreground: 35 20% 85%;
|
||||
--primary: 28 80% 45%;
|
||||
--primary-foreground: 30 15% 8%;
|
||||
--secondary: 35 30% 25%;
|
||||
--secondary-foreground: 35 20% 85%;
|
||||
--muted: 30 20% 20%;
|
||||
--muted-foreground: 35 15% 55%;
|
||||
--accent: 145 65% 35%;
|
||||
--accent-foreground: 145 10% 95%;
|
||||
--destructive: 0 75% 45%;
|
||||
--destructive-foreground: 0 10% 95%;
|
||||
--border: 30 25% 22%;
|
||||
--input: 30 25% 18%;
|
||||
--ring: 28 80% 45%;
|
||||
/* Heretek Light Theme - Dark Metal with Red Accents */
|
||||
--background: 0 0% 8%;
|
||||
--foreground: 0 0% 88%;
|
||||
--card: 0 0% 6%;
|
||||
--card-foreground: 0 0% 88%;
|
||||
--popover: 0 0% 4%;
|
||||
--popover-foreground: 0 0% 88%;
|
||||
--primary: 0 75% 50%;
|
||||
--primary-foreground: 0 0% 4%;
|
||||
--secondary: 0 10% 18%;
|
||||
--secondary-foreground: 0 0% 85%;
|
||||
--muted: 0 8% 14%;
|
||||
--muted-foreground: 0 5% 55%;
|
||||
--accent: 0 70% 45%;
|
||||
--accent-foreground: 0 0% 95%;
|
||||
--destructive: 0 80% 50%;
|
||||
--destructive-foreground: 0 0% 95%;
|
||||
--border: 0 12% 20%;
|
||||
--input: 0 10% 12%;
|
||||
--ring: 0 75% 50%;
|
||||
--radius: 0.25rem;
|
||||
/* Heretek Chart Colors - Rust & Corruption */
|
||||
--chart-1: 28 75% 45%;
|
||||
--chart-2: 35 60% 40%;
|
||||
--chart-3: 145 55% 35%;
|
||||
--chart-4: 180 40% 30%;
|
||||
--chart-5: 0 60% 40%;
|
||||
/* Heretek Chart Colors - Blood & Steel */
|
||||
--chart-1: 0 75% 50%;
|
||||
--chart-2: 0 60% 35%;
|
||||
--chart-3: 0 50% 45%;
|
||||
--chart-4: 0 40% 30%;
|
||||
--chart-5: 0 65% 40%;
|
||||
}
|
||||
|
||||
::selection {
|
||||
@@ -39,32 +39,32 @@
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* Heretek Dark Theme - Corrupted Forge */
|
||||
--background: 30 20% 4%;
|
||||
--foreground: 35 15% 80%;
|
||||
--card: 30 18% 6%;
|
||||
--card-foreground: 35 15% 80%;
|
||||
--popover: 30 20% 3%;
|
||||
--popover-foreground: 35 15% 80%;
|
||||
--primary: 28 85% 50%;
|
||||
--primary-foreground: 30 25% 5%;
|
||||
--secondary: 35 25% 15%;
|
||||
--secondary-foreground: 35 15% 80%;
|
||||
--muted: 30 20% 12%;
|
||||
--muted-foreground: 35 12% 50%;
|
||||
--accent: 145 70% 40%;
|
||||
--accent-foreground: 145 15% 95%;
|
||||
--destructive: 0 70% 40%;
|
||||
--destructive-foreground: 0 15% 95%;
|
||||
--border: 30 30% 18%;
|
||||
--input: 30 25% 12%;
|
||||
--ring: 28 85% 50%;
|
||||
/* Heretek Chart Colors - Corruption Spread */
|
||||
--chart-1: 28 80% 50%;
|
||||
--chart-2: 35 55% 35%;
|
||||
--chart-3: 145 60% 38%;
|
||||
--chart-4: 60 45% 30%;
|
||||
--chart-5: 0 65% 42%;
|
||||
/* Heretek Dark Theme - The Void Stares Back */
|
||||
--background: 0 0% 3%;
|
||||
--foreground: 0 0% 85%;
|
||||
--card: 0 0% 5%;
|
||||
--card-foreground: 0 0% 85%;
|
||||
--popover: 0 0% 2%;
|
||||
--popover-foreground: 0 0% 85%;
|
||||
--primary: 0 80% 55%;
|
||||
--primary-foreground: 0 0% 3%;
|
||||
--secondary: 0 8% 12%;
|
||||
--secondary-foreground: 0 0% 82%;
|
||||
--muted: 0 6% 10%;
|
||||
--muted-foreground: 0 5% 50%;
|
||||
--accent: 0 75% 50%;
|
||||
--accent-foreground: 0 0% 95%;
|
||||
--destructive: 0 85% 55%;
|
||||
--destructive-foreground: 0 0% 95%;
|
||||
--border: 0 15% 15%;
|
||||
--input: 0 10% 8%;
|
||||
--ring: 0 80% 55%;
|
||||
/* Heretek Chart Colors - Corruption Spreads */
|
||||
--chart-1: 0 80% 55%;
|
||||
--chart-2: 0 65% 40%;
|
||||
--chart-3: 0 55% 45%;
|
||||
--chart-4: 0 45% 35%;
|
||||
--chart-5: 0 70% 45%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,9 @@
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-feature-settings: "liga" 1, "calt" 1;
|
||||
font-feature-settings:
|
||||
"liga" 1,
|
||||
"calt" 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,45 +85,36 @@
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
/* Heretek Scrollbar - Corrupted Metal */
|
||||
/* Heretek Scrollbar - Dark Steel */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: hsl(30 20% 8%);
|
||||
border-left: 1px solid hsl(30 30% 18%);
|
||||
background: hsl(0 0% 4%);
|
||||
border-left: 1px solid hsl(0 15% 12%);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(180deg,
|
||||
hsl(28 70% 35%) 0%,
|
||||
hsl(35 50% 25%) 50%,
|
||||
hsl(28 70% 35%) 100%);
|
||||
background: linear-gradient(180deg, hsl(0 60% 30%) 0%, hsl(0 40% 20%) 50%, hsl(0 60% 30%) 100%);
|
||||
border-radius: 4px;
|
||||
border: 1px solid hsl(30 30% 20%);
|
||||
border: 1px solid hsl(0 20% 15%);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(180deg,
|
||||
hsl(28 80% 42%) 0%,
|
||||
hsl(35 60% 32%) 50%,
|
||||
hsl(28 80% 42%) 100%);
|
||||
background: linear-gradient(180deg, hsl(0 70% 40%) 0%, hsl(0 50% 28%) 50%, hsl(0 70% 40%) 100%);
|
||||
}
|
||||
|
||||
/* Glass Effect - Corrupted Viewport */
|
||||
.glass {
|
||||
backdrop-filter: blur(12px) saturate(120%);
|
||||
-webkit-backdrop-filter: blur(12px) saturate(120%);
|
||||
background: linear-gradient(135deg,
|
||||
hsl(30 20% 8% / 0.85) 0%,
|
||||
hsl(30 25% 12% / 0.75) 100%);
|
||||
border: 1px solid hsl(30 30% 18% / 0.6);
|
||||
background: linear-gradient(135deg, hsl(0 0% 5% / 0.9) 0%, hsl(0 5% 8% / 0.8) 100%);
|
||||
border: 1px solid hsl(0 20% 15% / 0.6);
|
||||
}
|
||||
|
||||
/* Heretek Corruption Effects */
|
||||
@layer utilities {
|
||||
|
||||
/* Noise Texture Overlay */
|
||||
.noise-overlay {
|
||||
position: relative;
|
||||
@@ -132,82 +125,88 @@
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
|
||||
opacity: 0.03;
|
||||
opacity: 0.04;
|
||||
pointer-events: none;
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
|
||||
/* Rust Border Effect */
|
||||
.rust-border {
|
||||
/* Blood Border Effect */
|
||||
.blood-border {
|
||||
border: 1px solid transparent;
|
||||
background:
|
||||
linear-gradient(hsl(30 20% 6%), hsl(30 20% 6%)) padding-box,
|
||||
linear-gradient(135deg,
|
||||
hsl(28 70% 30%) 0%,
|
||||
hsl(35 40% 20%) 25%,
|
||||
hsl(28 60% 25%) 50%,
|
||||
hsl(35 35% 18%) 75%,
|
||||
hsl(28 70% 30%) 100%) border-box;
|
||||
linear-gradient(hsl(0 0% 5%), hsl(0 0% 5%)) padding-box,
|
||||
linear-gradient(
|
||||
135deg,
|
||||
hsl(0 70% 35%) 0%,
|
||||
hsl(0 50% 20%) 25%,
|
||||
hsl(0 60% 30%) 50%,
|
||||
hsl(0 40% 18%) 75%,
|
||||
hsl(0 70% 35%) 100%
|
||||
)
|
||||
border-box;
|
||||
}
|
||||
|
||||
/* Corruption Glow */
|
||||
/* Corruption Glow - Red */
|
||||
.corruption-glow {
|
||||
box-shadow:
|
||||
0 0 20px hsl(145 60% 30% / 0.15),
|
||||
0 0 40px hsl(145 50% 25% / 0.1),
|
||||
inset 0 0 20px hsl(145 40% 20% / 0.05);
|
||||
0 0 20px hsl(0 70% 40% / 0.2),
|
||||
0 0 40px hsl(0 60% 30% / 0.15),
|
||||
inset 0 0 20px hsl(0 50% 25% / 0.08);
|
||||
}
|
||||
|
||||
/* Brass Text Effect */
|
||||
.brass-text {
|
||||
background: linear-gradient(180deg,
|
||||
hsl(43 70% 55%) 0%,
|
||||
hsl(35 60% 40%) 50%,
|
||||
hsl(43 65% 45%) 100%);
|
||||
/* Steel Text Effect */
|
||||
.steel-text {
|
||||
background: linear-gradient(180deg, hsl(0 0% 75%) 0%, hsl(0 5% 55%) 50%, hsl(0 0% 65%) 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
/* Copper Accent */
|
||||
.copper-accent {
|
||||
background: linear-gradient(135deg,
|
||||
hsl(28 75% 45%) 0%,
|
||||
hsl(25 65% 35%) 50%,
|
||||
hsl(28 70% 40%) 100%);
|
||||
/* Blood Accent */
|
||||
.blood-accent {
|
||||
background: linear-gradient(135deg, hsl(0 75% 50%) 0%, hsl(0 65% 40%) 50%, hsl(0 75% 45%) 100%);
|
||||
}
|
||||
|
||||
/* Glitch Animation */
|
||||
/* Enhanced Glitch Animation */
|
||||
.glitch {
|
||||
animation: glitch 4s infinite;
|
||||
animation: glitch 3s infinite;
|
||||
}
|
||||
|
||||
@keyframes glitch {
|
||||
|
||||
0%,
|
||||
90%,
|
||||
88%,
|
||||
100% {
|
||||
transform: translate(0);
|
||||
filter: none;
|
||||
}
|
||||
|
||||
89% {
|
||||
transform: translate(-2px, 1px);
|
||||
filter: hue-rotate(90deg) saturate(1.5);
|
||||
}
|
||||
|
||||
90% {
|
||||
transform: translate(2px, -1px);
|
||||
filter: hue-rotate(-90deg) saturate(1.5);
|
||||
}
|
||||
|
||||
91% {
|
||||
transform: translate(-1px, 1px);
|
||||
filter: hue-rotate(90deg);
|
||||
transform: translate(-1px, -1px);
|
||||
filter: hue-rotate(45deg) brightness(1.2);
|
||||
}
|
||||
|
||||
92% {
|
||||
transform: translate(1px, -1px);
|
||||
filter: hue-rotate(-90deg);
|
||||
transform: translate(1px, 1px);
|
||||
filter: none;
|
||||
}
|
||||
|
||||
93% {
|
||||
transform: translate(-1px, -1px);
|
||||
filter: hue-rotate(45deg);
|
||||
transform: translate(-2px, 2px);
|
||||
filter: hue-rotate(-45deg) saturate(1.3);
|
||||
}
|
||||
|
||||
94% {
|
||||
transform: translate(1px, 1px);
|
||||
transform: translate(2px, -2px);
|
||||
filter: none;
|
||||
}
|
||||
}
|
||||
@@ -217,93 +216,95 @@
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: repeating-linear-gradient(0deg,
|
||||
transparent,
|
||||
transparent 2px,
|
||||
hsl(30 20% 2% / 0.3) 2px,
|
||||
hsl(30 20% 2% / 0.3) 4px);
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent,
|
||||
transparent 2px,
|
||||
hsl(0 0% 2% / 0.4) 2px,
|
||||
hsl(0 0% 2% / 0.4) 4px
|
||||
);
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Flicker Animation */
|
||||
.flicker {
|
||||
animation: flicker 8s infinite;
|
||||
animation: flicker 6s infinite;
|
||||
}
|
||||
|
||||
@keyframes flicker {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
90% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
91% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
92% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
93% {
|
||||
opacity: 0.8;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
94% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
96% {
|
||||
95% {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
97% {
|
||||
96% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Corrupted Pulse */
|
||||
/* Corrupted Pulse - Red */
|
||||
.corrupted-pulse {
|
||||
animation: corrupted-pulse 3s ease-in-out infinite;
|
||||
animation: corrupted-pulse 2.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes corrupted-pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
box-shadow:
|
||||
0 0 10px hsl(145 50% 25% / 0.2),
|
||||
0 0 20px hsl(145 40% 20% / 0.1);
|
||||
0 0 10px hsl(0 65% 40% / 0.25),
|
||||
0 0 20px hsl(0 55% 30% / 0.15);
|
||||
}
|
||||
|
||||
50% {
|
||||
box-shadow:
|
||||
0 0 15px hsl(145 60% 30% / 0.3),
|
||||
0 0 30px hsl(145 50% 25% / 0.15),
|
||||
0 0 45px hsl(145 40% 20% / 0.1);
|
||||
0 0 15px hsl(0 75% 45% / 0.35),
|
||||
0 0 30px hsl(0 65% 35% / 0.2),
|
||||
0 0 45px hsl(0 55% 25% / 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Metal Surface Gradient */
|
||||
/* Dark Metal Surface Gradient */
|
||||
.metal-surface {
|
||||
background:
|
||||
linear-gradient(145deg,
|
||||
hsl(30 15% 15%) 0%,
|
||||
hsl(30 20% 10%) 50%,
|
||||
hsl(30 15% 12%) 100%);
|
||||
background: linear-gradient(145deg, hsl(0 0% 10%) 0%, hsl(0 5% 6%) 50%, hsl(0 0% 8%) 100%);
|
||||
box-shadow:
|
||||
inset 1px 1px 0 hsl(30 20% 20%),
|
||||
inset -1px -1px 0 hsl(30 25% 5%);
|
||||
inset 1px 1px 0 hsl(0 10% 15%),
|
||||
inset -1px -1px 0 hsl(0 15% 3%);
|
||||
}
|
||||
|
||||
/* Tech Heresy Warning */
|
||||
/* Heresy Warning - Enhanced */
|
||||
.heresy-warning {
|
||||
border-left: 3px solid hsl(0 70% 40%);
|
||||
background: linear-gradient(90deg,
|
||||
hsl(0 50% 10% / 0.3) 0%,
|
||||
transparent 100%);
|
||||
border-left: 3px solid hsl(0 80% 50%);
|
||||
background: linear-gradient(90deg, hsl(0 60% 15% / 0.4) 0%, transparent 100%);
|
||||
}
|
||||
|
||||
/* ===== NEW MECHANICUS EFFECTS ===== */
|
||||
/* ===== ENHANCED HERETEK EFFECTS ===== */
|
||||
|
||||
/* Pulsing Circuit Lines */
|
||||
/* Pulsing Circuit Lines - Red */
|
||||
.circuit-pulse {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@@ -313,11 +314,8 @@
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
hsl(145 60% 35% / 0.15) 50%,
|
||||
transparent 100%);
|
||||
animation: circuit-flow 3s linear infinite;
|
||||
background: linear-gradient(90deg, transparent 0%, hsl(0 70% 45% / 0.2) 50%, transparent 100%);
|
||||
animation: circuit-flow 2.5s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes circuit-flow {
|
||||
@@ -330,19 +328,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Rusted Metal Texture */
|
||||
.rusted-metal {
|
||||
background:
|
||||
linear-gradient(145deg,
|
||||
hsl(30 15% 12%) 0%,
|
||||
hsl(28 20% 8%) 50%,
|
||||
hsl(30 12% 10%) 100%);
|
||||
/* Dark Steel Texture */
|
||||
.dark-steel {
|
||||
background: linear-gradient(145deg, hsl(0 0% 8%) 0%, hsl(0 5% 5%) 50%, hsl(0 0% 6%) 100%);
|
||||
box-shadow:
|
||||
inset 2px 2px 4px hsl(30 20% 18%),
|
||||
inset -2px -2px 4px hsl(30 25% 5%);
|
||||
inset 2px 2px 4px hsl(0 10% 12%),
|
||||
inset -2px -2px 4px hsl(0 15% 2%);
|
||||
}
|
||||
|
||||
/* Tech-Priest Data Stream Effect */
|
||||
/* Data Stream Effect - Red */
|
||||
.data-stream {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@@ -352,12 +346,14 @@
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: repeating-linear-gradient(0deg,
|
||||
transparent,
|
||||
transparent 2px,
|
||||
hsl(145 50% 30% / 0.05) 2px,
|
||||
hsl(145 50% 30% / 0.05) 4px);
|
||||
animation: stream-scroll 20s linear infinite;
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent,
|
||||
transparent 2px,
|
||||
hsl(0 60% 40% / 0.06) 2px,
|
||||
hsl(0 60% 40% / 0.06) 4px
|
||||
);
|
||||
animation: stream-scroll 15s linear infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -371,7 +367,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Omnissiah's Blessing - Machine Spirit Glow */
|
||||
/* Machine Spirit Glow - Red */
|
||||
.machine-spirit {
|
||||
position: relative;
|
||||
}
|
||||
@@ -380,39 +376,34 @@
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
background: linear-gradient(45deg,
|
||||
hsl(28 80% 50% / 0.1),
|
||||
hsl(43 70% 50% / 0.1),
|
||||
hsl(28 80% 50% / 0.1));
|
||||
background: linear-gradient(45deg, hsl(0 80% 50% / 0.15), hsl(0 70% 40% / 0.1), hsl(0 80% 50% / 0.15));
|
||||
border-radius: inherit;
|
||||
animation: spirit-pulse 4s ease-in-out infinite;
|
||||
animation: spirit-pulse 3s ease-in-out infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@keyframes spirit-pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
opacity: 0.4;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
opacity: 0.7;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
/* Binary Choir Text Effect */
|
||||
/* Binary Choir Text Effect - Red */
|
||||
.binary-choir {
|
||||
text-shadow:
|
||||
0 0 10px hsl(145 70% 40% / 0.5),
|
||||
0 0 20px hsl(145 60% 30% / 0.3);
|
||||
animation: binary-flicker 5s infinite;
|
||||
0 0 10px hsl(0 75% 50% / 0.6),
|
||||
0 0 20px hsl(0 65% 40% / 0.4);
|
||||
animation: binary-flicker 4s infinite;
|
||||
}
|
||||
|
||||
@keyframes binary-flicker {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
@@ -431,51 +422,51 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Noosphere Connection Effect */
|
||||
/* Noosphere Border - Red/Black */
|
||||
.noosphere-border {
|
||||
border: 1px solid transparent;
|
||||
background:
|
||||
linear-gradient(hsl(30 20% 6%), hsl(30 20% 6%)) padding-box,
|
||||
linear-gradient(135deg,
|
||||
hsl(145 60% 35%) 0%,
|
||||
hsl(28 70% 40%) 25%,
|
||||
hsl(43 60% 35%) 50%,
|
||||
hsl(28 70% 40%) 75%,
|
||||
hsl(145 60% 35%) 100%) border-box;
|
||||
linear-gradient(hsl(0 0% 5%), hsl(0 0% 5%)) padding-box,
|
||||
linear-gradient(
|
||||
135deg,
|
||||
hsl(0 70% 45%) 0%,
|
||||
hsl(0 50% 30%) 25%,
|
||||
hsl(0 60% 35%) 50%,
|
||||
hsl(0 50% 25%) 75%,
|
||||
hsl(0 70% 45%) 100%
|
||||
)
|
||||
border-box;
|
||||
}
|
||||
|
||||
/* Mechanicus Panel */
|
||||
.mechanicus-panel {
|
||||
background: linear-gradient(145deg,
|
||||
hsl(30 18% 8%) 0%,
|
||||
hsl(30 22% 6%) 50%,
|
||||
hsl(30 18% 10%) 100%);
|
||||
border: 1px solid hsl(28 50% 20% / 0.5);
|
||||
/* Heretek Panel */
|
||||
.heretek-panel {
|
||||
background: linear-gradient(145deg, hsl(0 5% 6%) 0%, hsl(0 8% 4%) 50%, hsl(0 5% 7%) 100%);
|
||||
border: 1px solid hsl(0 40% 18% / 0.6);
|
||||
box-shadow:
|
||||
inset 0 1px 0 hsl(30 25% 15%),
|
||||
inset 0 -1px 0 hsl(30 20% 5%),
|
||||
0 4px 20px hsl(30 20% 5% / 0.3);
|
||||
inset 0 1px 0 hsl(0 15% 12%),
|
||||
inset 0 -1px 0 hsl(0 10% 3%),
|
||||
0 4px 20px hsl(0 0% 3% / 0.4);
|
||||
}
|
||||
|
||||
/* Sacred Machine Icon */
|
||||
/* Sacred Machine Icon - Red */
|
||||
.machine-icon {
|
||||
filter: drop-shadow(0 0 8px hsl(28 70% 45% / 0.4));
|
||||
filter: drop-shadow(0 0 8px hsl(0 70% 50% / 0.5));
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.machine-icon:hover {
|
||||
filter: drop-shadow(0 0 12px hsl(28 80% 50% / 0.6));
|
||||
filter: drop-shadow(0 0 12px hsl(0 80% 55% / 0.7));
|
||||
}
|
||||
|
||||
/* Heretek Input Focus */
|
||||
/* Heretek Focus - Red */
|
||||
.heretek-focus:focus {
|
||||
outline: none;
|
||||
box-shadow:
|
||||
0 0 0 2px hsl(28 70% 40%),
|
||||
0 0 15px hsl(28 60% 35% / 0.3);
|
||||
0 0 0 2px hsl(0 70% 45%),
|
||||
0 0 15px hsl(0 60% 40% / 0.4);
|
||||
}
|
||||
|
||||
/* Corrupted Text Glitch */
|
||||
/* Corrupted Text Glitch - Enhanced */
|
||||
.text-glitch {
|
||||
position: relative;
|
||||
}
|
||||
@@ -491,113 +482,106 @@
|
||||
}
|
||||
|
||||
.text-glitch::before {
|
||||
color: hsl(145 70% 40%);
|
||||
animation: glitch-1 2s infinite linear alternate-reverse;
|
||||
color: hsl(0 75% 50%);
|
||||
animation: glitch-1 1.5s infinite linear alternate-reverse;
|
||||
clip-path: polygon(0 0, 100% 0, 100% 35%, 0 35%);
|
||||
}
|
||||
|
||||
.text-glitch::after {
|
||||
color: hsl(0 70% 45%);
|
||||
animation: glitch-2 2s infinite linear alternate-reverse;
|
||||
color: hsl(0 85% 55%);
|
||||
animation: glitch-2 1.5s infinite linear alternate-reverse;
|
||||
clip-path: polygon(0 65%, 100% 65%, 100% 100%, 0 100%);
|
||||
}
|
||||
|
||||
@keyframes glitch-1 {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0);
|
||||
}
|
||||
|
||||
20% {
|
||||
transform: translate(-2px, 2px);
|
||||
transform: translate(-3px, 2px);
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: translate(2px, -2px);
|
||||
transform: translate(3px, -2px);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: translate(-1px, 1px);
|
||||
transform: translate(-2px, 1px);
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: translate(1px, -1px);
|
||||
transform: translate(2px, -1px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glitch-2 {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0);
|
||||
}
|
||||
|
||||
20% {
|
||||
transform: translate(2px, -2px);
|
||||
transform: translate(3px, -2px);
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: translate(-2px, 2px);
|
||||
transform: translate(-3px, 2px);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: translate(1px, -1px);
|
||||
transform: translate(2px, -1px);
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: translate(-1px, 1px);
|
||||
transform: translate(-2px, 1px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Forge Glow - for active elements */
|
||||
/* Forge Glow - Red */
|
||||
.forge-glow {
|
||||
box-shadow:
|
||||
0 0 10px hsl(28 70% 45% / 0.3),
|
||||
0 0 20px hsl(28 60% 35% / 0.2),
|
||||
inset 0 0 10px hsl(28 50% 25% / 0.1);
|
||||
0 0 10px hsl(0 70% 50% / 0.35),
|
||||
0 0 20px hsl(0 60% 40% / 0.25),
|
||||
inset 0 0 10px hsl(0 50% 30% / 0.15);
|
||||
}
|
||||
|
||||
/* Sacred Scroll Effect */
|
||||
.sacred-scroll {
|
||||
background:
|
||||
linear-gradient(180deg,
|
||||
hsl(30 20% 6%) 0%,
|
||||
hsl(30 18% 8%) 10%,
|
||||
hsl(30 20% 8%) 90%,
|
||||
hsl(30 20% 6%) 100%);
|
||||
border-top: 1px solid hsl(43 40% 25% / 0.3);
|
||||
border-bottom: 1px solid hsl(43 40% 25% / 0.3);
|
||||
background: linear-gradient(180deg, hsl(0 0% 4%) 0%, hsl(0 5% 6%) 10%, hsl(0 0% 5%) 90%, hsl(0 0% 4%) 100%);
|
||||
border-top: 1px solid hsl(0 30% 20% / 0.4);
|
||||
border-bottom: 1px solid hsl(0 30% 20% / 0.4);
|
||||
}
|
||||
|
||||
/* Circuit Board Pattern */
|
||||
/* Circuit Board Pattern - Red */
|
||||
.circuit-board {
|
||||
background-image:
|
||||
linear-gradient(90deg, hsl(145 40% 20% / 0.1) 1px, transparent 1px),
|
||||
linear-gradient(180deg, hsl(145 40% 20% / 0.1) 1px, transparent 1px);
|
||||
linear-gradient(90deg, hsl(0 50% 25% / 0.12) 1px, transparent 1px),
|
||||
linear-gradient(180deg, hsl(0 50% 25% / 0.12) 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
/* Rust Particle Animation */
|
||||
.rust-particles {
|
||||
/* Blood Particle Animation */
|
||||
.blood-particles {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rust-particles::after {
|
||||
.blood-particles::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 30%, hsl(28 60% 40% / 0.3) 1px, transparent 1px),
|
||||
radial-gradient(circle at 80% 70%, hsl(35 50% 35% / 0.3) 1px, transparent 1px),
|
||||
radial-gradient(circle at 50% 50%, hsl(28 70% 45% / 0.2) 1px, transparent 1px);
|
||||
radial-gradient(circle at 20% 30%, hsl(0 70% 45% / 0.35) 1px, transparent 1px),
|
||||
radial-gradient(circle at 80% 70%, hsl(0 60% 40% / 0.35) 1px, transparent 1px),
|
||||
radial-gradient(circle at 50% 50%, hsl(0 75% 50% / 0.25) 1px, transparent 1px);
|
||||
background-size: 100px 100px;
|
||||
animation: rust-drift 30s linear infinite;
|
||||
animation: blood-drift 25s linear infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes rust-drift {
|
||||
@keyframes blood-drift {
|
||||
0% {
|
||||
transform: translateY(0) rotate(0deg);
|
||||
}
|
||||
@@ -606,4 +590,232 @@
|
||||
transform: translateY(-100px) rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Heretek Glitch Text - Advanced */
|
||||
.heretek-glitch {
|
||||
position: relative;
|
||||
animation: heretek-glitch 4s infinite;
|
||||
}
|
||||
|
||||
.heretek-glitch::before,
|
||||
.heretek-glitch::after {
|
||||
content: attr(data-text);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.heretek-glitch::before {
|
||||
color: hsl(0 80% 50%);
|
||||
animation: heretek-glitch-1 0.3s infinite;
|
||||
clip-path: polygon(0 0, 100% 0, 100% 45%, 0 45%);
|
||||
transform: translate(-2px, -2px);
|
||||
}
|
||||
|
||||
.heretek-glitch::after {
|
||||
color: hsl(0 90% 60%);
|
||||
animation: heretek-glitch-2 0.3s infinite;
|
||||
clip-path: polygon(0 55%, 100% 55%, 100% 100%, 0 100%);
|
||||
transform: translate(2px, 2px);
|
||||
}
|
||||
|
||||
@keyframes heretek-glitch {
|
||||
0%,
|
||||
85%,
|
||||
100% {
|
||||
transform: translate(0);
|
||||
}
|
||||
86% {
|
||||
transform: translate(-2px, 1px);
|
||||
}
|
||||
87% {
|
||||
transform: translate(2px, -1px);
|
||||
}
|
||||
88% {
|
||||
transform: translate(-1px, -1px);
|
||||
}
|
||||
89% {
|
||||
transform: translate(1px, 1px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes heretek-glitch-1 {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(-2px, -2px);
|
||||
}
|
||||
50% {
|
||||
transform: translate(2px, 2px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes heretek-glitch-2 {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(2px, 2px);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-2px, -2px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Void Background */
|
||||
.void-bg {
|
||||
background: radial-gradient(ellipse at center, hsl(0 0% 5%) 0%, hsl(0 0% 3%) 50%, hsl(0 0% 1%) 100%);
|
||||
}
|
||||
|
||||
/* Blood Drip Effect */
|
||||
.blood-drip {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.blood-drip::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
width: 2px;
|
||||
height: 0;
|
||||
background: linear-gradient(180deg, hsl(0 80% 50%), hsl(0 70% 40%), transparent);
|
||||
animation: blood-drip 3s ease-in-out infinite;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
@keyframes blood-drip {
|
||||
0%,
|
||||
100% {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
height: 10px;
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
height: 15px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
/* Static Noise Effect */
|
||||
.static-noise {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.static-noise::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
|
||||
opacity: 0;
|
||||
animation: static-flash 8s infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes static-flash {
|
||||
0%,
|
||||
95%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
96% {
|
||||
opacity: 0.08;
|
||||
}
|
||||
97% {
|
||||
opacity: 0;
|
||||
}
|
||||
98% {
|
||||
opacity: 0.05;
|
||||
}
|
||||
99% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Chromatic Aberration */
|
||||
.chromatic-aberration {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chromatic-aberration::before,
|
||||
.chromatic-aberration::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.chromatic-aberration::before {
|
||||
background: inherit;
|
||||
transform: translate(-2px, 0);
|
||||
opacity: 0.5;
|
||||
mix-blend-mode: multiply;
|
||||
filter: hue-rotate(90deg);
|
||||
}
|
||||
|
||||
.chromatic-aberration::after {
|
||||
background: inherit;
|
||||
transform: translate(2px, 0);
|
||||
opacity: 0.5;
|
||||
mix-blend-mode: multiply;
|
||||
filter: hue-rotate(-90deg);
|
||||
}
|
||||
|
||||
/* Terminal Text Effect */
|
||||
.terminal-text {
|
||||
font-family: "Courier New", monospace;
|
||||
text-shadow:
|
||||
0 0 5px hsl(0 70% 50% / 0.8),
|
||||
0 0 10px hsl(0 60% 40% / 0.5);
|
||||
animation: terminal-flicker 0.1s infinite;
|
||||
}
|
||||
|
||||
@keyframes terminal-flicker {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.98;
|
||||
}
|
||||
}
|
||||
|
||||
/* Heretek Card */
|
||||
.heretek-card {
|
||||
background: linear-gradient(145deg, hsl(0 5% 7%) 0%, hsl(0 8% 5%) 50%, hsl(0 5% 8%) 100%);
|
||||
border: 1px solid hsl(0 30% 18%);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.heretek-card:hover {
|
||||
border-color: hsl(0 60% 35%);
|
||||
box-shadow:
|
||||
0 0 15px hsl(0 70% 40% / 0.2),
|
||||
0 0 30px hsl(0 60% 30% / 0.1),
|
||||
inset 0 0 10px hsl(0 50% 20% / 0.05);
|
||||
}
|
||||
|
||||
/* Corruption Spread */
|
||||
.corruption-spread {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.corruption-spread::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at var(--x, 50%) var(--y, 50%), hsl(0 70% 40% / 0.15) 0%, transparent 50%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.corruption-spread:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
+182
-133
@@ -2,19 +2,12 @@
|
||||
//
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const {
|
||||
default: flattenColorPalette,
|
||||
} = require("tailwindcss/lib/util/flattenColorPalette");
|
||||
const { default: flattenColorPalette } = require("tailwindcss/lib/util/flattenColorPalette");
|
||||
const svgToDataUri = require("mini-svg-data-uri");
|
||||
|
||||
const config = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
"./pages/**/*.{ts,tsx}",
|
||||
"./components/**/*.{ts,tsx}",
|
||||
"./app/**/*.{ts,tsx}",
|
||||
"./src/**/*.{ts,tsx}",
|
||||
],
|
||||
content: ["./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}"],
|
||||
prefix: "",
|
||||
theme: {
|
||||
container: {
|
||||
@@ -59,71 +52,97 @@ const config = {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
// Heretek Custom Colors
|
||||
// Heretek Custom Colors - Blood & Steel
|
||||
blood: {
|
||||
50: "hsl(0 60% 95%)",
|
||||
100: "hsl(0 65% 90%)",
|
||||
200: "hsl(0 70% 80%)",
|
||||
300: "hsl(0 75% 65%)",
|
||||
400: "hsl(0 80% 55%)",
|
||||
500: "hsl(0 85% 45%)",
|
||||
600: "hsl(0 80% 38%)",
|
||||
700: "hsl(0 75% 30%)",
|
||||
800: "hsl(0 70% 22%)",
|
||||
900: "hsl(0 65% 15%)",
|
||||
950: "hsl(0 60% 8%)",
|
||||
},
|
||||
steel: {
|
||||
50: "hsl(0 5% 95%)",
|
||||
100: "hsl(0 8% 88%)",
|
||||
200: "hsl(0 10% 78%)",
|
||||
300: "hsl(0 12% 65%)",
|
||||
400: "hsl(0 15% 50%)",
|
||||
500: "hsl(0 18% 42%)",
|
||||
600: "hsl(0 20% 35%)",
|
||||
700: "hsl(0 18% 28%)",
|
||||
800: "hsl(0 15% 20%)",
|
||||
900: "hsl(0 12% 12%)",
|
||||
950: "hsl(0 10% 6%)",
|
||||
},
|
||||
void: {
|
||||
50: "hsl(0 0% 95%)",
|
||||
100: "hsl(0 0% 88%)",
|
||||
200: "hsl(0 0% 78%)",
|
||||
300: "hsl(0 0% 65%)",
|
||||
400: "hsl(0 0% 50%)",
|
||||
500: "hsl(0 0% 42%)",
|
||||
600: "hsl(0 0% 35%)",
|
||||
700: "hsl(0 0% 28%)",
|
||||
800: "hsl(0 0% 20%)",
|
||||
900: "hsl(0 0% 12%)",
|
||||
950: "hsl(0 0% 6%)",
|
||||
},
|
||||
rust: {
|
||||
50: "hsl(28 60% 95%)",
|
||||
100: "hsl(28 60% 90%)",
|
||||
200: "hsl(28 60% 80%)",
|
||||
300: "hsl(28 65% 70%)",
|
||||
400: "hsl(28 70% 55%)",
|
||||
500: "hsl(28 75% 45%)",
|
||||
600: "hsl(28 80% 38%)",
|
||||
700: "hsl(28 75% 30%)",
|
||||
800: "hsl(28 70% 22%)",
|
||||
900: "hsl(28 65% 15%)",
|
||||
950: "hsl(28 60% 8%)",
|
||||
50: "hsl(0 60% 95%)",
|
||||
100: "hsl(0 65% 90%)",
|
||||
200: "hsl(0 70% 80%)",
|
||||
300: "hsl(0 75% 70%)",
|
||||
400: "hsl(0 80% 55%)",
|
||||
500: "hsl(0 85% 45%)",
|
||||
600: "hsl(0 80% 38%)",
|
||||
700: "hsl(0 75% 30%)",
|
||||
800: "hsl(0 70% 22%)",
|
||||
900: "hsl(0 65% 15%)",
|
||||
950: "hsl(0 60% 8%)",
|
||||
},
|
||||
brass: {
|
||||
50: "hsl(43 50% 92%)",
|
||||
100: "hsl(43 55% 85%)",
|
||||
200: "hsl(43 60% 75%)",
|
||||
300: "hsl(43 65% 62%)",
|
||||
400: "hsl(43 70% 50%)",
|
||||
500: "hsl(43 65% 45%)",
|
||||
600: "hsl(43 60% 38%)",
|
||||
700: "hsl(43 55% 30%)",
|
||||
800: "hsl(43 50% 22%)",
|
||||
900: "hsl(43 45% 15%)",
|
||||
950: "hsl(43 40% 8%)",
|
||||
},
|
||||
copper: {
|
||||
50: "hsl(25 50% 92%)",
|
||||
100: "hsl(25 55% 85%)",
|
||||
200: "hsl(25 60% 75%)",
|
||||
300: "hsl(25 65% 62%)",
|
||||
400: "hsl(25 70% 50%)",
|
||||
500: "hsl(25 75% 42%)",
|
||||
600: "hsl(25 80% 35%)",
|
||||
700: "hsl(25 75% 28%)",
|
||||
800: "hsl(25 70% 20%)",
|
||||
900: "hsl(25 65% 14%)",
|
||||
950: "hsl(25 60% 7%)",
|
||||
50: "hsl(45 50% 92%)",
|
||||
100: "hsl(45 55% 85%)",
|
||||
200: "hsl(45 60% 75%)",
|
||||
300: "hsl(45 65% 62%)",
|
||||
400: "hsl(45 70% 50%)",
|
||||
500: "hsl(45 65% 45%)",
|
||||
600: "hsl(45 60% 38%)",
|
||||
700: "hsl(45 55% 30%)",
|
||||
800: "hsl(45 50% 22%)",
|
||||
900: "hsl(45 45% 15%)",
|
||||
950: "hsl(45 40% 8%)",
|
||||
},
|
||||
corruption: {
|
||||
50: "hsl(145 40% 95%)",
|
||||
100: "hsl(145 45% 88%)",
|
||||
200: "hsl(145 50% 78%)",
|
||||
300: "hsl(145 55% 65%)",
|
||||
400: "hsl(145 60% 50%)",
|
||||
500: "hsl(145 65% 40%)",
|
||||
600: "hsl(145 70% 32%)",
|
||||
700: "hsl(145 65% 25%)",
|
||||
800: "hsl(145 60% 18%)",
|
||||
900: "hsl(145 55% 12%)",
|
||||
950: "hsl(145 50% 6%)",
|
||||
50: "hsl(0 40% 95%)",
|
||||
100: "hsl(0 45% 88%)",
|
||||
200: "hsl(0 50% 78%)",
|
||||
300: "hsl(0 55% 65%)",
|
||||
400: "hsl(0 60% 50%)",
|
||||
500: "hsl(0 65% 40%)",
|
||||
600: "hsl(0 70% 32%)",
|
||||
700: "hsl(0 65% 25%)",
|
||||
800: "hsl(0 60% 18%)",
|
||||
900: "hsl(0 55% 12%)",
|
||||
950: "hsl(0 50% 6%)",
|
||||
},
|
||||
iron: {
|
||||
50: "hsl(30 10% 95%)",
|
||||
100: "hsl(30 12% 88%)",
|
||||
200: "hsl(30 15% 78%)",
|
||||
300: "hsl(30 18% 65%)",
|
||||
400: "hsl(30 20% 50%)",
|
||||
500: "hsl(30 22% 42%)",
|
||||
600: "hsl(30 25% 35%)",
|
||||
700: "hsl(30 22% 28%)",
|
||||
800: "hsl(30 20% 20%)",
|
||||
900: "hsl(30 18% 12%)",
|
||||
950: "hsl(30 15% 6%)",
|
||||
50: "hsl(0 5% 95%)",
|
||||
100: "hsl(0 8% 88%)",
|
||||
200: "hsl(0 10% 78%)",
|
||||
300: "hsl(0 12% 65%)",
|
||||
400: "hsl(0 15% 50%)",
|
||||
500: "hsl(0 18% 42%)",
|
||||
600: "hsl(0 20% 35%)",
|
||||
700: "hsl(0 18% 28%)",
|
||||
800: "hsl(0 15% 20%)",
|
||||
900: "hsl(0 12% 12%)",
|
||||
950: "hsl(0 10% 6%)",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
@@ -140,11 +159,11 @@ const config = {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
"shine": {
|
||||
shine: {
|
||||
from: { backgroundPosition: "200% 0" },
|
||||
to: { backgroundPosition: "-200% 0" },
|
||||
},
|
||||
"gradient": {
|
||||
gradient: {
|
||||
to: {
|
||||
backgroundPosition: "var(--bg-size) 0",
|
||||
},
|
||||
@@ -156,11 +175,11 @@ const config = {
|
||||
"50%": {
|
||||
"background-position": "100% 100%",
|
||||
},
|
||||
"to": {
|
||||
to: {
|
||||
"background-position": "0% 0%",
|
||||
},
|
||||
},
|
||||
"moveHorizontal": {
|
||||
moveHorizontal: {
|
||||
"0%": {
|
||||
transform: "translateX(-50%) translateY(-10%)",
|
||||
},
|
||||
@@ -171,7 +190,7 @@ const config = {
|
||||
transform: "translateX(-50%) translateY(-10%)",
|
||||
},
|
||||
},
|
||||
"moveInCircle": {
|
||||
moveInCircle: {
|
||||
"0%": {
|
||||
transform: "rotate(0deg)",
|
||||
},
|
||||
@@ -182,7 +201,7 @@ const config = {
|
||||
transform: "rotate(360deg)",
|
||||
},
|
||||
},
|
||||
"moveVertical": {
|
||||
moveVertical: {
|
||||
"0%": {
|
||||
transform: "translateY(-50%)",
|
||||
},
|
||||
@@ -193,26 +212,34 @@ const config = {
|
||||
transform: "translateY(-50%)",
|
||||
},
|
||||
},
|
||||
// Heretek Glitch Animations
|
||||
"glitch": {
|
||||
"0%, 90%, 100%": {
|
||||
// Heretek Glitch Animations - Enhanced
|
||||
glitch: {
|
||||
"0%, 88%, 100%": {
|
||||
transform: "translate(0)",
|
||||
filter: "none",
|
||||
},
|
||||
"91%": {
|
||||
"89%": {
|
||||
transform: "translate(-2px, 1px)",
|
||||
filter: "hue-rotate(90deg) saturate(1.5)",
|
||||
},
|
||||
"92%": {
|
||||
"90%": {
|
||||
transform: "translate(2px, -1px)",
|
||||
filter: "hue-rotate(-90deg) saturate(1.5)",
|
||||
},
|
||||
"93%": {
|
||||
"91%": {
|
||||
transform: "translate(-1px, -1px)",
|
||||
filter: "hue-rotate(45deg)",
|
||||
filter: "hue-rotate(45deg) brightness(1.2)",
|
||||
},
|
||||
"92%": {
|
||||
transform: "translate(1px, 1px)",
|
||||
filter: "none",
|
||||
},
|
||||
"93%": {
|
||||
transform: "translate(-2px, 2px)",
|
||||
filter: "hue-rotate(-45deg) saturate(1.3)",
|
||||
},
|
||||
"94%": {
|
||||
transform: "translate(1px, 1px)",
|
||||
transform: "translate(2px, -2px)",
|
||||
filter: "none",
|
||||
},
|
||||
},
|
||||
@@ -221,29 +248,32 @@ const config = {
|
||||
"text-shadow": "none",
|
||||
},
|
||||
"1%": {
|
||||
"text-shadow": "-2px 0 hsl(145 70% 40%), 2px 0 hsl(0 70% 45%)",
|
||||
"text-shadow": "-2px 0 hsl(0 80% 50%), 2px 0 hsl(0 90% 60%)",
|
||||
},
|
||||
"2%": {
|
||||
"text-shadow": "2px 0 hsl(145 70% 40%), -2px 0 hsl(0 70% 45%)",
|
||||
"text-shadow": "2px 0 hsl(0 80% 50%), -2px 0 hsl(0 90% 60%)",
|
||||
},
|
||||
"3%": {
|
||||
"text-shadow": "none",
|
||||
},
|
||||
},
|
||||
"flicker": {
|
||||
flicker: {
|
||||
"0%, 100%": { opacity: "1" },
|
||||
"90%": { opacity: "1" },
|
||||
"91%": { opacity: "0.7" },
|
||||
"92%": { opacity: "1" },
|
||||
"93%": { opacity: "0.8" },
|
||||
"93%": { opacity: "0.85" },
|
||||
"94%": { opacity: "1" },
|
||||
"96%": { opacity: "0.9" },
|
||||
"97%": { opacity: "1" },
|
||||
"95%": { opacity: "0.9" },
|
||||
"96%": { opacity: "1" },
|
||||
},
|
||||
"corrupted-pulse": {
|
||||
"0%, 100%": {
|
||||
"box-shadow": "0 0 10px hsl(145 50% 25% / 0.2), 0 0 20px hsl(145 40% 20% / 0.1)",
|
||||
"box-shadow": "0 0 10px hsl(0 65% 40% / 0.25), 0 0 20px hsl(0 55% 30% / 0.15)",
|
||||
},
|
||||
"50%": {
|
||||
"box-shadow": "0 0 15px hsl(145 60% 30% / 0.3), 0 0 30px hsl(145 50% 25% / 0.15), 0 0 45px hsl(145 40% 20% / 0.1)",
|
||||
"box-shadow":
|
||||
"0 0 15px hsl(0 75% 45% / 0.35), 0 0 30px hsl(0 65% 35% / 0.2), 0 0 45px hsl(0 55% 25% / 0.1)",
|
||||
},
|
||||
},
|
||||
"scan-line": {
|
||||
@@ -254,20 +284,18 @@ const config = {
|
||||
transform: "translateY(100vh)",
|
||||
},
|
||||
},
|
||||
"rust-fall": {
|
||||
"0%": {
|
||||
transform: "translateY(-10%) rotate(0deg)",
|
||||
"blood-drip": {
|
||||
"0%, 100%": {
|
||||
height: "0",
|
||||
opacity: "0",
|
||||
},
|
||||
"10%": {
|
||||
"50%": {
|
||||
height: "10px",
|
||||
opacity: "1",
|
||||
},
|
||||
"90%": {
|
||||
opacity: "1",
|
||||
},
|
||||
"100%": {
|
||||
transform: "translateY(100vh) rotate(720deg)",
|
||||
opacity: "0",
|
||||
"80%": {
|
||||
height: "15px",
|
||||
opacity: "0.5",
|
||||
},
|
||||
},
|
||||
"metal-shine": {
|
||||
@@ -280,13 +308,13 @@ const config = {
|
||||
},
|
||||
"heretic-glow": {
|
||||
"0%, 100%": {
|
||||
"filter": "brightness(1) saturate(1)",
|
||||
filter: "brightness(1) saturate(1)",
|
||||
},
|
||||
"50%": {
|
||||
"filter": "brightness(1.1) saturate(1.2)",
|
||||
filter: "brightness(1.15) saturate(1.3)",
|
||||
},
|
||||
},
|
||||
// New Mechanicus Animations
|
||||
// Heretek Enhanced Animations
|
||||
"binary-flicker": {
|
||||
"0%, 100%": { opacity: "1" },
|
||||
"50%": { opacity: "0.95" },
|
||||
@@ -298,8 +326,8 @@ const config = {
|
||||
"100%": { transform: "translateX(100%)" },
|
||||
},
|
||||
"spirit-pulse": {
|
||||
"0%, 100%": { opacity: "0.3", transform: "scale(1)" },
|
||||
"50%": { opacity: "0.6", transform: "scale(1.02)" },
|
||||
"0%, 100%": { opacity: "0.4", transform: "scale(1)" },
|
||||
"50%": { opacity: "0.7", transform: "scale(1.02)" },
|
||||
},
|
||||
"stream-scroll": {
|
||||
"0%": { transform: "translateY(0)" },
|
||||
@@ -307,50 +335,73 @@ const config = {
|
||||
},
|
||||
"forge-pulse": {
|
||||
"0%, 100%": {
|
||||
"box-shadow": "0 0 10px hsl(28 70% 45% / 0.3), 0 0 20px hsl(28 60% 35% / 0.2)",
|
||||
"box-shadow": "0 0 10px hsl(0 70% 50% / 0.35), 0 0 20px hsl(0 60% 40% / 0.25)",
|
||||
},
|
||||
"50%": {
|
||||
"box-shadow": "0 0 15px hsl(28 80% 50% / 0.4), 0 0 30px hsl(28 70% 40% / 0.3), 0 0 45px hsl(28 60% 35% / 0.2)",
|
||||
"box-shadow":
|
||||
"0 0 15px hsl(0 80% 55% / 0.45), 0 0 30px hsl(0 70% 45% / 0.35), 0 0 45px hsl(0 60% 35% / 0.2)",
|
||||
},
|
||||
},
|
||||
"rust-drift": {
|
||||
"blood-drift": {
|
||||
"0%": { transform: "translateY(0) rotate(0deg)" },
|
||||
"100%": { transform: "translateY(-100px) rotate(360deg)" },
|
||||
},
|
||||
"mechanicus-shimmer": {
|
||||
"heretek-shimmer": {
|
||||
"0%": { backgroundPosition: "-200% 0" },
|
||||
"100%": { backgroundPosition: "200% 0" },
|
||||
},
|
||||
"static-flash": {
|
||||
"0%, 95%, 100%": { opacity: "0" },
|
||||
"96%": { opacity: "0.08" },
|
||||
"97%": { opacity: "0" },
|
||||
"98%": { opacity: "0.05" },
|
||||
"99%": { opacity: "0" },
|
||||
},
|
||||
"terminal-flicker": {
|
||||
"0%, 100%": { opacity: "1" },
|
||||
"50%": { opacity: "0.98" },
|
||||
},
|
||||
"void-pulse": {
|
||||
"0%, 100%": {
|
||||
"box-shadow": "0 0 20px hsl(0 0% 0% / 0.5), inset 0 0 20px hsl(0 0% 0% / 0.3)",
|
||||
},
|
||||
"50%": {
|
||||
"box-shadow": "0 0 40px hsl(0 0% 5% / 0.6), inset 0 0 30px hsl(0 0% 3% / 0.4)",
|
||||
},
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
"shine": "shine 8s ease-in-out infinite",
|
||||
"gradient": "gradient 8s linear infinite",
|
||||
shine: "shine 8s ease-in-out infinite",
|
||||
gradient: "gradient 8s linear infinite",
|
||||
// Heretek Animations
|
||||
"glitch": "glitch 4s infinite",
|
||||
"glitch-text": "glitch-text 8s infinite",
|
||||
"flicker": "flicker 8s infinite",
|
||||
"corrupted-pulse": "corrupted-pulse 3s ease-in-out infinite",
|
||||
"scan-line": "scan-line 8s linear infinite",
|
||||
"rust-fall": "rust-fall 10s linear infinite",
|
||||
glitch: "glitch 3s infinite",
|
||||
"glitch-text": "glitch-text 6s infinite",
|
||||
flicker: "flicker 6s infinite",
|
||||
"corrupted-pulse": "corrupted-pulse 2.5s ease-in-out infinite",
|
||||
"scan-line": "scan-line 6s linear infinite",
|
||||
"blood-drip": "blood-drip 3s ease-in-out infinite",
|
||||
"metal-shine": "metal-shine 3s ease-in-out infinite",
|
||||
"heretic-glow": "heretic-glow 4s ease-in-out infinite",
|
||||
// New Mechanicus Animations
|
||||
"binary-flicker": "binary-flicker 5s infinite",
|
||||
"circuit-flow": "circuit-flow 3s linear infinite",
|
||||
"spirit-pulse": "spirit-pulse 4s ease-in-out infinite",
|
||||
"stream-scroll": "stream-scroll 20s linear infinite",
|
||||
"forge-pulse": "forge-pulse 3s ease-in-out infinite",
|
||||
"rust-drift": "rust-drift 30s linear infinite",
|
||||
"mechanicus-shimmer": "mechanicus-shimmer 3s ease-in-out infinite",
|
||||
// Heretek Enhanced Animations
|
||||
"binary-flicker": "binary-flicker 4s infinite",
|
||||
"circuit-flow": "circuit-flow 2.5s linear infinite",
|
||||
"spirit-pulse": "spirit-pulse 3s ease-in-out infinite",
|
||||
"stream-scroll": "stream-scroll 15s linear infinite",
|
||||
"forge-pulse": "forge-pulse 2.5s ease-in-out infinite",
|
||||
"blood-drift": "blood-drift 25s linear infinite",
|
||||
"heretek-shimmer": "heretek-shimmer 3s ease-in-out infinite",
|
||||
"static-flash": "static-flash 8s infinite",
|
||||
"terminal-flicker": "terminal-flicker 0.1s infinite",
|
||||
"void-pulse": "void-pulse 4s ease-in-out infinite",
|
||||
},
|
||||
backgroundImage: {
|
||||
// Heretek Background Patterns
|
||||
"rust-gradient": "linear-gradient(135deg, hsl(28 70% 35%) 0%, hsl(35 50% 25%) 50%, hsl(28 60% 30%) 100%)",
|
||||
"corruption-gradient": "linear-gradient(180deg, hsl(145 50% 20%) 0%, hsl(145 60% 30%) 50%, hsl(145 50% 25%) 100%)",
|
||||
"metal-surface": "linear-gradient(145deg, hsl(30 15% 15%) 0%, hsl(30 20% 10%) 50%, hsl(30 15% 12%) 100%)",
|
||||
"brass-shine": "linear-gradient(90deg, hsl(43 60% 35%) 0%, hsl(43 75% 50%) 50%, hsl(43 60% 35%) 100%)",
|
||||
"blood-gradient": "linear-gradient(135deg, hsl(0 70% 35%) 0%, hsl(0 50% 25%) 50%, hsl(0 60% 30%) 100%)",
|
||||
"void-gradient": "linear-gradient(180deg, hsl(0 0% 5%) 0%, hsl(0 0% 3%) 50%, hsl(0 0% 8%) 100%)",
|
||||
"steel-surface": "linear-gradient(145deg, hsl(0 5% 12%) 0%, hsl(0 8% 8%) 50%, hsl(0 5% 10%) 100%)",
|
||||
"corruption-spread": "linear-gradient(90deg, hsl(0 0% 5%) 0%, hsl(0 60% 30%) 50%, hsl(0 0% 5%) 100%)",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -382,9 +433,9 @@ const config = {
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" fill="none" stroke="${value}" stroke-width="0.5"><path d="M0 32h20m4 0h16m4 0h20M32 0v20m0 4v16m0 4v20"/><circle cx="32" cy="32" r="4" fill="${value}" fill-opacity="0.3"/><circle cx="24" cy="32" r="2" fill="${value}"/><circle cx="40" cy="32" r="2" fill="${value}"/><circle cx="32" cy="24" r="2" fill="${value}"/><circle cx="32" cy="40" r="2" fill="${value}"/></svg>`,
|
||||
)}")`,
|
||||
}),
|
||||
"bg-rust-texture": (value: any) => ({
|
||||
"bg-void-texture": (value: any) => ({
|
||||
backgroundImage: `url("${svgToDataUri(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100"><filter id="rust"><feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="4" result="noise"/><feColorMatrix type="saturate" values="0.3" in="noise" result="rust"/></filter><rect width="100" height="100" fill="${value}" style="filter:url(#rust)"/></svg>`,
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100"><filter id="void"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="4" result="noise"/><feColorMatrix type="saturate" values="0" in="noise" result="void"/></filter><rect width="100" height="100" fill="${value}" style="filter:url(#void)"/></svg>`,
|
||||
)}")`,
|
||||
}),
|
||||
},
|
||||
@@ -399,9 +450,7 @@ const config = {
|
||||
|
||||
function addVariablesForColors({ addBase, theme }: any) {
|
||||
const allColors = flattenColorPalette(theme("colors"));
|
||||
const newVars = Object.fromEntries(
|
||||
Object.entries(allColors).map(([key, val]) => [`--${key}`, val]),
|
||||
);
|
||||
const newVars = Object.fromEntries(Object.entries(allColors).map(([key, val]) => [`--${key}`, val]));
|
||||
addBase({
|
||||
":root": newVars,
|
||||
});
|
||||
|
||||
+4
@@ -422,6 +422,10 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
||||
|
||||
## 2026-03-18
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- 🔄 Upstream Sync - 2026-03-18 [@BillyOutlast](https://github.com/BillyOutlast) ([#62](https://github.com/Heretek-AI/ProxmoxVE/pull/62))
|
||||
|
||||
## 2026-03-16
|
||||
|
||||
## 2026-03-15
|
||||
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"name": "Unsloth Studio (In Beta)",
|
||||
"slug": "unsolth-studio",
|
||||
"categories": [20],
|
||||
"date_created": "2026-03-18",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 8888,
|
||||
"documentation": "https://unsloth.ai/docs/new/studio/start",
|
||||
"website": "https://unsloth.ai/",
|
||||
"logo": "https://unsloth.ai/favicon.ico",
|
||||
"config_path": "/opt/unsolth-studio/.env",
|
||||
"description": "Local, browser-based GUI for fine-tuning LLMs without writing code. Supports QLoRA, LoRA, and full fine-tuning with live training monitoring, GPU stats, and model export to GGUF/Safetensors.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/unsolth-studio.sh",
|
||||
"resources": {
|
||||
"cpu": 4,
|
||||
"ram": 16384,
|
||||
"hdd": 50,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Requires GPU passthrough for training. NVIDIA, AMD (ROCm), and Intel GPUs are supported.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "First launch takes 5-10 minutes to compile llama.cpp binaries.",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "Minimum 16GB RAM recommended for training models up to 7B parameters.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "For AMD GPUs, ensure ROCm is properly configured on the host.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: cobalt (cobaltgit)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
|
||||
# Source: https://ntfy.sh/
|
||||
|
||||
APP="Alpine-ntfy"
|
||||
var_tags="${var_tags:-notification}"
|
||||
var_cpu="${var_cpu:-1}"
|
||||
var_ram="${var_ram:-256}"
|
||||
var_disk="${var_disk:-2}"
|
||||
var_os="${var_os:-alpine}"
|
||||
var_version="${var_version:-3.23}"
|
||||
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 /etc/ntfy ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
msg_info "Updating ntfy LXC"
|
||||
$STD apk -U upgrade
|
||||
setcap 'cap_net_bind_service=+ep' /usr/bin/ntfy
|
||||
msg_ok "Updated ntfy LXC"
|
||||
|
||||
msg_info "Restarting ntfy"
|
||||
rc-service ntfy restart
|
||||
msg_ok "Restarted ntfy"
|
||||
msg_ok "Updated successfully!"
|
||||
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}${CL}"
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://anytype.io
|
||||
|
||||
APP="Anytype-Server"
|
||||
var_tags="${var_tags:-notes;productivity;sync}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-4096}"
|
||||
var_disk="${var_disk:-16}"
|
||||
var_os="${var_os:-ubuntu}"
|
||||
var_version="${var_version:-24.04}"
|
||||
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/anytype/any-sync-bundle ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "anytype" "grishy/any-sync-bundle"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop anytype
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
msg_info "Backing up Data"
|
||||
cp -r /opt/anytype/data /opt/anytype_data_backup
|
||||
msg_ok "Backed up Data"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "anytype" "grishy/any-sync-bundle" "prebuild" "latest" "/opt/anytype" "any-sync-bundle_*_linux_amd64.tar.gz"
|
||||
chmod +x /opt/anytype/any-sync-bundle
|
||||
|
||||
msg_info "Restoring Data"
|
||||
cp -r /opt/anytype_data_backup/. /opt/anytype/data
|
||||
rm -rf /opt/anytype_data_backup
|
||||
msg_ok "Restored Data"
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start anytype
|
||||
msg_ok "Started Service"
|
||||
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}:33010${CL}"
|
||||
echo -e "${INFO}${YW} Client config file:${CL}"
|
||||
echo -e "${TAB}${GATEWAY}${BGN}/opt/anytype/data/client-config.yml${CL}"
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/qdm12/gluetun
|
||||
|
||||
APP="Gluetun"
|
||||
var_tags="${var_tags:-vpn;wireguard;openvpn}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-2048}"
|
||||
var_disk="${var_disk:-8}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
var_tun="${var_tun:-yes}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
color
|
||||
catch_errors
|
||||
|
||||
function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
|
||||
if [[ ! -f /usr/local/bin/gluetun ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "gluetun" "qdm12/gluetun"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop gluetun
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "gluetun" "qdm12/gluetun" "tarball"
|
||||
|
||||
msg_info "Building Gluetun"
|
||||
cd /opt/gluetun
|
||||
$STD go mod download
|
||||
CGO_ENABLED=0 $STD go build -trimpath -ldflags="-s -w" -o /usr/local/bin/gluetun ./cmd/gluetun/
|
||||
msg_ok "Built Gluetun"
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start gluetun
|
||||
msg_ok "Started Service"
|
||||
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}:8000${CL}"
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
___ __ _ __ ____
|
||||
/ | / /___ (_)___ ___ ____ / /_/ __/_ __
|
||||
/ /| | / / __ \/ / __ \/ _ \______/ __ \/ __/ /_/ / / /
|
||||
/ ___ |/ / /_/ / / / / / __/_____/ / / / /_/ __/ /_/ /
|
||||
/_/ |_/_/ .___/_/_/ /_/\___/ /_/ /_/\__/_/ \__, /
|
||||
/_/ /____/
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
___ __ _____
|
||||
/ | ____ __ __/ /___ ______ ___ / ___/___ ______ _____ _____
|
||||
/ /| | / __ \/ / / / __/ / / / __ \/ _ \______\__ \/ _ \/ ___/ | / / _ \/ ___/
|
||||
/ ___ |/ / / / /_/ / /_/ /_/ / /_/ / __/_____/__/ / __/ / | |/ / __/ /
|
||||
/_/ |_/_/ /_/\__, /\__/\__, / .___/\___/ /____/\___/_/ |___/\___/_/
|
||||
/____/ /____/_/
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
________ __
|
||||
/ ____/ /_ _____ / /___ ______
|
||||
/ / __/ / / / / _ \/ __/ / / / __ \
|
||||
/ /_/ / / /_/ / __/ /_/ /_/ / / / /
|
||||
\____/_/\__,_/\___/\__/\__,_/_/ /_/
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
_____ ___ __ ____
|
||||
/ ___/____ / (_) /_ / __ \_________
|
||||
\__ \/ __ \/ / / __/_____/ /_/ / ___/ __ \
|
||||
___/ / /_/ / / / /_/_____/ ____/ / / /_/ /
|
||||
/____/ .___/_/_/\__/ /_/ /_/ \____/
|
||||
/_/
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
__ __ __ __
|
||||
\ \/ /___ _____ ___ / /__________ ______/ /__
|
||||
\ / __ `/ __ `__ \/ __/ ___/ __ `/ ___/ //_/
|
||||
/ / /_/ / / / / / / /_/ / / /_/ / /__/ ,<
|
||||
/_/\__,_/_/ /_/ /_/\__/_/ \__,_/\___/_/|_|
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: johanngrobe
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/oss-apps/split-pro
|
||||
|
||||
APP="Split-Pro"
|
||||
var_tags="${var_tags:-finance;expense-sharing}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-4096}"
|
||||
var_disk="${var_disk:-6}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
|
||||
variables
|
||||
color
|
||||
catch_errors
|
||||
|
||||
function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
|
||||
if [[ ! -d /opt/split-pro ]]; then
|
||||
msg_error "No Split Pro Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "split-pro" "oss-apps/split-pro"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop split-pro
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
msg_info "Backing up Data"
|
||||
cp /opt/split-pro/.env /opt/split-pro.env
|
||||
msg_ok "Backed up Data"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "split-pro" "oss-apps/split-pro" "tarball"
|
||||
|
||||
msg_info "Building Application"
|
||||
cd /opt/split-pro
|
||||
$STD pnpm install --frozen-lockfile
|
||||
$STD pnpm build
|
||||
cp /opt/split-pro.env /opt/split-pro/.env
|
||||
rm -f /opt/split-pro.env
|
||||
ln -sf /opt/split-pro_data/uploads /opt/split-pro/uploads
|
||||
$STD pnpm exec prisma migrate deploy
|
||||
msg_ok "Built Application"
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start split-pro
|
||||
msg_ok "Started Service"
|
||||
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}:3000${CL}"
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env bash
|
||||
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/FuzzyGrim/Yamtrack
|
||||
|
||||
APP="Yamtrack"
|
||||
var_tags="${var_tags:-media;tracker;movies;anime}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-2048}"
|
||||
var_disk="${var_disk:-8}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
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/yamtrack ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "yamtrack" "FuzzyGrim/Yamtrack"; then
|
||||
msg_info "Stopping Services"
|
||||
systemctl stop yamtrack yamtrack-celery
|
||||
msg_ok "Stopped Services"
|
||||
|
||||
msg_info "Backing up Data"
|
||||
cp /opt/yamtrack/src/.env /opt/yamtrack_env.bak
|
||||
msg_ok "Backed up Data"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "yamtrack" "FuzzyGrim/Yamtrack" "tarball"
|
||||
|
||||
msg_info "Installing Python Dependencies"
|
||||
cd /opt/yamtrack
|
||||
$STD uv venv .venv
|
||||
$STD uv pip install --no-cache-dir -r requirements.txt
|
||||
msg_ok "Installed Python Dependencies"
|
||||
|
||||
msg_info "Restoring Data"
|
||||
cp /opt/yamtrack_env.bak /opt/yamtrack/src/.env
|
||||
rm -f /opt/yamtrack_env.bak
|
||||
msg_ok "Restored Data"
|
||||
|
||||
msg_info "Updating Yamtrack"
|
||||
cd /opt/yamtrack/src
|
||||
$STD /opt/yamtrack/.venv/bin/python manage.py migrate
|
||||
$STD /opt/yamtrack/.venv/bin/python manage.py collectstatic --noinput
|
||||
msg_ok "Updated Yamtrack"
|
||||
|
||||
msg_info "Updating Nginx Configuration"
|
||||
cp /opt/yamtrack/nginx.conf /etc/nginx/nginx.conf
|
||||
sed -i 's|user abc;|user www-data;|' /etc/nginx/nginx.conf
|
||||
sed -i 's|/yamtrack/staticfiles/|/opt/yamtrack/src/staticfiles/|' /etc/nginx/nginx.conf
|
||||
$STD systemctl reload nginx
|
||||
msg_ok "Updated Nginx Configuration"
|
||||
|
||||
msg_info "Starting Services"
|
||||
systemctl start yamtrack yamtrack-celery
|
||||
msg_ok "Started Services"
|
||||
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}:8000${CL}"
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"name": "Unsloth Studio (In Beta)",
|
||||
"slug": "unsolth-studio",
|
||||
"categories": [20],
|
||||
"date_created": "2026-03-18",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 8888,
|
||||
"documentation": "https://unsloth.ai/docs/new/studio/start",
|
||||
"website": "https://unsloth.ai/",
|
||||
"logo": "https://unsloth.ai/favicon.ico",
|
||||
"config_path": "/opt/unsolth-studio/.env",
|
||||
"description": "Local, browser-based GUI for fine-tuning LLMs without writing code. Supports QLoRA, LoRA, and full fine-tuning with live training monitoring, GPU stats, and model export to GGUF/Safetensors.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/unsolth-studio.sh",
|
||||
"resources": {
|
||||
"cpu": 4,
|
||||
"ram": 16384,
|
||||
"hdd": 50,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Requires GPU passthrough for training. NVIDIA, AMD (ROCm), and Intel GPUs are supported.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "First launch takes 5-10 minutes to compile llama.cpp binaries.",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "Minimum 16GB RAM recommended for training models up to 7B parameters.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "For AMD GPUs, ensure ROCm is properly configured on the host.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: cobalt (cobaltgit)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
|
||||
# Source: https://ntfy.sh/
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
msg_info "Installing ntfy"
|
||||
$STD apk add --no-cache ntfy ntfy-openrc libcap
|
||||
sed -i '/^listen-http/s/^\(.*\)$/#\1\n/' /etc/ntfy/server.yml
|
||||
setcap 'cap_net_bind_service=+ep' /usr/bin/ntfy
|
||||
$STD rc-update add ntfy default
|
||||
$STD service ntfy start
|
||||
msg_ok "Installed ntfy"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://anytype.io
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
setup_mongodb
|
||||
|
||||
msg_info "Configuring MongoDB Replica Set"
|
||||
cat <<EOF >>/etc/mongod.conf
|
||||
|
||||
replication:
|
||||
replSetName: "rs0"
|
||||
EOF
|
||||
systemctl restart mongod
|
||||
sleep 3
|
||||
$STD mongosh --eval 'rs.initiate({_id: "rs0", members: [{_id: 0, host: "127.0.0.1:27017"}]})'
|
||||
msg_ok "Configured MongoDB Replica Set"
|
||||
|
||||
msg_info "Installing Redis Stack"
|
||||
setup_deb822_repo \
|
||||
"redis-stack" \
|
||||
"https://packages.redis.io/gpg" \
|
||||
"https://packages.redis.io/deb" \
|
||||
"jammy" \
|
||||
"main"
|
||||
$STD apt install -y redis-stack-server
|
||||
systemctl enable -q --now redis-stack-server
|
||||
msg_ok "Installed Redis Stack"
|
||||
|
||||
fetch_and_deploy_gh_release "anytype" "grishy/any-sync-bundle" "prebuild" "latest" "/opt/anytype" "any-sync-bundle_*_linux_amd64.tar.gz"
|
||||
chmod +x /opt/anytype/any-sync-bundle
|
||||
|
||||
msg_info "Configuring Anytype"
|
||||
mkdir -p /opt/anytype/data/storage
|
||||
cat <<EOF >/opt/anytype/.env
|
||||
ANY_SYNC_BUNDLE_CONFIG=/opt/anytype/data/bundle-config.yml
|
||||
ANY_SYNC_BUNDLE_CLIENT_CONFIG=/opt/anytype/data/client-config.yml
|
||||
ANY_SYNC_BUNDLE_INIT_STORAGE=/opt/anytype/data/storage/
|
||||
ANY_SYNC_BUNDLE_INIT_EXTERNAL_ADDRS=${LOCAL_IP}
|
||||
ANY_SYNC_BUNDLE_INIT_MONGO_URI=mongodb://127.0.0.1:27017/
|
||||
ANY_SYNC_BUNDLE_INIT_REDIS_URI=redis://127.0.0.1:6379/
|
||||
ANY_SYNC_BUNDLE_LOG_LEVEL=info
|
||||
EOF
|
||||
msg_ok "Configured Anytype"
|
||||
|
||||
msg_info "Creating Service"
|
||||
cat <<EOF >/etc/systemd/system/anytype.service
|
||||
[Unit]
|
||||
Description=Anytype Sync Server (any-sync-bundle)
|
||||
After=network-online.target mongod.service redis-stack-server.service
|
||||
Wants=network-online.target
|
||||
Requires=mongod.service redis-stack-server.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/anytype
|
||||
EnvironmentFile=/opt/anytype/.env
|
||||
ExecStart=/opt/anytype/any-sync-bundle start-bundle
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q --now anytype
|
||||
msg_ok "Created Service"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/qdm12/gluetun
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
msg_info "Installing Dependencies"
|
||||
$STD apt install -y \
|
||||
openvpn \
|
||||
wireguard-tools \
|
||||
iptables
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
msg_info "Configuring iptables"
|
||||
$STD update-alternatives --set iptables /usr/sbin/iptables-legacy
|
||||
$STD update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy
|
||||
ln -sf /usr/sbin/openvpn /usr/sbin/openvpn2.6
|
||||
msg_ok "Configured iptables"
|
||||
|
||||
setup_go
|
||||
|
||||
fetch_and_deploy_gh_release "gluetun" "qdm12/gluetun" "tarball"
|
||||
|
||||
msg_info "Building Gluetun"
|
||||
cd /opt/gluetun
|
||||
$STD go mod download
|
||||
CGO_ENABLED=0 $STD go build -trimpath -ldflags="-s -w" -o /usr/local/bin/gluetun ./cmd/gluetun/
|
||||
msg_ok "Built Gluetun"
|
||||
|
||||
msg_info "Configuring Gluetun"
|
||||
mkdir -p /opt/gluetun-data
|
||||
touch /etc/alpine-release
|
||||
ln -sf /opt/gluetun-data /gluetun
|
||||
cat <<EOF >/opt/gluetun-data/.env
|
||||
VPN_SERVICE_PROVIDER=custom
|
||||
VPN_TYPE=openvpn
|
||||
OPENVPN_CUSTOM_CONFIG=/opt/gluetun-data/custom.ovpn
|
||||
OPENVPN_USER=
|
||||
OPENVPN_PASSWORD=
|
||||
OPENVPN_PROCESS_USER=root
|
||||
PUID=0
|
||||
PGID=0
|
||||
HTTP_CONTROL_SERVER_ADDRESS=:8000
|
||||
HTTPPROXY=off
|
||||
SHADOWSOCKS=off
|
||||
PPROF_ENABLED=no
|
||||
PPROF_BLOCK_PROFILE_RATE=0
|
||||
PPROF_MUTEX_PROFILE_RATE=0
|
||||
PPROF_HTTP_SERVER_ADDRESS=:6060
|
||||
FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT=on
|
||||
HEALTH_SERVER_ADDRESS=127.0.0.1:9999
|
||||
DNS_UPSTREAM_RESOLVERS=cloudflare
|
||||
LOG_LEVEL=info
|
||||
STORAGE_FILEPATH=/gluetun/servers.json
|
||||
PUBLICIP_FILE=/gluetun/ip
|
||||
VPN_PORT_FORWARDING_STATUS_FILE=/gluetun/forwarded_port
|
||||
TZ=UTC
|
||||
EOF
|
||||
msg_ok "Configured Gluetun"
|
||||
|
||||
msg_info "Creating Service"
|
||||
cat <<EOF >/etc/systemd/system/gluetun.service
|
||||
[Unit]
|
||||
Description=Gluetun VPN Client
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/gluetun-data
|
||||
EnvironmentFile=/opt/gluetun-data/.env
|
||||
UnsetEnvironment=USER
|
||||
ExecStartPre=/bin/sh -c 'rm -f /etc/openvpn/target.ovpn'
|
||||
ExecStart=/usr/local/bin/gluetun
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
AmbientCapabilities=CAP_NET_ADMIN
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q --now gluetun
|
||||
msg_ok "Created Service"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: johanngrobe
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/oss-apps/split-pro
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
NODE_VERSION="22" NODE_MODULE="pnpm" setup_nodejs
|
||||
PG_VERSION="17" PG_MODULES="cron" setup_postgresql
|
||||
|
||||
msg_info "Installing Dependencies"
|
||||
$STD apt install -y openssl
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
PG_DB_NAME="splitpro" PG_DB_USER="splitpro" PG_DB_EXTENSIONS="pg_cron" setup_postgresql_db
|
||||
fetch_and_deploy_gh_release "split-pro" "oss-apps/split-pro" "tarball"
|
||||
|
||||
msg_info "Installing Dependencies"
|
||||
cd /opt/split-pro
|
||||
$STD pnpm install --frozen-lockfile
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
msg_info "Building Split Pro"
|
||||
cd /opt/split-pro
|
||||
mkdir -p /opt/split-pro_data/uploads
|
||||
ln -sf /opt/split-pro_data/uploads /opt/split-pro/uploads
|
||||
NEXTAUTH_SECRET=$(openssl rand -base64 32)
|
||||
cp .env.example .env
|
||||
sed -i "s|^DATABASE_URL=.*|DATABASE_URL=\"postgresql://${PG_DB_USER}:${PG_DB_PASS}@localhost:5432/${PG_DB_NAME}\"|" .env
|
||||
sed -i "s|^NEXTAUTH_SECRET=.*|NEXTAUTH_SECRET=\"${NEXTAUTH_SECRET}\"|" .env
|
||||
sed -i "s|^NEXTAUTH_URL=.*|NEXTAUTH_URL=\"http://${LOCAL_IP}:3000\"|" .env
|
||||
sed -i "s|^NEXTAUTH_URL_INTERNAL=.*|NEXTAUTH_URL_INTERNAL=\"http://localhost:3000\"|" .env
|
||||
sed -i "/^POSTGRES_CONTAINER_NAME=/d" .env
|
||||
sed -i "/^POSTGRES_USER=/d" .env
|
||||
sed -i "/^POSTGRES_PASSWORD=/d" .env
|
||||
sed -i "/^POSTGRES_DB=/d" .env
|
||||
sed -i "/^POSTGRES_PORT=/d" .env
|
||||
$STD pnpm build
|
||||
$STD pnpm exec prisma migrate deploy
|
||||
msg_ok "Built Split Pro"
|
||||
|
||||
msg_info "Creating Service"
|
||||
cat <<EOF >/etc/systemd/system/split-pro.service
|
||||
[Unit]
|
||||
Description=Split Pro
|
||||
After=network.target postgresql.service
|
||||
Requires=postgresql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/split-pro
|
||||
EnvironmentFile=/opt/split-pro/.env
|
||||
ExecStart=/usr/bin/pnpm start
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q --now split-pro
|
||||
msg_ok "Created Service"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/FuzzyGrim/Yamtrack
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
msg_info "Installing Dependencies"
|
||||
$STD apt install -y \
|
||||
nginx \
|
||||
redis-server
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
PG_VERSION="16" setup_postgresql
|
||||
PG_DB_NAME="yamtrack" PG_DB_USER="yamtrack" setup_postgresql_db
|
||||
PYTHON_VERSION="3.12" setup_uv
|
||||
|
||||
fetch_and_deploy_gh_release "yamtrack" "FuzzyGrim/Yamtrack" "tarball"
|
||||
|
||||
msg_info "Installing Python Dependencies"
|
||||
cd /opt/yamtrack
|
||||
$STD uv venv .venv
|
||||
$STD uv pip install --no-cache-dir -r requirements.txt
|
||||
msg_ok "Installed Python Dependencies"
|
||||
|
||||
msg_info "Configuring Yamtrack"
|
||||
SECRET=$(openssl rand -hex 32)
|
||||
cat <<EOF >/opt/yamtrack/src/.env
|
||||
SECRET=${SECRET}
|
||||
DB_HOST=localhost
|
||||
DB_NAME=${PG_DB_NAME}
|
||||
DB_USER=${PG_DB_USER}
|
||||
DB_PASSWORD=${PG_DB_PASS}
|
||||
DB_PORT=5432
|
||||
REDIS_URL=redis://localhost:6379
|
||||
URLS=http://${LOCAL_IP}:8000
|
||||
EOF
|
||||
|
||||
cd /opt/yamtrack/src
|
||||
$STD /opt/yamtrack/.venv/bin/python manage.py migrate
|
||||
$STD /opt/yamtrack/.venv/bin/python manage.py collectstatic --noinput
|
||||
msg_ok "Configured Yamtrack"
|
||||
|
||||
msg_info "Configuring Nginx"
|
||||
rm -f /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default
|
||||
cp /opt/yamtrack/nginx.conf /etc/nginx/nginx.conf
|
||||
sed -i 's|user abc;|user www-data;|' /etc/nginx/nginx.conf
|
||||
sed -i 's|pid /tmp/nginx.pid;|pid /run/nginx.pid;|' /etc/nginx/nginx.conf
|
||||
sed -i 's|/yamtrack/staticfiles/|/opt/yamtrack/src/staticfiles/|' /etc/nginx/nginx.conf
|
||||
sed -i 's|error_log /dev/stderr|error_log /var/log/nginx/error.log|' /etc/nginx/nginx.conf
|
||||
sed -i 's|access_log /dev/stdout|access_log /var/log/nginx/access.log|' /etc/nginx/nginx.conf
|
||||
$STD nginx -t
|
||||
systemctl enable -q nginx
|
||||
$STD systemctl restart nginx
|
||||
msg_ok "Configured Nginx"
|
||||
|
||||
msg_info "Creating Services"
|
||||
cat <<EOF >/etc/systemd/system/yamtrack.service
|
||||
[Unit]
|
||||
Description=Yamtrack Gunicorn
|
||||
After=network.target postgresql.service redis-server.service
|
||||
Requires=postgresql.service redis-server.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/opt/yamtrack/src
|
||||
ExecStart=/opt/yamtrack/.venv/bin/gunicorn config.wsgi:application -b 127.0.0.1:8001 -w 2 --timeout 120
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
cat <<EOF >/etc/systemd/system/yamtrack-celery.service
|
||||
[Unit]
|
||||
Description=Yamtrack Celery Worker
|
||||
After=network.target postgresql.service redis-server.service yamtrack.service
|
||||
Requires=postgresql.service redis-server.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/opt/yamtrack/src
|
||||
ExecStart=/opt/yamtrack/.venv/bin/celery -A config worker --beat --scheduler django --loglevel INFO
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl enable -q --now redis-server yamtrack yamtrack-celery
|
||||
msg_ok "Created Services"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"name": "Unsloth Studio (In Beta)",
|
||||
"slug": "unsolth-studio",
|
||||
"categories": [20],
|
||||
"date_created": "2026-03-18",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 8888,
|
||||
"documentation": "https://unsloth.ai/docs/new/studio/start",
|
||||
"website": "https://unsloth.ai/",
|
||||
"logo": "https://unsloth.ai/favicon.ico",
|
||||
"config_path": "/opt/unsolth-studio/.env",
|
||||
"description": "Local, browser-based GUI for fine-tuning LLMs without writing code. Supports QLoRA, LoRA, and full fine-tuning with live training monitoring, GPU stats, and model export to GGUF/Safetensors.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/unsolth-studio.sh",
|
||||
"resources": {
|
||||
"cpu": 4,
|
||||
"ram": 16384,
|
||||
"hdd": 50,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Requires GPU passthrough for training. NVIDIA, AMD (ROCm), and Intel GPUs are supported.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "First launch takes 5-10 minutes to compile llama.cpp binaries.",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "Minimum 16GB RAM recommended for training models up to 7B parameters.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "For AMD GPUs, ensure ROCm is properly configured on the host.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
+4
-3
@@ -91,17 +91,18 @@ msg_info "Running Unsloth Studio Setup"
|
||||
# This requires GPU access - set up environment for ROCm if installed
|
||||
|
||||
# Set up ROCm environment if available
|
||||
# Use ${VAR:-} to handle unset variables (set -u causes errors otherwise)
|
||||
if [ -d "/opt/rocm" ]; then
|
||||
export PATH="/opt/rocm/bin:$PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm/lib:$LD_LIBRARY_PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||
export ROCM_PATH="/opt/rocm"
|
||||
elif [ -d "/opt/rocm-7.2" ]; then
|
||||
export PATH="/opt/rocm-7.2/bin:$PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm-7.2/lib:$LD_LIBRARY_PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm-7.2/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||
export ROCM_PATH="/opt/rocm-7.2"
|
||||
elif [ -d "/opt/rocm-6.2" ]; then
|
||||
export PATH="/opt/rocm-6.2/bin:$PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm-6.2/lib:$LD_LIBRARY_PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm-6.2/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||
export ROCM_PATH="/opt/rocm-6.2"
|
||||
fi
|
||||
|
||||
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"name": "Unsloth Studio (In Beta)",
|
||||
"slug": "unsolth-studio",
|
||||
"categories": [20],
|
||||
"date_created": "2026-03-18",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 8888,
|
||||
"documentation": "https://unsloth.ai/docs/new/studio/start",
|
||||
"website": "https://unsloth.ai/",
|
||||
"logo": "https://unsloth.ai/favicon.ico",
|
||||
"config_path": "/opt/unsolth-studio/.env",
|
||||
"description": "Local, browser-based GUI for fine-tuning LLMs without writing code. Supports QLoRA, LoRA, and full fine-tuning with live training monitoring, GPU stats, and model export to GGUF/Safetensors.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/unsolth-studio.sh",
|
||||
"resources": {
|
||||
"cpu": 4,
|
||||
"ram": 16384,
|
||||
"hdd": 50,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Requires GPU passthrough for training. NVIDIA, AMD (ROCm), and Intel GPUs are supported.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "First launch takes 5-10 minutes to compile llama.cpp binaries.",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "Minimum 16GB RAM recommended for training models up to 7B parameters.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "For AMD GPUs, ensure ROCm is properly configured on the host.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"name": "Unsloth Studio (In Beta)",
|
||||
"slug": "unsolth-studio",
|
||||
"categories": [20],
|
||||
"date_created": "2026-03-18",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 8888,
|
||||
"documentation": "https://unsloth.ai/docs/new/studio/start",
|
||||
"website": "https://unsloth.ai/",
|
||||
"logo": "https://unsloth.ai/favicon.ico",
|
||||
"config_path": "/opt/unsolth-studio/.env",
|
||||
"description": "Local, browser-based GUI for fine-tuning LLMs without writing code. Supports QLoRA, LoRA, and full fine-tuning with live training monitoring, GPU stats, and model export to GGUF/Safetensors.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/unsolth-studio.sh",
|
||||
"resources": {
|
||||
"cpu": 4,
|
||||
"ram": 16384,
|
||||
"hdd": 50,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Requires GPU passthrough for training. NVIDIA, AMD (ROCm), and Intel GPUs are supported.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "First launch takes 5-10 minutes to compile llama.cpp binaries.",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "Minimum 16GB RAM recommended for training models up to 7B parameters.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "For AMD GPUs, ensure ROCm is properly configured on the host.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
+109
-11
@@ -30,20 +30,55 @@ msg_ok "Installed Dependencies"
|
||||
setup_hwaccel
|
||||
|
||||
# Setup Python virtual environment with uv (fast Python package manager)
|
||||
PYTHON_VERSION="3.12" setup_uv
|
||||
PYTHON_VERSION="3.13" setup_uv
|
||||
|
||||
msg_info "Creating Virtual Environment"
|
||||
mkdir -p /opt/unsolth-studio
|
||||
cd /opt/unsolth-studio || exit
|
||||
$STD uv venv --python 3.12
|
||||
$STD uv venv --python 3.13
|
||||
source .venv/bin/activate
|
||||
msg_ok "Created Virtual Environment"
|
||||
|
||||
msg_info "Installing PyTorch"
|
||||
# Install PyTorch first (required by unsloth)
|
||||
# Use CPU version for broader compatibility; GPU version can be installed manually if needed
|
||||
$STD uv pip install torch --index-url https://download.pytorch.org/whl/cpu
|
||||
msg_ok "Installed PyTorch"
|
||||
msg_info "Detecting GPU Type for PyTorch Installation"
|
||||
# Detect GPU type based on what setup_hwaccel installed
|
||||
# setup_hwaccel runs before this and installs NVIDIA drivers or ROCm
|
||||
|
||||
GPU_TYPE="cpu"
|
||||
|
||||
# Check for NVIDIA GPU (nvidia-smi installed by setup_hwaccel)
|
||||
if command -v nvidia-smi &>/dev/null && nvidia-smi &>/dev/null; then
|
||||
GPU_TYPE="nvidia"
|
||||
msg_info "NVIDIA GPU detected - installing PyTorch with CUDA support"
|
||||
# Check for AMD GPU (ROCm installed by setup_hwaccel at /opt/rocm)
|
||||
elif [ -d "/opt/rocm" ] || [ -d "/opt/rocm-7.2" ] || [ -d "/opt/rocm-6.2" ]; then
|
||||
GPU_TYPE="amd"
|
||||
msg_info "AMD GPU detected (ROCm installed) - installing PyTorch with ROCm support"
|
||||
# Check for AMD render devices (GPU passthrough configured)
|
||||
elif ls /dev/dri/renderD* &>/dev/null 2>&1; then
|
||||
# Check if any render device is AMD
|
||||
for render_dev in /dev/dri/renderD*; do
|
||||
if [ -e "$render_dev" ]; then
|
||||
GPU_TYPE="amd"
|
||||
msg_info "AMD GPU detected (render device) - installing PyTorch with ROCm support"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ "$GPU_TYPE" = "nvidia" ]; then
|
||||
# NVIDIA GPU - install PyTorch with CUDA 12.4 support
|
||||
$STD uv pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124
|
||||
msg_ok "Installed PyTorch with CUDA Support"
|
||||
elif [ "$GPU_TYPE" = "amd" ]; then
|
||||
# AMD GPU - install PyTorch with ROCm 7.2 support
|
||||
$STD uv pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/test/rocm7.2
|
||||
msg_ok "Installed PyTorch with ROCm Support"
|
||||
else
|
||||
# No GPU detected - install CPU version
|
||||
msg_info "No GPU detected - installing PyTorch CPU version"
|
||||
$STD uv pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
|
||||
msg_ok "Installed PyTorch (CPU version)"
|
||||
fi
|
||||
|
||||
msg_info "Installing Unsloth"
|
||||
# Install unsloth and its dependencies
|
||||
@@ -53,9 +88,36 @@ msg_ok "Installed Unsloth"
|
||||
|
||||
msg_info "Running Unsloth Studio Setup"
|
||||
# Run the unsloth studio setup command to compile llama.cpp
|
||||
# Use Python module invocation since uv pip install doesn't create entry points
|
||||
$STD /opt/unsolth-studio/.venv/bin/python -m unsloth studio setup
|
||||
msg_ok "Completed Unsloth Studio Setup"
|
||||
# This requires GPU access - set up environment for ROCm if installed
|
||||
|
||||
# Set up ROCm environment if available
|
||||
if [ -d "/opt/rocm" ]; then
|
||||
export PATH="/opt/rocm/bin:$PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm/lib:$LD_LIBRARY_PATH"
|
||||
export ROCM_PATH="/opt/rocm"
|
||||
elif [ -d "/opt/rocm-7.2" ]; then
|
||||
export PATH="/opt/rocm-7.2/bin:$PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm-7.2/lib:$LD_LIBRARY_PATH"
|
||||
export ROCM_PATH="/opt/rocm-7.2"
|
||||
elif [ -d "/opt/rocm-6.2" ]; then
|
||||
export PATH="/opt/rocm-6.2/bin:$PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm-6.2/lib:$LD_LIBRARY_PATH"
|
||||
export ROCM_PATH="/opt/rocm-6.2"
|
||||
fi
|
||||
|
||||
# Check if GPU is available (works for both CUDA and ROCm)
|
||||
if /opt/unsolth-studio/.venv/bin/python -c "import torch; exit(0 if torch.cuda.is_available() else 1)" 2>/dev/null; then
|
||||
$STD /opt/unsolth-studio/.venv/bin/python -m unsloth studio setup
|
||||
msg_ok "Completed Unsloth Studio Setup"
|
||||
else
|
||||
msg_info "GPU not detected via torch.cuda - skipping Unsloth Studio setup"
|
||||
msg_info "This may be normal if ROCm libraries need system restart to take effect"
|
||||
echo ""
|
||||
echo -e "${GN}Note: If you have GPU passthrough configured, try:${CL}"
|
||||
echo -e "${GN} 1. Restart the container: pct stop <CTID> && pct start <CTID>${CL}"
|
||||
echo -e "${GN} 2. Then run: source /opt/unsolth-studio/.venv/bin/activate && unsloth studio setup${CL}"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
msg_info "Creating Directories"
|
||||
mkdir -p /opt/unsolth-studio/models
|
||||
@@ -65,6 +127,34 @@ chmod 755 /var/log/unsolth-studio
|
||||
msg_ok "Created Directories"
|
||||
|
||||
msg_info "Creating Service"
|
||||
# Create environment file for ROCm/CUDA paths
|
||||
cat <<EOF >/opt/unsolth-studio/environment.sh
|
||||
#!/bin/bash
|
||||
# Set up GPU environment for Unsloth Studio
|
||||
|
||||
# ROCm environment (AMD GPUs)
|
||||
if [ -d "/opt/rocm" ]; then
|
||||
export PATH="/opt/rocm/bin:\$PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm/lib:\$LD_LIBRARY_PATH"
|
||||
export ROCM_PATH="/opt/rocm"
|
||||
elif [ -d "/opt/rocm-7.2" ]; then
|
||||
export PATH="/opt/rocm-7.2/bin:\$PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm-7.2/lib:\$LD_LIBRARY_PATH"
|
||||
export ROCM_PATH="/opt/rocm-7.2"
|
||||
elif [ -d "/opt/rocm-6.2" ]; then
|
||||
export PATH="/opt/rocm-6.2/bin:\$PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm-6.2/lib:\$LD_LIBRARY_PATH"
|
||||
export ROCM_PATH="/opt/rocm-6.2"
|
||||
fi
|
||||
|
||||
# NVIDIA CUDA environment
|
||||
if [ -d "/usr/local/cuda" ]; then
|
||||
export PATH="/usr/local/cuda/bin:\$PATH"
|
||||
export LD_LIBRARY_PATH="/usr/local/cuda/lib64:\$LD_LIBRARY_PATH"
|
||||
fi
|
||||
EOF
|
||||
chmod +x /opt/unsolth-studio/environment.sh
|
||||
|
||||
cat <<EOF >/etc/systemd/system/unsolth-studio.service
|
||||
[Unit]
|
||||
Description=Unsloth Studio - Local LLM Fine-tuning Web UI
|
||||
@@ -75,6 +165,7 @@ Wants=network-online.target
|
||||
Type=simple
|
||||
WorkingDirectory=/opt/unsolth-studio
|
||||
Environment="PATH=/opt/unsolth-studio/.venv/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
EnvironmentFile=/opt/unsolth-studio/environment.sh
|
||||
ExecStart=/opt/unsolth-studio/.venv/bin/python -m unsloth studio -H 0.0.0.0 -p 8888
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
@@ -90,8 +181,15 @@ TimeoutStopSec=60
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q --now unsolth-studio
|
||||
# Don't auto-start the service since GPU passthrough may not be configured yet
|
||||
# User needs to configure GPU passthrough first, then start the service manually
|
||||
systemctl enable -q unsolth-studio
|
||||
msg_ok "Created Service"
|
||||
echo ""
|
||||
echo -e "${GN}Note: The unsolth-studio service is enabled but not started.${CL}"
|
||||
echo -e "${GN}Configure GPU passthrough first, then start with:${CL}"
|
||||
echo -e "${GN} systemctl start unsolth-studio${CL}"
|
||||
echo ""
|
||||
|
||||
# Create GPU passthrough info file
|
||||
cat <<EOF >/opt/unsolth-studio/GPU_PASSTHROUGH.md
|
||||
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Author: Heretek-AI
|
||||
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/unslothai/unsloth
|
||||
|
||||
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 \
|
||||
curl \
|
||||
wget \
|
||||
git \
|
||||
cmake \
|
||||
build-essential \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
pciutils
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
# Setup GPU hardware acceleration FIRST (detects GPU, installs drivers, configures permissions)
|
||||
# This must run before installing unsloth/torch so PyTorch can detect the GPU
|
||||
setup_hwaccel
|
||||
|
||||
# Setup Python virtual environment with uv (fast Python package manager)
|
||||
PYTHON_VERSION="3.12" setup_uv
|
||||
|
||||
msg_info "Creating Virtual Environment"
|
||||
mkdir -p /opt/unsolth-studio
|
||||
cd /opt/unsolth-studio || exit
|
||||
$STD uv venv --python 3.12
|
||||
source .venv/bin/activate
|
||||
msg_ok "Created Virtual Environment"
|
||||
|
||||
msg_info "Installing PyTorch"
|
||||
# Install PyTorch first (required by unsloth)
|
||||
# Use CPU version for broader compatibility; GPU version can be installed manually if needed
|
||||
$STD uv pip install torch --index-url https://download.pytorch.org/whl/cpu
|
||||
msg_ok "Installed PyTorch"
|
||||
|
||||
msg_info "Installing Unsloth"
|
||||
# Install unsloth and its dependencies
|
||||
# packaging module is required but not declared as dependency
|
||||
$STD uv pip install unsloth packaging
|
||||
msg_ok "Installed Unsloth"
|
||||
|
||||
msg_info "Running Unsloth Studio Setup"
|
||||
# Run the unsloth studio setup command to compile llama.cpp
|
||||
# Use Python module invocation since uv pip install doesn't create entry points
|
||||
$STD /opt/unsolth-studio/.venv/bin/python -m unsloth studio setup
|
||||
msg_ok "Completed Unsloth Studio Setup"
|
||||
|
||||
msg_info "Creating Directories"
|
||||
mkdir -p /opt/unsolth-studio/models
|
||||
mkdir -p /opt/unsolth-studio/datasets
|
||||
mkdir -p /var/log/unsolth-studio
|
||||
chmod 755 /var/log/unsolth-studio
|
||||
msg_ok "Created Directories"
|
||||
|
||||
msg_info "Creating Service"
|
||||
cat <<EOF >/etc/systemd/system/unsolth-studio.service
|
||||
[Unit]
|
||||
Description=Unsloth Studio - Local LLM Fine-tuning Web UI
|
||||
After=network.target network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/opt/unsolth-studio
|
||||
Environment="PATH=/opt/unsolth-studio/.venv/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
ExecStart=/opt/unsolth-studio/.venv/bin/python -m unsloth studio -H 0.0.0.0 -p 8888
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=unsolth-studio
|
||||
|
||||
# Resource limits
|
||||
LimitNOFILE=65535
|
||||
TimeoutStartSec=600
|
||||
TimeoutStopSec=60
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q --now unsolth-studio
|
||||
msg_ok "Created Service"
|
||||
|
||||
# Create GPU passthrough info file
|
||||
cat <<EOF >/opt/unsolth-studio/GPU_PASSTHROUGH.md
|
||||
# GPU Passthrough Configuration for Unsloth Studio
|
||||
|
||||
This container has been configured for GPU acceleration for LLM fine-tuning.
|
||||
|
||||
## Required Proxmox Configuration
|
||||
|
||||
Add the following lines to your container config file:
|
||||
/etc/pve/lxc/<CTID>.conf
|
||||
|
||||
### For NVIDIA GPUs:
|
||||
\`\`\`
|
||||
# Requires nvidia-container-toolkit on host
|
||||
lxc.cgroup2.devices.allow: c 195:* rwm
|
||||
lxc.cgroup2.devices.allow: c 509:* rwm
|
||||
dev0: /dev/nvidia0,gid=104
|
||||
dev1: /dev/nvidiactl,gid=104
|
||||
dev2: /dev/nvidia-uvm,gid=104
|
||||
dev3: /dev/nvidia-uvm-tools,gid=104
|
||||
\`\`\`
|
||||
|
||||
### For AMD GPUs (ROCm):
|
||||
\`\`\`
|
||||
dev0: /dev/kfd,gid=104
|
||||
dev1: /dev/dri/renderD128,gid=104
|
||||
lxc.cgroup2.devices.allow: c 226:0 rwm
|
||||
lxc.cgroup2.devices.allow: c 226:128 rwm
|
||||
\`\`\`
|
||||
|
||||
### For Intel GPUs:
|
||||
\`\`\`
|
||||
dev0: /dev/dri/renderD128,gid=104
|
||||
lxc.cgroup2.devices.allow: c 226:128 rwm
|
||||
\`\`\`
|
||||
|
||||
## Verify GPU Access
|
||||
|
||||
Run these commands inside the container:
|
||||
- nvidia-smi (NVIDIA GPUs)
|
||||
- rocminfo (AMD GPUs)
|
||||
- python -c "import torch; print(torch.cuda.is_available())"
|
||||
|
||||
## Usage
|
||||
|
||||
Access the web UI at: http://<IP>:8888
|
||||
|
||||
On first launch:
|
||||
1. Create a password to secure your account
|
||||
2. Follow the onboarding wizard to select a model and dataset
|
||||
3. Configure training parameters
|
||||
4. Start fine-tuning!
|
||||
|
||||
## Supported Models
|
||||
|
||||
Unsloth Studio supports fine-tuning many LLM models including:
|
||||
- Llama 3.x
|
||||
- Qwen 2.x / 3.x
|
||||
- Mistral
|
||||
- Gemma
|
||||
- Phi-3
|
||||
- And many more...
|
||||
|
||||
## Documentation
|
||||
|
||||
- Official Docs: https://unsloth.ai/docs/new/studio/start
|
||||
- GitHub: https://github.com/unslothai/unsloth
|
||||
EOF
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generated": "2026-03-15T12:22:39Z",
|
||||
"generated": "2026-03-16T18:42:54Z",
|
||||
"versions": [
|
||||
{
|
||||
"slug": "agregarr",
|
||||
@@ -18,9 +18,9 @@
|
||||
{
|
||||
"slug": "llamacpp",
|
||||
"repo": "ggml-org/llama.cpp",
|
||||
"version": "b8354",
|
||||
"version": "b8373",
|
||||
"pinned": false,
|
||||
"date": "2026-03-15T10:06:38Z"
|
||||
"date": "2026-03-16T10:55:12Z"
|
||||
},
|
||||
{
|
||||
"slug": "localai",
|
||||
@@ -46,9 +46,9 @@
|
||||
{
|
||||
"slug": "pegaprox",
|
||||
"repo": "PegaProx/project-pegaprox",
|
||||
"version": "v0.9.1.3",
|
||||
"version": "v0.9.2",
|
||||
"pinned": false,
|
||||
"date": "2026-03-11T20:23:52Z"
|
||||
"date": "2026-03-16T08:00:29Z"
|
||||
},
|
||||
{
|
||||
"slug": "ragflow",
|
||||
|
||||
+16
-8
@@ -48,7 +48,8 @@ jobs:
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const url = require('url');
|
||||
function request(fullUrl, opts) {
|
||||
function request(fullUrl, opts, redirectCount) {
|
||||
redirectCount = redirectCount || 0;
|
||||
return new Promise(function(resolve, reject) {
|
||||
const u = url.parse(fullUrl);
|
||||
const isHttps = u.protocol === 'https:';
|
||||
@@ -63,6 +64,13 @@ jobs:
|
||||
if (body) options.headers['Content-Length'] = Buffer.byteLength(body);
|
||||
const lib = isHttps ? https : http;
|
||||
const req = lib.request(options, function(res) {
|
||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||
if (redirectCount >= 5) return reject(new Error('Too many redirects from ' + fullUrl));
|
||||
const redirectUrl = url.resolve(fullUrl, res.headers.location);
|
||||
res.resume();
|
||||
resolve(request(redirectUrl, opts, redirectCount + 1));
|
||||
return;
|
||||
}
|
||||
let data = '';
|
||||
res.on('data', function(chunk) { data += chunk; });
|
||||
res.on('end', function() {
|
||||
@@ -125,15 +133,15 @@ jobs:
|
||||
var osVersionToId = {};
|
||||
try {
|
||||
const res = await request(apiBase + '/collections/z_ref_note_types/records?perPage=500', { headers: { 'Authorization': token } });
|
||||
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.type != null) noteTypeToId[item.type] = item.id; });
|
||||
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.type != null) { noteTypeToId[item.type] = item.id; noteTypeToId[item.type.toLowerCase()] = item.id; } });
|
||||
} catch (e) { console.warn('z_ref_note_types:', e.message); }
|
||||
try {
|
||||
const res = await request(apiBase + '/collections/z_ref_install_method_types/records?perPage=500', { headers: { 'Authorization': token } });
|
||||
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.type != null) installMethodTypeToId[item.type] = item.id; });
|
||||
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.type != null) { installMethodTypeToId[item.type] = item.id; installMethodTypeToId[item.type.toLowerCase()] = item.id; } });
|
||||
} catch (e) { console.warn('z_ref_install_method_types:', e.message); }
|
||||
try {
|
||||
const res = await request(apiBase + '/collections/z_ref_os/records?perPage=500', { headers: { 'Authorization': token } });
|
||||
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.os != null) osToId[item.os] = item.id; });
|
||||
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.os != null) { osToId[item.os] = item.id; osToId[item.os.toLowerCase()] = item.id; } });
|
||||
} catch (e) { console.warn('z_ref_os:', e.message); }
|
||||
try {
|
||||
const res = await request(apiBase + '/collections/z_ref_os_version/records?perPage=500&expand=os', { headers: { 'Authorization': token } });
|
||||
@@ -154,7 +162,7 @@ jobs:
|
||||
name: data.name,
|
||||
slug: data.slug,
|
||||
script_created: data.date_created || data.script_created,
|
||||
script_updated: data.date_created || data.script_updated,
|
||||
script_updated: new Date().toISOString().split('T')[0],
|
||||
updateable: data.updateable,
|
||||
privileged: data.privileged,
|
||||
port: data.interface_port != null ? data.interface_port : data.port,
|
||||
@@ -163,8 +171,8 @@ jobs:
|
||||
logo: data.logo,
|
||||
description: data.description,
|
||||
config_path: data.config_path,
|
||||
default_user: (data.default_credentials && data.default_credentials.username) || data.default_user,
|
||||
default_passwd: (data.default_credentials && data.default_credentials.password) || data.default_passwd,
|
||||
default_user: (data.default_credentials && data.default_credentials.username) || data.default_user || null,
|
||||
default_passwd: (data.default_credentials && data.default_credentials.password) || data.default_passwd || null,
|
||||
is_dev: false
|
||||
};
|
||||
var resolvedType = typeValueToId[data.type];
|
||||
@@ -190,7 +198,7 @@ jobs:
|
||||
var postRes = await request(notesCollUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: note.text || '', type: typeId })
|
||||
body: JSON.stringify({ text: note.text || '', type: typeId, script: scriptId })
|
||||
});
|
||||
if (postRes.ok) noteIds.push(JSON.parse(postRes.body).id);
|
||||
}
|
||||
|
||||
+100
-50
@@ -3,12 +3,12 @@ name: Sync Upstream (Git Merge Strategy)
|
||||
on:
|
||||
schedule:
|
||||
# Runs automatically every day at 2:00 AM UTC
|
||||
- cron: '0 2 * * *'
|
||||
- cron: "0 2 * * *"
|
||||
workflow_dispatch:
|
||||
# Allows manual trigger from the Actions tab
|
||||
inputs:
|
||||
force_sync:
|
||||
description: 'Force sync even if no new commits detected'
|
||||
description: "Force sync even if no new commits detected"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
|
||||
# Configure the 'ours' merge driver for fork-specific files
|
||||
# This tells git to use our version for files marked with merge=ours in .gitattributes
|
||||
git config merge.ours.name "ours merge driver"
|
||||
@@ -64,17 +64,17 @@ jobs:
|
||||
run: |
|
||||
# Get the merge base between fork and upstream
|
||||
MERGE_BASE=$(git merge-base HEAD upstream/${{ env.UPSTREAM_BRANCH }} 2>/dev/null || echo "")
|
||||
|
||||
|
||||
if [ -z "$MERGE_BASE" ]; then
|
||||
echo "No common ancestor found - this may be a fresh sync"
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
echo "commit_count=unknown" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
# Count commits in upstream that are not in fork
|
||||
UPSTREAM_COMMITS=$(git rev-list $MERGE_BASE..upstream/${{ env.UPSTREAM_BRANCH }} --count 2>/dev/null || echo "0")
|
||||
|
||||
|
||||
if [ "$UPSTREAM_COMMITS" -eq 0 ] && [ "${{ github.event.inputs.force_sync }}" != "true" ]; then
|
||||
echo "No new commits from upstream. Nothing to sync."
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
|
||||
# Configure the 'ours' merge driver
|
||||
# Files with merge=ours in .gitattributes will automatically keep fork version
|
||||
git config merge.ours.name "ours merge driver"
|
||||
@@ -135,14 +135,14 @@ jobs:
|
||||
id: merge
|
||||
run: |
|
||||
echo "Starting merge from upstream/${{ env.UPSTREAM_BRANCH }}..."
|
||||
|
||||
|
||||
# Attempt merge with the 'ours' strategy for conflicts
|
||||
# The .gitattributes file configures which files use merge=ours
|
||||
git merge upstream/${{ env.UPSTREAM_BRANCH }} \
|
||||
--no-edit \
|
||||
-m "Merge upstream ${{ env.UPSTREAM_REPO }} into ${{ env.FORK_BRANCH }}" \
|
||||
2>&1 || MERGE_STATUS=$?
|
||||
|
||||
|
||||
if [ "${MERGE_STATUS:-0}" -eq 0 ]; then
|
||||
echo "Merge completed successfully with no conflicts."
|
||||
echo "merge_status=success" >> $GITHUB_OUTPUT
|
||||
@@ -152,29 +152,70 @@ jobs:
|
||||
echo "merge_status=conflicts" >> $GITHUB_OUTPUT
|
||||
echo "has_conflicts=true" >> $GITHUB_OUTPUT
|
||||
|
||||
# List conflicted files
|
||||
# List all conflicted files
|
||||
CONFLICTS=$(git diff --name-only --diff-filter=U 2>/dev/null || echo "")
|
||||
echo "conflicted_files<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$CONFLICTS" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
# Check if conflicts are only in fork-specific files
|
||||
# These should have been handled by merge=ours, but check anyway
|
||||
FORK_FILES=$(git diff --name-only --diff-filter=U \
|
||||
-- 'install/' 'ct/' 'frontend/' 'misc/images/' '.github/workflows/' '.github/runner/' 'tools/addon/' 'README.md' 'CHANGELOG.md' 2>/dev/null || echo "")
|
||||
echo "Resolving conflicts..."
|
||||
|
||||
if [ -n "$FORK_FILES" ]; then
|
||||
echo "Warning: Fork-specific files have unexpected conflicts:"
|
||||
echo "$FORK_FILES"
|
||||
# For fork files, always take our version
|
||||
for file in $FORK_FILES; do
|
||||
# 1. Handle content conflicts in fork-specific files - always keep our version
|
||||
# These are files that exist in both but have different content
|
||||
FORK_CONTENT_FILES=$(git diff --name-only --diff-filter=U \
|
||||
-- 'CHANGELOG.md' 'misc/tools.func' 'README.md' 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$FORK_CONTENT_FILES" ]; then
|
||||
echo "Resolving content conflicts in fork-specific files:"
|
||||
echo "$FORK_CONTENT_FILES"
|
||||
for file in $FORK_CONTENT_FILES; do
|
||||
echo " Keeping fork version: $file"
|
||||
git checkout --ours "$file" 2>/dev/null || true
|
||||
git add "$file" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
|
||||
# For other conflicts, we leave them for manual resolution in the PR
|
||||
# This allows reviewers to see what needs attention
|
||||
# 2. Handle modify/delete conflicts - files deleted in fork but modified upstream
|
||||
# We want to keep our deletion (remove the file)
|
||||
MODIFY_DELETE_CONFLICTS=$(git status --porcelain | grep "^DU" | awk '{print $2}' 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$MODIFY_DELETE_CONFLICTS" ]; then
|
||||
echo "Resolving modify/delete conflicts (keeping fork deletions):"
|
||||
echo "$MODIFY_DELETE_CONFLICTS"
|
||||
for file in $MODIFY_DELETE_CONFLICTS; do
|
||||
echo " Removing file (fork deleted it): $file"
|
||||
git rm --ignore-unmatch "$file" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
|
||||
# 3. Handle any remaining content conflicts in fork directories
|
||||
REMAINING_CONFLICTS=$(git diff --name-only --diff-filter=U 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$REMAINING_CONFLICTS" ]; then
|
||||
echo "Resolving remaining conflicts in fork directories:"
|
||||
echo "$REMAINING_CONFLICTS"
|
||||
|
||||
# For files in fork-specific directories, keep our version
|
||||
FORK_DIR_CONFLICTS=$(echo "$REMAINING_CONFLICTS" | grep -E '^(install/|ct/|frontend/|misc/images/|tools/addon/)' || true)
|
||||
|
||||
if [ -n "$FORK_DIR_CONFLICTS" ]; then
|
||||
for file in $FORK_DIR_CONFLICTS; do
|
||||
echo " Keeping fork version: $file"
|
||||
git checkout --ours "$file" 2>/dev/null || true
|
||||
git add "$file" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if there are still unresolved conflicts
|
||||
REMAINING=$(git diff --name-only --diff-filter=U 2>/dev/null || echo "")
|
||||
if [ -n "$REMAINING" ]; then
|
||||
echo "Warning: Some conflicts could not be auto-resolved:"
|
||||
echo "$REMAINING"
|
||||
echo "These will need manual resolution in the PR."
|
||||
else
|
||||
echo "All conflicts resolved successfully."
|
||||
fi
|
||||
else
|
||||
echo "Merge failed with status: ${MERGE_STATUS}"
|
||||
echo "merge_status=failed" >> $GITHUB_OUTPUT
|
||||
@@ -188,22 +229,28 @@ jobs:
|
||||
echo "upstream_commits<<EOF" >> $GITHUB_OUTPUT
|
||||
git log HEAD..upstream/${{ env.UPSTREAM_BRANCH }} --oneline --no-merges | head -30 >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
# Get changed files summary
|
||||
echo "changed_files<<EOF" >> $GITHUB_OUTPUT
|
||||
git diff --name-only HEAD upstream/${{ env.UPSTREAM_BRANCH }} | head -50 >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
# Count files by category
|
||||
TOTAL_CHANGED=$(git diff --name-only HEAD upstream/${{ env.UPSTREAM_BRANCH }} | wc -l)
|
||||
echo "total_changed=$TOTAL_CHANGED" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Commit merge result
|
||||
if: steps.merge.outputs.merge_status == 'conflicts'
|
||||
run: |
|
||||
# Commit the merge with resolved fork-specific conflicts
|
||||
# Remaining conflicts will be visible in the PR for review
|
||||
git commit -m "Merge upstream with partial conflict resolution" -m "Fork-specific files preserved, other conflicts need review" || true
|
||||
# Check if there are staged changes to commit
|
||||
if git diff --cached --quiet 2>/dev/null && git diff --quiet 2>/dev/null; then
|
||||
echo "No changes to commit."
|
||||
else
|
||||
# Commit the merge with resolved conflicts
|
||||
git commit -m "Merge upstream ${{ env.UPSTREAM_REPO }} with conflict resolution" \
|
||||
-m "- Fork-specific files preserved (CHANGELOG.md, misc/tools.func)" \
|
||||
-m "- Deleted files kept as deleted (fork deletions preserved)" \
|
||||
-m "- New upstream scripts added" || echo "Commit may have already been made"
|
||||
fi
|
||||
|
||||
- name: Push sync branch
|
||||
run: |
|
||||
@@ -216,56 +263,60 @@ jobs:
|
||||
BRANCH="${{ steps.branch.outputs.branch_name }}"
|
||||
HAS_CONFLICTS="${{ steps.merge.outputs.has_conflicts }}"
|
||||
CONFLICTED_FILES="${{ steps.merge.outputs.conflicted_files }}"
|
||||
|
||||
|
||||
# Build PR body
|
||||
PR_BODY="## 🔄 Upstream Sync Summary
|
||||
|
||||
|
||||
This PR syncs the latest changes from upstream [\`${{ env.UPSTREAM_REPO }}\`](https://github.com/${{ env.UPSTREAM_REPO }}) while preserving fork-specific customizations.
|
||||
|
||||
### 📊 Sync Statistics
|
||||
- **Upstream commits:** ${{ needs.check-and-sync.outputs.commit_count }}
|
||||
- **Files changed:** ${{ steps.summary.outputs.total_changed }}
|
||||
- **Merge conflicts:** ${HAS_CONFLICTS:-none}
|
||||
|
||||
|
||||
### 📋 Upstream Commits (last 30)
|
||||
\`\`\`
|
||||
${{ steps.summary.outputs.upstream_commits }}
|
||||
\`\`\`
|
||||
|
||||
|
||||
### 📁 Changed Files
|
||||
<details>
|
||||
<summary>Click to view changed files</summary>
|
||||
|
||||
|
||||
\`\`\`
|
||||
${{ steps.summary.outputs.changed_files }}
|
||||
\`\`\`
|
||||
</details>
|
||||
"
|
||||
|
||||
# Add conflict warning if needed
|
||||
|
||||
# Add conflict resolution section
|
||||
if [ "$HAS_CONFLICTS" = "true" ]; then
|
||||
PR_BODY="$PR_BODY
|
||||
### ⚠️ Merge Conflicts Detected
|
||||
|
||||
The following files have conflicts that need manual review:
|
||||
### ⚠️ Conflicts Auto-Resolved
|
||||
|
||||
The following conflict resolution strategy was applied:
|
||||
|
||||
1. **Fork-specific content files** (CHANGELOG.md, misc/tools.func, README.md) - Kept fork version
|
||||
2. **Modify/delete conflicts** - Kept fork's deletions (files removed in fork stay removed)
|
||||
3. **Fork directory conflicts** (install/, ct/, frontend/) - Kept fork version
|
||||
|
||||
Original conflicted files:
|
||||
\`\`\`
|
||||
$CONFLICTED_FILES
|
||||
\`\`\`
|
||||
|
||||
**Note:** Fork-specific files have been automatically preserved using the \`merge=ours\` strategy configured in \`.gitattributes\`.
|
||||
"
|
||||
else
|
||||
PR_BODY="$PR_BODY
|
||||
### ✅ Clean Merge
|
||||
|
||||
No conflicts detected. Fork-specific files were automatically preserved using the \`merge=ours\` strategy.
|
||||
|
||||
No conflicts detected. All changes merged cleanly.
|
||||
"
|
||||
fi
|
||||
|
||||
|
||||
# Add preserved files section
|
||||
PR_BODY="$PR_BODY
|
||||
### 🔒 Fork-Specific Files Preserved
|
||||
|
||||
|
||||
The following files/directories are configured in \`.gitattributes\` to always keep the fork version:
|
||||
- \`install/\` - Custom install scripts
|
||||
- \`ct/\` - Custom container scripts
|
||||
@@ -276,25 +327,24 @@ jobs:
|
||||
- \`tools/addon/\` - Custom addon tools
|
||||
- \`README.md\` - Fork-specific documentation
|
||||
- \`CHANGELOG.md\` - Fork-specific changelog
|
||||
|
||||
|
||||
### ✅ Pre-Merge Checklist
|
||||
- [ ] Review all changed files for unexpected modifications
|
||||
- [ ] Verify fork-specific files are intact
|
||||
- [ ] Resolve any remaining merge conflicts
|
||||
- [ ] Test any new scripts or features from upstream
|
||||
|
||||
|
||||
---
|
||||
*This PR was automatically created by the [upstream-sync workflow](https://github.com/${{ github.repository }}/blob/main/.github/workflows/upstream-sync.yml).*
|
||||
"
|
||||
|
||||
|
||||
# Create PR title with conflict indicator
|
||||
if [ "$HAS_CONFLICTS" = "true" ]; then
|
||||
TITLE="🔄 Upstream Sync - $(date +%Y-%m-%d) ⚠️ CONFLICTS"
|
||||
TITLE="🔄 Upstream Sync - $(date +%Y-%m-%d) ✅ Conflicts Resolved"
|
||||
else
|
||||
TITLE="🔄 Upstream Sync - $(date +%Y-%m-%d)"
|
||||
fi
|
||||
|
||||
# Create the PR (labels are optional - will fail silently if labels don't exist)
|
||||
|
||||
# Create the PR
|
||||
gh pr create \
|
||||
--title "$TITLE" \
|
||||
--body "$PR_BODY" \
|
||||
|
||||
+2
@@ -420,6 +420,8 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
||||
|
||||
</details>
|
||||
|
||||
## 2026-03-18
|
||||
|
||||
## 2026-03-16
|
||||
|
||||
## 2026-03-15
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user