secubox-openwrt/package/secubox/secubox-app-hexojs/files/usr/sbin/hexoctl
CyberMind-FR 04908fc414 feat(multi): CrowdSec LAPI port fix, Streamlit/HexoJS multi-instance
CrowdSec:
- Change LAPI default port from 8080 to 8180 (avoid Docker conflict)
- Update bouncer config, init script, and RPCD dashboard
- Fix port detection hex value (1FF4 for 8180)

Streamlit:
- Complete rewrite with folder-based app structure
- Multi-instance support (multiple apps on different ports)
- Gitea integration (clone, pull, setup commands)
- Auto-install requirements.txt with hash-based caching

HexoJS:
- Multi-instance support with folder structure
- Multiple blog instances on different ports

HAProxy:
- Auto-generate fallback backends (luci, apps, default_luci)
- Add --server letsencrypt to ACME commands

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 06:37:19 +01:00

1247 lines
31 KiB
Bash

#!/bin/sh
# SecuBox Hexo CMS Controller
# Copyright (C) 2025 CyberMind.fr
#
# Manages Hexo static site generator in LXC container
# Supports multiple instances on different ports
CONFIG="hexojs"
LXC_NAME="hexojs"
# Paths
LXC_PATH="/srv/lxc"
LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs"
LXC_CONFIG="$LXC_PATH/$LXC_NAME/config"
SHARE_PATH="/usr/share/hexojs"
# Logging
log_info() { echo "[INFO] $*"; logger -t hexojs "$*"; }
log_warn() { echo "[WARN] $*" >&2; logger -t hexojs -p warning "$*"; }
log_error() { echo "[ERROR] $*" >&2; logger -t hexojs -p err "$*"; }
log_debug() { [ "$DEBUG" = "1" ] && echo "[DEBUG] $*"; }
# Helpers
require_root() {
[ "$(id -u)" -eq 0 ] || {
log_error "This command requires root privileges"
exit 1
}
}
has_lxc() { command -v lxc-start >/dev/null 2>&1; }
has_git() { command -v git >/dev/null 2>&1; }
ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; }
uci_get() { uci -q get ${CONFIG}.$1; }
uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; }
# Load main configuration
load_config() {
data_path="$(uci_get main.data_path)" || data_path="/srv/hexojs"
memory_limit="$(uci_get main.memory_limit)" || memory_limit="512M"
# Legacy support: active_site for default instance
active_site="$(uci_get main.active_site)" || active_site="default"
http_port="$(uci_get main.http_port)" || http_port="4000"
# Gitea config (shared)
gitea_enabled="$(uci_get gitea.enabled)" || gitea_enabled="0"
gitea_url="$(uci_get gitea.url)" || gitea_url="http://192.168.255.1:3000"
gitea_user="$(uci_get gitea.user)" || gitea_user="admin"
gitea_token="$(uci_get gitea.token)" || gitea_token=""
gitea_content_repo="$(uci_get gitea.content_repo)" || gitea_content_repo="blog-content"
gitea_content_branch="$(uci_get gitea.content_branch)" || gitea_content_branch="main"
ensure_dir "$data_path"
ensure_dir "$data_path/instances"
ensure_dir "$data_path/themes"
}
# Load instance configuration
load_instance_config() {
local instance="$1"
[ -z "$instance" ] && instance="default"
current_instance="$instance"
# Check if instance section exists
local instance_type=$(uci_get "${instance}")
if [ "$instance_type" != "instance" ]; then
# Legacy: check if it's old-style site section or doesn't exist
if [ -z "$instance_type" ] && [ "$instance" = "default" ]; then
# Use legacy main config for default
instance_port="$http_port"
instance_title="$(uci_get default.title)" || instance_title="My Blog"
instance_theme="$(uci_get default.theme)" || instance_theme="cybermind"
instance_enabled="1"
else
instance_port=""
instance_title=""
instance_theme=""
instance_enabled="0"
return 1
fi
else
instance_port="$(uci_get ${instance}.port)" || instance_port="4000"
instance_title="$(uci_get ${instance}.title)" || instance_title="My Blog"
instance_theme="$(uci_get ${instance}.theme)" || instance_theme="cybermind"
instance_enabled="$(uci_get ${instance}.enabled)" || instance_enabled="0"
fi
instance_path="$data_path/instances/$instance"
instance_site="$instance_path/site"
return 0
}
# Get list of all enabled instances
get_enabled_instances() {
local instances=""
# Check for instance sections in UCI
for section in $(uci show hexojs 2>/dev/null | grep '=instance$' | cut -d'.' -f2 | cut -d'=' -f1); do
local enabled=$(uci_get "${section}.enabled")
[ "$enabled" = "1" ] && instances="$instances $section"
done
# If no instances defined, check for legacy default
if [ -z "$instances" ]; then
if [ -d "$data_path/site" ] || [ -d "$data_path/instances/default/site" ]; then
instances="default"
fi
fi
echo "$instances"
}
# Usage
usage() {
cat <<EOF
SecuBox Hexo CMS Controller (Multi-Instance)
Usage: $(basename $0) <command> [options]
Container Commands:
install Download and setup Hexo LXC container
uninstall Remove Hexo container (keeps data)
update Update Hexo and dependencies
status Show service status
Instance Management:
instance list List all instances
instance create <name> Create new instance
instance delete <name> Delete an instance
instance start <name> Start instance server
instance stop <name> Stop instance server
instance status <name> Show instance status
Site Management (operates on current/specified instance):
site create [instance] Create Hexo site for instance
site delete [instance] Delete site for instance
Content Commands:
new post "Title" [instance] Create new blog post
new page "Title" [instance] Create new page
new draft "Title" [instance] Create new draft
list posts [instance] List all posts
list drafts [instance] List all drafts
Build Commands:
serve [instance] Start preview server
build [instance] Generate static files
clean [instance] Clean generated files
deploy [instance] Deploy to configured target
publish [instance] Copy static files to /www/
Service Commands:
service-run Run all instances (for init)
service-stop Stop all instances
Gitea Integration:
gitea setup [instance] Configure git credentials
gitea clone [instance] Clone content repo
gitea sync [instance] Pull latest content
Utility:
shell Open shell in container
logs [instance] View logs
exec <cmd> Execute command in container
Examples:
hexoctl instance create myblog # Create new instance
hexoctl instance start myblog # Start on configured port
hexoctl site create myblog # Initialize Hexo site
hexoctl new post "Hello" myblog # Create post in myblog
Configuration:
/etc/config/hexojs
EOF
}
# Check prerequisites
lxc_check_prereqs() {
if ! has_lxc; then
log_error "LXC not installed. Install with: opkg install lxc lxc-common"
return 1
fi
return 0
}
# Create Node.js LXC rootfs from Alpine
lxc_create_rootfs() {
local rootfs="$LXC_ROOTFS"
local arch=$(uname -m)
log_info "Creating Alpine rootfs for Hexo..."
ensure_dir "$rootfs"
local alpine_version="3.21"
case "$arch" in
x86_64) alpine_arch="x86_64" ;;
aarch64) alpine_arch="aarch64" ;;
armv7l) alpine_arch="armv7" ;;
*) log_error "Unsupported architecture: $arch"; return 1 ;;
esac
local alpine_url="https://dl-cdn.alpinelinux.org/alpine/v${alpine_version}/releases/${alpine_arch}/alpine-minirootfs-${alpine_version}.0-${alpine_arch}.tar.gz"
local tmpfile="/tmp/alpine-rootfs.tar.gz"
log_info "Downloading Alpine rootfs..."
wget -q -O "$tmpfile" "$alpine_url" || {
log_error "Failed to download Alpine rootfs"
return 1
}
log_info "Extracting rootfs..."
tar -xzf "$tmpfile" -C "$rootfs" || {
log_error "Failed to extract rootfs"
return 1
}
rm -f "$tmpfile"
# Setup resolv.conf
cp /etc/resolv.conf "$rootfs/etc/resolv.conf" 2>/dev/null || \
echo "nameserver 1.1.1.1" > "$rootfs/etc/resolv.conf"
# Create Hexo directory
ensure_dir "$rootfs/opt/hexojs"
log_info "Rootfs created successfully"
return 0
}
# Create LXC config
lxc_create_config() {
load_config
ensure_dir "$(dirname "$LXC_CONFIG")"
# Convert memory limit to bytes
local mem_bytes
case "$memory_limit" in
*G|*g) mem_bytes=$((${memory_limit%[Gg]} * 1024 * 1024 * 1024)) ;;
*M|*m) mem_bytes=$((${memory_limit%[Mm]} * 1024 * 1024)) ;;
*K|*k) mem_bytes=$((${memory_limit%[Kk]} * 1024)) ;;
*) mem_bytes="$memory_limit" ;;
esac
cat > "$LXC_CONFIG" << EOF
# Hexo CMS LXC Configuration (Multi-Instance)
lxc.uts.name = $LXC_NAME
lxc.rootfs.path = dir:$LXC_ROOTFS
lxc.arch = $(uname -m)
# Network: use host network
lxc.net.0.type = none
# Mount points
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
lxc.mount.entry = $data_path opt/hexojs none bind,create=dir 0 0
# Environment
lxc.environment = NODE_ENV=production
# Security
lxc.cap.drop = sys_admin sys_module mac_admin mac_override sys_time sys_rawio
# Resource limits
lxc.cgroup.memory.limit_in_bytes = $mem_bytes
# Init command - multi-instance manager
lxc.init.cmd = /opt/start-hexo-multi.sh
EOF
log_info "LXC config created"
}
# Create multi-instance startup script
create_startup_script() {
load_config
local start_script="$LXC_ROOTFS/opt/start-hexo-multi.sh"
cat > "$start_script" << 'STARTEOF'
#!/bin/sh
export PATH=/usr/local/bin:/usr/bin:/bin:$PATH
export HOME=/root
export NODE_ENV=production
HEXO_BASE="/opt/hexojs"
INSTANCES_DIR="$HEXO_BASE/instances"
PIDS_DIR="/var/run/hexo"
LOG_DIR="/var/log/hexo"
mkdir -p "$PIDS_DIR" "$LOG_DIR"
# Install dependencies on first run
if [ ! -f /opt/.installed ]; then
echo "Installing Node.js and Hexo..."
apk update
apk add --no-cache nodejs npm git openssh-client
npm install -g hexo-cli
touch /opt/.installed
fi
# Function to start a single instance
start_instance() {
local name="$1"
local port="$2"
local site_dir="$INSTANCES_DIR/$name/site"
[ -d "$site_dir" ] || return 1
[ -f "$site_dir/package.json" ] || return 1
echo "Starting instance '$name' on port $port..."
cd "$site_dir"
[ -d "node_modules" ] || npm install
# Start hexo server in background
nohup npx hexo server -p "$port" -i 0.0.0.0 > "$LOG_DIR/$name.log" 2>&1 &
echo $! > "$PIDS_DIR/$name.pid"
echo "Instance '$name' started (PID: $!)"
}
# Function to stop an instance
stop_instance() {
local name="$1"
local pidfile="$PIDS_DIR/$name.pid"
if [ -f "$pidfile" ]; then
local pid=$(cat "$pidfile")
if kill -0 "$pid" 2>/dev/null; then
kill "$pid"
echo "Stopped instance '$name' (PID: $pid)"
fi
rm -f "$pidfile"
fi
}
# Read instances config from file
INSTANCES_CONF="$HEXO_BASE/instances.conf"
# Main loop - keep container running
if [ -f "$INSTANCES_CONF" ]; then
echo "Loading instances from config..."
while IFS=: read -r name port; do
[ -n "$name" ] && [ -n "$port" ] && start_instance "$name" "$port"
done < "$INSTANCES_CONF"
fi
# Legacy: check for old-style single site
if [ -d "$HEXO_BASE/site" ] && [ ! -L "$HEXO_BASE/site" ]; then
echo "Starting legacy site on port ${HEXO_PORT:-4000}..."
cd "$HEXO_BASE/site"
[ -d "node_modules" ] || npm install
nohup npx hexo server -p "${HEXO_PORT:-4000}" -i 0.0.0.0 > "$LOG_DIR/default.log" 2>&1 &
echo $! > "$PIDS_DIR/default.pid"
fi
# Keep container running
echo "Hexo multi-instance manager running. Instances:"
ls -1 "$PIDS_DIR"/*.pid 2>/dev/null | while read f; do
name=$(basename "$f" .pid)
pid=$(cat "$f")
echo " - $name (PID: $pid)"
done
# Wait forever
exec tail -f /dev/null
STARTEOF
chmod +x "$start_script"
}
# Generate instances.conf for container
generate_instances_conf() {
load_config
local conf_file="$data_path/instances.conf"
> "$conf_file"
for instance in $(get_enabled_instances); do
load_instance_config "$instance" || continue
[ "$instance_enabled" = "1" ] || continue
[ -d "$instance_site" ] || continue
echo "${instance}:${instance_port}" >> "$conf_file"
done
log_debug "Generated instances.conf with $(wc -l < "$conf_file") instances"
}
# Container control
lxc_running() {
lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"
}
lxc_exists() {
[ -f "$LXC_CONFIG" ] && [ -d "$LXC_ROOTFS" ]
}
lxc_stop() {
if lxc_running; then
log_info "Stopping Hexo container..."
lxc-stop -n "$LXC_NAME" -k 2>/dev/null || true
sleep 2
fi
}
lxc_run() {
load_config
lxc_stop
if ! lxc_exists; then
log_error "Container not installed. Run: hexoctl install"
return 1
fi
# Regenerate config
lxc_create_config
create_startup_script
generate_instances_conf
log_info "Starting Hexo container..."
exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONFIG"
}
lxc_exec() {
if ! lxc_running; then
log_error "Container not running. Start with: /etc/init.d/hexojs start"
return 1
fi
lxc-attach -n "$LXC_NAME" -- env PATH=/usr/local/bin:/usr/bin:/bin "$@"
}
# Instance management commands
cmd_instance_list() {
load_config
echo "Hexo Instances:"
echo "---------------"
local found=0
for section in $(uci show hexojs 2>/dev/null | grep '=instance$' | cut -d'.' -f2 | cut -d'=' -f1); do
found=1
load_instance_config "$section"
local status="disabled"
[ "$instance_enabled" = "1" ] && status="enabled"
local site_status="no site"
[ -d "$instance_site" ] && site_status="site ready"
local running=""
if lxc_running && [ -f "$LXC_ROOTFS/var/run/hexo/${section}.pid" ]; then
running=" [RUNNING]"
fi
printf " %-15s port:%-5s %s (%s)%s\n" "$section" "$instance_port" "[$status]" "$site_status" "$running"
done
# Check for legacy default
if [ "$found" = "0" ] && [ -d "$data_path/site" ]; then
echo " default port:$http_port [legacy] (site ready)"
fi
[ "$found" = "0" ] && [ ! -d "$data_path/site" ] && echo " (no instances)"
}
cmd_instance_create() {
require_root
load_config
local name="$1"
[ -z "$name" ] && { log_error "Instance name required"; return 1; }
# Validate name
echo "$name" | grep -qE '^[a-z][a-z0-9_]*$' || {
log_error "Invalid instance name. Use lowercase letters, numbers, underscore. Start with letter."
return 1
}
# Check if exists
local existing=$(uci_get "$name")
[ -n "$existing" ] && { log_error "Instance '$name' already exists"; return 1; }
# Find next available port
local port=4000
while uci show hexojs 2>/dev/null | grep -q "port='$port'"; do
port=$((port + 1))
done
# Create UCI config
uci set hexojs.${name}=instance
uci set hexojs.${name}.enabled='1'
uci set hexojs.${name}.port="$port"
uci set hexojs.${name}.title="$name Blog"
uci set hexojs.${name}.theme='cybermind'
uci commit hexojs
# Create directory
ensure_dir "$data_path/instances/$name"
log_info "Instance '$name' created on port $port"
log_info "Next: hexoctl site create $name"
}
cmd_instance_delete() {
require_root
load_config
local name="$1"
[ -z "$name" ] && { log_error "Instance name required"; return 1; }
# Stop instance first
cmd_instance_stop "$name" 2>/dev/null
# Remove UCI config
uci delete hexojs.${name} 2>/dev/null
uci commit hexojs
# Optionally remove data (ask user)
local instance_dir="$data_path/instances/$name"
if [ -d "$instance_dir" ]; then
log_warn "Data directory exists: $instance_dir"
log_info "Remove manually if needed: rm -rf $instance_dir"
fi
log_info "Instance '$name' deleted"
}
cmd_instance_start() {
require_root
load_config
local name="$1"
[ -z "$name" ] && { log_error "Instance name required"; return 1; }
load_instance_config "$name" || { log_error "Instance '$name' not found"; return 1; }
if ! lxc_running; then
log_error "Container not running. Start with: /etc/init.d/hexojs start"
return 1
fi
if [ ! -d "$instance_site" ]; then
log_error "No site for instance '$name'. Create with: hexoctl site create $name"
return 1
fi
log_info "Starting instance '$name' on port $instance_port..."
lxc_exec sh -c "
cd /opt/hexojs/instances/$name/site || exit 1
[ -d node_modules ] || npm install
# Kill existing if running
[ -f /var/run/hexo/$name.pid ] && kill \$(cat /var/run/hexo/$name.pid) 2>/dev/null
mkdir -p /var/run/hexo /var/log/hexo
nohup npx hexo server -p $instance_port -i 0.0.0.0 > /var/log/hexo/$name.log 2>&1 &
echo \$! > /var/run/hexo/$name.pid
echo \"Started on port $instance_port (PID: \$!)\"
"
# Update instances.conf
generate_instances_conf
}
cmd_instance_stop() {
require_root
load_config
local name="$1"
[ -z "$name" ] && { log_error "Instance name required"; return 1; }
if ! lxc_running; then
return 0
fi
log_info "Stopping instance '$name'..."
lxc_exec sh -c "
if [ -f /var/run/hexo/$name.pid ]; then
kill \$(cat /var/run/hexo/$name.pid) 2>/dev/null
rm -f /var/run/hexo/$name.pid
echo 'Stopped'
else
echo 'Not running'
fi
"
}
cmd_instance_status() {
load_config
local name="$1"
[ -z "$name" ] && { log_error "Instance name required"; return 1; }
load_instance_config "$name" || { log_error "Instance '$name' not found"; return 1; }
local running="false"
local pid=""
if lxc_running; then
pid=$(lxc_exec cat /var/run/hexo/$name.pid 2>/dev/null)
if [ -n "$pid" ] && lxc_exec kill -0 "$pid" 2>/dev/null; then
running="true"
fi
fi
local site_exists="false"
[ -d "$instance_site" ] && site_exists="true"
cat << EOF
Instance: $name
--------------
Enabled: $([ "$instance_enabled" = "1" ] && echo "yes" || echo "no")
Running: $([ "$running" = "true" ] && echo "yes (PID: $pid)" || echo "no")
Port: $instance_port
Title: $instance_title
Theme: $instance_theme
Site: $([ "$site_exists" = "true" ] && echo "ready" || echo "not created")
Path: $instance_path
EOF
if [ "$running" = "true" ]; then
echo "URL: http://$(uci -q get network.lan.ipaddr || echo 'localhost'):$instance_port"
fi
}
# Commands
cmd_install() {
require_root
load_config
log_info "Installing Hexo CMS..."
lxc_check_prereqs || exit 1
if ! lxc_exists; then
lxc_create_rootfs || exit 1
fi
lxc_create_config || exit 1
create_startup_script
# Copy theme
if [ -d "$SHARE_PATH/themes/cybermind" ]; then
log_info "Installing CyberMind theme..."
ensure_dir "$data_path/themes"
cp -r "$SHARE_PATH/themes/cybermind" "$data_path/themes/"
fi
# Copy scaffolds
if [ -d "$SHARE_PATH/scaffolds" ]; then
ensure_dir "$data_path/scaffolds"
cp -r "$SHARE_PATH/scaffolds/"* "$data_path/scaffolds/"
fi
log_info "Installation complete!"
log_info ""
log_info "Next steps:"
log_info " 1. Create instance: hexoctl instance create myblog"
log_info " 2. Create site: hexoctl site create myblog"
log_info " 3. Start service: /etc/init.d/hexojs start"
}
cmd_uninstall() {
require_root
log_info "Uninstalling Hexo CMS..."
/etc/init.d/hexojs stop 2>/dev/null || true
/etc/init.d/hexojs disable 2>/dev/null || true
lxc_stop
if [ -d "$LXC_PATH/$LXC_NAME" ]; then
rm -rf "$LXC_PATH/$LXC_NAME"
log_info "Container removed"
fi
log_info "Hexo CMS uninstalled"
log_info "Data preserved in: $(uci_get main.data_path)"
}
cmd_update() {
require_root
load_config
log_info "Updating Hexo CMS..."
if ! lxc_running; then
log_error "Container not running"
return 1
fi
lxc_exec sh -c 'npm update -g hexo-cli'
# Update each instance
for instance in $(get_enabled_instances); do
load_instance_config "$instance" || continue
if [ -d "$instance_site" ]; then
log_info "Updating instance '$instance'..."
lxc_exec sh -c "cd /opt/hexojs/instances/$instance/site && npm update"
fi
done
log_info "Update complete!"
}
cmd_status() {
load_config
local enabled="$(uci_get main.enabled)"
local running="false"
lxc_running && running="true"
cat << EOF
Hexo CMS Status
===============
Enabled: $([ "$enabled" = "1" ] && echo "yes" || echo "no")
Running: $([ "$running" = "true" ] && echo "yes" || echo "no")
Data Path: $data_path
Memory: $memory_limit
Container: $LXC_NAME
Instances:
EOF
for instance in $(get_enabled_instances); do
load_instance_config "$instance" || continue
local status="stopped"
if [ "$running" = "true" ]; then
local pid=$(lxc_exec cat /var/run/hexo/$instance.pid 2>/dev/null)
[ -n "$pid" ] && status="running:$instance_port"
fi
printf " %-15s %s\n" "$instance" "[$status]"
done
}
# Site management (instance-aware)
cmd_site_create() {
require_root
load_config
local instance="${1:-default}"
load_instance_config "$instance" || {
# Auto-create instance if it doesn't exist
log_info "Creating instance '$instance'..."
cmd_instance_create "$instance"
load_instance_config "$instance"
}
log_info "Creating Hexo site for instance: $instance"
if [ -d "$instance_site" ]; then
log_error "Site already exists at $instance_site"
return 1
fi
ensure_dir "$instance_path"
# Start container if not running
local was_stopped=0
if ! lxc_running; then
was_stopped=1
lxc_create_config
create_startup_script
lxc-start -n "$LXC_NAME" -d -f "$LXC_CONFIG"
sleep 5
fi
# Create site in container
lxc_exec sh -c "cd /opt/hexojs/instances/$instance && hexo init site" || {
log_error "Failed to initialize site"
return 1
}
# Install dependencies
lxc_exec sh -c "cd /opt/hexojs/instances/$instance/site && npm install" || {
log_error "Failed to install dependencies"
return 1
}
# Install deploy plugin
lxc_exec sh -c "cd /opt/hexojs/instances/$instance/site && npm install hexo-deployer-git --save" || true
# Install theme
if [ -d "$data_path/themes/cybermind" ]; then
log_info "Installing CyberMind theme..."
cp -r "$data_path/themes/cybermind" "$instance_site/themes/"
sed -i 's/^theme:.*/theme: cybermind/' "$instance_site/_config.yml"
fi
# Copy scaffolds
if [ -d "$data_path/scaffolds" ]; then
cp -r "$data_path/scaffolds/"* "$instance_site/scaffolds/" 2>/dev/null || true
fi
# Update config
if [ -f "$instance_site/_config.yml" ]; then
sed -i "s/^title:.*/title: $instance_title/" "$instance_site/_config.yml"
sed -i "s|^url:.*|url: http://localhost:$instance_port|" "$instance_site/_config.yml"
fi
if [ "$was_stopped" = "1" ]; then
lxc_stop
fi
log_info "Site created for instance '$instance'!"
log_info "Start with: hexoctl instance start $instance"
}
cmd_site_delete() {
require_root
load_config
local instance="${1:-default}"
load_instance_config "$instance" || { log_error "Instance not found"; return 1; }
if [ ! -d "$instance_site" ]; then
log_error "No site exists for instance '$instance'"
return 1
fi
cmd_instance_stop "$instance" 2>/dev/null
rm -rf "$instance_site"
log_info "Site deleted for instance '$instance'"
}
# Content commands (instance-aware)
cmd_new_post() {
require_root
load_config
local title="$1"
local instance="${2:-default}"
[ -z "$title" ] && { log_error "Title required"; return 1; }
load_instance_config "$instance" || { log_error "Instance not found"; return 1; }
if ! lxc_running; then
log_error "Container not running"
return 1
fi
lxc_exec sh -c "cd /opt/hexojs/instances/$instance/site && hexo new post \"$title\""
}
cmd_new_page() {
require_root
load_config
local title="$1"
local instance="${2:-default}"
[ -z "$title" ] && { log_error "Title required"; return 1; }
load_instance_config "$instance" || { log_error "Instance not found"; return 1; }
if ! lxc_running; then
log_error "Container not running"
return 1
fi
lxc_exec sh -c "cd /opt/hexojs/instances/$instance/site && hexo new page \"$title\""
}
cmd_new_draft() {
require_root
load_config
local title="$1"
local instance="${2:-default}"
[ -z "$title" ] && { log_error "Title required"; return 1; }
load_instance_config "$instance" || { log_error "Instance not found"; return 1; }
if ! lxc_running; then
log_error "Container not running"
return 1
fi
lxc_exec sh -c "cd /opt/hexojs/instances/$instance/site && hexo new draft \"$title\""
}
cmd_list_posts() {
load_config
local instance="${1:-default}"
load_instance_config "$instance" || { log_error "Instance not found"; return 1; }
local posts_dir="$instance_site/source/_posts"
[ -d "$posts_dir" ] || { echo "[]"; return; }
echo "["
local first=1
for f in "$posts_dir"/*.md; do
[ -f "$f" ] || continue
local filename=$(basename "$f")
local slug="${filename%.md}"
local title=$(grep -m1 "^title:" "$f" | sed 's/^title:[[:space:]]*//' | tr -d '"' | tr -d "'")
[ "$first" = "1" ] || echo ","
first=0
echo " {\"slug\": \"$slug\", \"title\": \"$title\"}"
done
echo "]"
}
cmd_list_drafts() {
load_config
local instance="${1:-default}"
load_instance_config "$instance" || { log_error "Instance not found"; return 1; }
local drafts_dir="$instance_site/source/_drafts"
[ -d "$drafts_dir" ] || { echo "[]"; return; }
echo "["
local first=1
for f in "$drafts_dir"/*.md; do
[ -f "$f" ] || continue
local filename=$(basename "$f")
local slug="${filename%.md}"
local title=$(grep -m1 "^title:" "$f" | sed 's/^title:[[:space:]]*//' | tr -d '"' | tr -d "'")
[ "$first" = "1" ] || echo ","
first=0
echo " {\"slug\": \"$slug\", \"title\": \"$title\"}"
done
echo "]"
}
# Build commands (instance-aware)
cmd_serve() {
require_root
load_config
local instance="${1:-default}"
load_instance_config "$instance" || { log_error "Instance not found"; return 1; }
if ! lxc_running; then
log_error "Container not running"
return 1
fi
log_info "Starting preview server for '$instance' on port $instance_port..."
lxc_exec sh -c "cd /opt/hexojs/instances/$instance/site && hexo server -p $instance_port -i 0.0.0.0"
}
cmd_build() {
require_root
load_config
local instance="${1:-default}"
load_instance_config "$instance" || { log_error "Instance not found"; return 1; }
if ! lxc_running; then
log_error "Container not running"
return 1
fi
log_info "Generating static files for '$instance'..."
lxc_exec sh -c "cd /opt/hexojs/instances/$instance/site && hexo generate"
log_info "Build complete!"
}
cmd_clean() {
require_root
load_config
local instance="${1:-default}"
load_instance_config "$instance" || { log_error "Instance not found"; return 1; }
if ! lxc_running; then
log_error "Container not running"
return 1
fi
log_info "Cleaning generated files for '$instance'..."
lxc_exec sh -c "cd /opt/hexojs/instances/$instance/site && hexo clean"
}
cmd_publish() {
require_root
load_config
local instance="${1:-default}"
load_instance_config "$instance" || { log_error "Instance not found"; return 1; }
local public_dir="$instance_site/public"
local portal_path="$(uci_get ${instance}.publish_path)" || portal_path="/www/${instance}"
if ! lxc_running; then
log_error "Container not running"
return 1
fi
# Calculate web root
local web_root="${portal_path#/www}"
[ -z "$web_root" ] && web_root="/"
[ "${web_root%/}" = "$web_root" ] && web_root="$web_root/"
log_info "Setting root to: $web_root"
# Update config
sed -i "s|^root:.*|root: $web_root|" "$instance_site/_config.yml"
log_info "Regenerating..."
lxc_exec sh -c "cd /opt/hexojs/instances/$instance/site && hexo clean && hexo generate"
[ -d "$public_dir" ] || { log_error "Build failed"; return 1; }
log_info "Publishing to $portal_path..."
ensure_dir "$portal_path"
rsync -av --delete "$public_dir/" "$portal_path/"
log_info "Published $(find "$portal_path" -type f | wc -l) files"
}
cmd_logs() {
load_config
local instance="${1:-default}"
if lxc_running; then
lxc_exec cat /var/log/hexo/$instance.log 2>/dev/null || echo "No logs for '$instance'"
else
echo "Container not running"
fi
}
cmd_shell() {
require_root
if ! lxc_running; then
log_error "Container not running"
exit 1
fi
lxc-attach -n "$LXC_NAME" -- /bin/sh
}
cmd_exec() {
require_root
lxc_exec "$@"
}
cmd_service_run() {
require_root
load_config
lxc_check_prereqs || exit 1
lxc_run
}
cmd_service_stop() {
require_root
lxc_stop
}
# Gitea integration (instance-aware)
cmd_gitea_setup() {
require_root
load_config
local instance="${1:-default}"
if [ -z "$gitea_token" ]; then
log_error "Gitea token not configured"
return 1
fi
if ! lxc_running; then
log_error "Container not running"
return 1
fi
log_info "Configuring git credentials..."
local gitea_host=$(echo "$gitea_url" | sed 's|^https\?://||' | sed 's|/.*||')
lxc_exec sh -c "
export PATH=/usr/local/bin:\$PATH
git config --global user.name '$gitea_user'
git config --global user.email '${gitea_user}@localhost'
git config --global credential.helper store
rm -rf ~/.git-credentials
cat > ~/.git-credentials << CRED
https://${gitea_user}:${gitea_token}@${gitea_host}
http://${gitea_user}:${gitea_token}@${gitea_host}
CRED
chmod 600 ~/.git-credentials
"
log_info "Git credentials configured"
}
cmd_gitea_clone() {
require_root
load_config
local instance="${1:-default}"
load_instance_config "$instance" || { log_error "Instance not found"; return 1; }
if [ "$gitea_enabled" != "1" ]; then
log_error "Gitea integration not enabled"
return 1
fi
if ! lxc_running; then
log_error "Container not running"
return 1
fi
local content_path="$instance_path/content"
if [ -d "$content_path/.git" ]; then
log_info "Content repo already cloned, pulling..."
cd "$content_path" && git pull
else
log_info "Cloning content repo..."
local gitea_host=$(echo "$gitea_url" | sed 's|^https\?://||' | sed 's|/.*||')
local clone_url="http://${gitea_user}:${gitea_token}@${gitea_host}/${gitea_user}/${gitea_content_repo}.git"
ensure_dir "$(dirname "$content_path")"
rm -rf "$content_path"
git clone -b "$gitea_content_branch" "$clone_url" "$content_path" || {
log_error "Failed to clone"
return 1
}
fi
# Check if content is a full hexo site
if [ -f "$content_path/package.json" ] && [ -d "$content_path/source" ]; then
log_info "Content is a complete Hexo site, linking..."
lxc_exec sh -c "
rm -rf /opt/hexojs/instances/$instance/site
ln -sf /opt/hexojs/instances/$instance/content /opt/hexojs/instances/$instance/site
cd /opt/hexojs/instances/$instance/site && npm install
"
fi
log_info "Content cloned for instance '$instance'"
}
cmd_gitea_sync() {
require_root
load_config
local instance="${1:-default}"
load_instance_config "$instance" || { log_error "Instance not found"; return 1; }
local content_path="$instance_path/content"
[ -d "$content_path/.git" ] || { log_error "Content not cloned"; return 1; }
log_info "Pulling latest content..."
cd "$content_path" && git pull
log_info "Content synced for '$instance'"
}
# Main
case "${1:-}" in
install) shift; cmd_install "$@" ;;
uninstall) shift; cmd_uninstall "$@" ;;
update) shift; cmd_update "$@" ;;
status) shift; cmd_status "$@" ;;
instance)
shift
case "${1:-}" in
list) shift; cmd_instance_list "$@" ;;
create) shift; cmd_instance_create "$@" ;;
delete) shift; cmd_instance_delete "$@" ;;
start) shift; cmd_instance_start "$@" ;;
stop) shift; cmd_instance_stop "$@" ;;
status) shift; cmd_instance_status "$@" ;;
*) echo "Usage: hexoctl instance {list|create|delete|start|stop|status} [name]" ;;
esac
;;
site)
shift
case "${1:-}" in
create) shift; cmd_site_create "$@" ;;
delete) shift; cmd_site_delete "$@" ;;
list) shift; cmd_instance_list "$@" ;;
*) echo "Usage: hexoctl site {create|delete|list} [instance]" ;;
esac
;;
new)
shift
case "${1:-}" in
post) shift; cmd_new_post "$@" ;;
page) shift; cmd_new_page "$@" ;;
draft) shift; cmd_new_draft "$@" ;;
*) echo "Usage: hexoctl new {post|page|draft} \"Title\" [instance]" ;;
esac
;;
list)
shift
case "${1:-}" in
posts) shift; cmd_list_posts "$@" ;;
drafts) shift; cmd_list_drafts "$@" ;;
*) echo "Usage: hexoctl list {posts|drafts} [instance]" ;;
esac
;;
serve) shift; cmd_serve "$@" ;;
build|generate) shift; cmd_build "$@" ;;
clean) shift; cmd_clean "$@" ;;
publish) shift; cmd_publish "$@" ;;
logs) shift; cmd_logs "$@" ;;
shell) shift; cmd_shell "$@" ;;
exec) shift; cmd_exec "$@" ;;
service-run) shift; cmd_service_run "$@" ;;
service-stop) shift; cmd_service_stop "$@" ;;
gitea)
shift
case "${1:-}" in
setup) shift; cmd_gitea_setup "$@" ;;
clone) shift; cmd_gitea_clone "$@" ;;
sync) shift; cmd_gitea_sync "$@" ;;
*) echo "Usage: hexoctl gitea {setup|clone|sync} [instance]" ;;
esac
;;
*) usage ;;
esac