secubox-openwrt/package/secubox/luci-app-hexojs/root/usr/libexec/rpcd/luci.hexojs
CyberMind-FR 0be687b89b feat(hexojs): Add Build & Publish LuCI interface for Gitea workflow
- Add publish_to_www RPCD method to publish static files to /www/blog
- Add Build & Publish card in sync.js with configurable publish path
- Add generate RPC call for building site
- Fix file permissions for all RPCD scripts and init.d scripts
- Bump luci-app-hexojs to 1.0.0-r3

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 16:18:40 +01:00

1734 lines
41 KiB
Bash
Executable File

#!/bin/sh
# SPDX-License-Identifier: MIT
# Hexo CMS RPCD backend
# Copyright (C) 2025 CyberMind.fr
. /lib/functions.sh
. /usr/share/libubox/jshn.sh
CONFIG="hexojs"
HEXOCTL="/usr/sbin/hexoctl"
DATA_PATH="/srv/hexojs"
# Helper functions
uci_get() { uci -q get ${CONFIG}.$1; }
uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; }
is_running() {
pgrep -f "lxc.*hexojs" >/dev/null 2>&1
}
get_site_path() {
local data_path=$(uci_get main.data_path) || data_path="$DATA_PATH"
echo "$data_path/site"
}
# ============================================
# Status Methods
# ============================================
get_status() {
local data_path=$(uci_get main.data_path) || data_path="$DATA_PATH"
local site_path="$data_path/site"
local enabled=$(uci_get main.enabled) || enabled="0"
local http_port=$(uci_get main.http_port) || http_port="4000"
local active_site=$(uci_get main.active_site) || active_site="default"
json_init
json_add_boolean "enabled" "$enabled"
json_add_boolean "running" "$(is_running && echo 1 || echo 0)"
json_add_int "http_port" "$http_port"
json_add_string "active_site" "$active_site"
json_add_string "data_path" "$data_path"
# Site info
if [ -d "$site_path" ]; then
json_add_boolean "site_exists" 1
local post_count=0
local draft_count=0
local page_count=0
[ -d "$site_path/source/_posts" ] && post_count=$(ls -1 "$site_path/source/_posts/"*.md 2>/dev/null | wc -l)
[ -d "$site_path/source/_drafts" ] && draft_count=$(ls -1 "$site_path/source/_drafts/"*.md 2>/dev/null | wc -l)
json_add_int "post_count" "$post_count"
json_add_int "draft_count" "$draft_count"
# Site config
local title=$(uci_get ${active_site}.title) || title="Blog"
local author=$(uci_get ${active_site}.author) || author="Admin"
local theme=$(uci_get ${active_site}.theme) || theme="cybermind"
json_add_object "site"
json_add_string "title" "$title"
json_add_string "author" "$author"
json_add_string "theme" "$theme"
json_close_object
else
json_add_boolean "site_exists" 0
json_add_int "post_count" 0
json_add_int "draft_count" 0
fi
json_dump
}
get_site_stats() {
local site_path=$(get_site_path)
json_init
if [ ! -d "$site_path" ]; then
json_add_int "posts" 0
json_add_int "drafts" 0
json_add_int "pages" 0
json_add_int "categories" 0
json_add_int "tags" 0
json_add_int "media" 0
json_dump
return
fi
local post_count=0
local draft_count=0
local page_count=0
local media_count=0
[ -d "$site_path/source/_posts" ] && post_count=$(ls -1 "$site_path/source/_posts/"*.md 2>/dev/null | wc -l)
[ -d "$site_path/source/_drafts" ] && draft_count=$(ls -1 "$site_path/source/_drafts/"*.md 2>/dev/null | wc -l)
[ -d "$site_path/source/images" ] && media_count=$(find "$site_path/source/images" -type f 2>/dev/null | wc -l)
# Count unique categories and tags from posts
local categories=""
local tags=""
if [ -d "$site_path/source/_posts" ]; then
for f in "$site_path/source/_posts/"*.md; do
[ -f "$f" ] || continue
local cat=$(grep -m1 "^categories:" "$f" 2>/dev/null | sed 's/^categories:[[:space:]]*//' | tr -d '[]' | tr ',' '\n')
local tag=$(grep -m1 "^tags:" "$f" 2>/dev/null | sed 's/^tags:[[:space:]]*//' | tr -d '[]' | tr ',' '\n')
categories="$categories
$cat"
tags="$tags
$tag"
done
fi
local cat_count=$(echo "$categories" | grep -v '^$' | sort -u | wc -l)
local tag_count=$(echo "$tags" | grep -v '^$' | sort -u | wc -l)
json_add_int "posts" "$post_count"
json_add_int "drafts" "$draft_count"
json_add_int "pages" "$page_count"
json_add_int "categories" "$cat_count"
json_add_int "tags" "$tag_count"
json_add_int "media" "$media_count"
json_dump
}
# ============================================
# Post Methods
# ============================================
list_posts() {
local site_path=$(get_site_path)
local posts_dir="$site_path/source/_posts"
json_init
json_add_array "posts"
if [ -d "$posts_dir" ]; then
for f in "$posts_dir"/*.md; do
[ -f "$f" ] || continue
local filename=$(basename "$f")
local slug="${filename%.md}"
# Parse front matter
local title=$(grep -m1 "^title:" "$f" | sed 's/^title:[[:space:]]*//' | tr -d '"' | tr -d "'")
local date=$(grep -m1 "^date:" "$f" | sed 's/^date:[[:space:]]*//')
local categories=$(grep -m1 "^categories:" "$f" | sed 's/^categories:[[:space:]]*//' | tr -d '[]')
local tags=$(grep -m1 "^tags:" "$f" | sed 's/^tags:[[:space:]]*//' | tr -d '[]')
local excerpt=$(grep -m1 "^excerpt:" "$f" | sed 's/^excerpt:[[:space:]]*//' | tr -d '"')
json_add_object
json_add_string "slug" "$slug"
json_add_string "title" "${title:-$slug}"
json_add_string "date" "$date"
json_add_string "categories" "$categories"
json_add_string "tags" "$tags"
json_add_string "excerpt" "$excerpt"
json_add_string "path" "$f"
json_close_object
done
fi
json_close_array
json_dump
}
get_post() {
read input
json_load "$input"
json_get_var slug slug
json_init
if [ -z "$slug" ]; then
json_add_boolean "success" 0
json_add_string "error" "Slug required"
json_dump
return
fi
local site_path=$(get_site_path)
local post_file="$site_path/source/_posts/${slug}.md"
if [ ! -f "$post_file" ]; then
json_add_boolean "success" 0
json_add_string "error" "Post not found"
json_dump
return
fi
# Parse front matter and content
local title=$(grep -m1 "^title:" "$post_file" | sed 's/^title:[[:space:]]*//' | tr -d '"' | tr -d "'")
local date=$(grep -m1 "^date:" "$post_file" | sed 's/^date:[[:space:]]*//')
local categories=$(grep -m1 "^categories:" "$post_file" | sed 's/^categories:[[:space:]]*//' | tr -d '[]')
local tags=$(grep -m1 "^tags:" "$post_file" | sed 's/^tags:[[:space:]]*//' | tr -d '[]')
local cover=$(grep -m1 "^cover:" "$post_file" | sed 's/^cover:[[:space:]]*//')
local excerpt=$(grep -m1 "^excerpt:" "$post_file" | sed 's/^excerpt:[[:space:]]*//' | tr -d '"')
# Get content after front matter
local content=$(awk '/^---$/,/^---$/{next} {print}' "$post_file" | tail -n +2)
json_add_boolean "success" 1
json_add_string "slug" "$slug"
json_add_string "title" "$title"
json_add_string "date" "$date"
json_add_string "categories" "$categories"
json_add_string "tags" "$tags"
json_add_string "cover" "$cover"
json_add_string "excerpt" "$excerpt"
json_add_string "content" "$content"
json_add_string "path" "$post_file"
json_dump
}
create_post() {
read input
json_load "$input"
json_get_var title title
json_get_var content content
json_get_var categories categories
json_get_var tags tags
json_get_var excerpt excerpt
json_init
if [ -z "$title" ]; then
json_add_boolean "success" 0
json_add_string "error" "Title required"
json_dump
return
fi
# Generate slug from title
local slug=$(echo "$title" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd 'a-z0-9-')
local date=$(date "+%Y-%m-%d %H:%M:%S")
local site_path=$(get_site_path)
local posts_dir="$site_path/source/_posts"
local post_file="$posts_dir/${slug}.md"
[ -d "$posts_dir" ] || mkdir -p "$posts_dir"
# Check if file exists
if [ -f "$post_file" ]; then
json_add_boolean "success" 0
json_add_string "error" "Post with this slug already exists"
json_dump
return
fi
# Create post file
cat > "$post_file" << EOF
---
title: $title
date: $date
categories: [$categories]
tags: [$tags]
excerpt: $excerpt
---
$content
EOF
json_add_boolean "success" 1
json_add_string "slug" "$slug"
json_add_string "path" "$post_file"
json_add_string "message" "Post created"
json_dump
}
update_post() {
read input
json_load "$input"
json_get_var slug slug
json_get_var title title
json_get_var content content
json_get_var categories categories
json_get_var tags tags
json_get_var excerpt excerpt
json_get_var cover cover
json_init
if [ -z "$slug" ]; then
json_add_boolean "success" 0
json_add_string "error" "Slug required"
json_dump
return
fi
local site_path=$(get_site_path)
local post_file="$site_path/source/_posts/${slug}.md"
if [ ! -f "$post_file" ]; then
json_add_boolean "success" 0
json_add_string "error" "Post not found"
json_dump
return
fi
# Get original date
local date=$(grep -m1 "^date:" "$post_file" | sed 's/^date:[[:space:]]*//')
# Rewrite post file
cat > "$post_file" << EOF
---
title: $title
date: $date
categories: [$categories]
tags: [$tags]
cover: $cover
excerpt: $excerpt
---
$content
EOF
json_add_boolean "success" 1
json_add_string "message" "Post updated"
json_dump
}
delete_post() {
read input
json_load "$input"
json_get_var slug slug
json_init
if [ -z "$slug" ]; then
json_add_boolean "success" 0
json_add_string "error" "Slug required"
json_dump
return
fi
local site_path=$(get_site_path)
local post_file="$site_path/source/_posts/${slug}.md"
if [ ! -f "$post_file" ]; then
json_add_boolean "success" 0
json_add_string "error" "Post not found"
json_dump
return
fi
rm -f "$post_file"
json_add_boolean "success" 1
json_add_string "message" "Post deleted"
json_dump
}
publish_post() {
read input
json_load "$input"
json_get_var slug slug
json_init
if [ -z "$slug" ]; then
json_add_boolean "success" 0
json_add_string "error" "Slug required"
json_dump
return
fi
local site_path=$(get_site_path)
local draft_file="$site_path/source/_drafts/${slug}.md"
local posts_dir="$site_path/source/_posts"
local post_file="$posts_dir/${slug}.md"
if [ ! -f "$draft_file" ]; then
json_add_boolean "success" 0
json_add_string "error" "Draft not found"
json_dump
return
fi
[ -d "$posts_dir" ] || mkdir -p "$posts_dir"
# Add date if not present
if ! grep -q "^date:" "$draft_file"; then
local date=$(date "+%Y-%m-%d %H:%M:%S")
sed -i "/^title:/a date: $date" "$draft_file"
fi
mv "$draft_file" "$post_file"
json_add_boolean "success" 1
json_add_string "message" "Draft published"
json_add_string "path" "$post_file"
json_dump
}
list_drafts() {
local site_path=$(get_site_path)
local drafts_dir="$site_path/source/_drafts"
json_init
json_add_array "drafts"
if [ -d "$drafts_dir" ]; then
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 "'")
json_add_object
json_add_string "slug" "$slug"
json_add_string "title" "${title:-$slug}"
json_add_string "path" "$f"
json_close_object
done
fi
json_close_array
json_dump
}
search_posts() {
read input
json_load "$input"
json_get_var query query
json_get_var category category
json_get_var tag tag
local site_path=$(get_site_path)
local posts_dir="$site_path/source/_posts"
json_init
json_add_array "posts"
if [ -d "$posts_dir" ]; then
for f in "$posts_dir"/*.md; do
[ -f "$f" ] || continue
local match=1
# Filter by query
if [ -n "$query" ]; then
grep -qi "$query" "$f" || match=0
fi
# Filter by category
if [ -n "$category" ] && [ "$match" = "1" ]; then
grep -qi "categories:.*$category" "$f" || match=0
fi
# Filter by tag
if [ -n "$tag" ] && [ "$match" = "1" ]; then
grep -qi "tags:.*$tag" "$f" || match=0
fi
if [ "$match" = "1" ]; then
local filename=$(basename "$f")
local slug="${filename%.md}"
local title=$(grep -m1 "^title:" "$f" | sed 's/^title:[[:space:]]*//' | tr -d '"' | tr -d "'")
local date=$(grep -m1 "^date:" "$f" | sed 's/^date:[[:space:]]*//')
json_add_object
json_add_string "slug" "$slug"
json_add_string "title" "${title:-$slug}"
json_add_string "date" "$date"
json_close_object
fi
done
fi
json_close_array
json_dump
}
# ============================================
# Taxonomy Methods
# ============================================
list_categories() {
local site_path=$(get_site_path)
local posts_dir="$site_path/source/_posts"
json_init
json_add_array "categories"
if [ -d "$posts_dir" ]; then
local cats=""
for f in "$posts_dir"/*.md; do
[ -f "$f" ] || continue
local cat=$(grep -m1 "^categories:" "$f" | sed 's/^categories:[[:space:]]*//' | tr -d '[]' | tr ',' '\n')
cats="$cats
$cat"
done
# Count unique categories
echo "$cats" | grep -v '^$' | sort | uniq -c | while read count name; do
name=$(echo "$name" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
[ -n "$name" ] || continue
json_add_object
json_add_string "name" "$name"
json_add_int "count" "$count"
json_close_object
done
fi
json_close_array
json_dump
}
list_tags() {
local site_path=$(get_site_path)
local posts_dir="$site_path/source/_posts"
json_init
json_add_array "tags"
if [ -d "$posts_dir" ]; then
local tags=""
for f in "$posts_dir"/*.md; do
[ -f "$f" ] || continue
local tag=$(grep -m1 "^tags:" "$f" | sed 's/^tags:[[:space:]]*//' | tr -d '[]' | tr ',' '\n')
tags="$tags
$tag"
done
# Count unique tags
echo "$tags" | grep -v '^$' | sort | uniq -c | while read count name; do
name=$(echo "$name" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
[ -n "$name" ] || continue
json_add_object
json_add_string "name" "$name"
json_add_int "count" "$count"
json_close_object
done
fi
json_close_array
json_dump
}
# ============================================
# Media Methods
# ============================================
list_media() {
local site_path=$(get_site_path)
local media_dir="$site_path/source/images"
json_init
json_add_array "media"
if [ -d "$media_dir" ]; then
find "$media_dir" -type f | while read f; do
local filename=$(basename "$f")
local size=$(stat -c %s "$f" 2>/dev/null || echo 0)
local mtime=$(stat -c %Y "$f" 2>/dev/null || echo 0)
local relpath="${f#$site_path/source}"
json_add_object
json_add_string "name" "$filename"
json_add_string "path" "$relpath"
json_add_string "full_path" "$f"
json_add_int "size" "$size"
json_add_int "mtime" "$mtime"
json_close_object
done
fi
json_close_array
json_dump
}
delete_media() {
read input
json_load "$input"
json_get_var path path
json_init
if [ -z "$path" ]; then
json_add_boolean "success" 0
json_add_string "error" "Path required"
json_dump
return
fi
local site_path=$(get_site_path)
local full_path="$site_path/source$path"
if [ ! -f "$full_path" ]; then
json_add_boolean "success" 0
json_add_string "error" "File not found"
json_dump
return
fi
rm -f "$full_path"
json_add_boolean "success" 1
json_add_string "message" "Media deleted"
json_dump
}
# ============================================
# Apps (Portfolio) Methods
# ============================================
list_apps() {
local site_path=$(get_site_path)
local apps_dir="$site_path/source/apps"
json_init
json_add_array "apps"
if [ -d "$apps_dir" ]; then
for f in "$apps_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 "'")
local icon=$(grep -m1 "^icon:" "$f" | sed 's/^icon:[[:space:]]*//')
local description=$(grep -m1 "^description:" "$f" | sed 's/^description:[[:space:]]*//' | tr -d '"')
local url=$(grep -m1 "^url:" "$f" | sed 's/^url:[[:space:]]*//')
local category=$(grep -m1 "^category:" "$f" | sed 's/^category:[[:space:]]*//')
local featured=$(grep -m1 "^featured:" "$f" | sed 's/^featured:[[:space:]]*//')
json_add_object
json_add_string "slug" "$slug"
json_add_string "title" "${title:-$slug}"
json_add_string "icon" "$icon"
json_add_string "description" "$description"
json_add_string "url" "$url"
json_add_string "category" "$category"
json_add_boolean "featured" "$([ "$featured" = "true" ] && echo 1 || echo 0)"
json_close_object
done
fi
json_close_array
json_dump
}
create_app() {
read input
json_load "$input"
json_get_var title title
json_get_var icon icon
json_get_var description description
json_get_var url url
json_get_var category category
json_get_var content content
json_init
if [ -z "$title" ]; then
json_add_boolean "success" 0
json_add_string "error" "Title required"
json_dump
return
fi
local slug=$(echo "$title" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd 'a-z0-9-')
local date=$(date "+%Y-%m-%d %H:%M:%S")
local site_path=$(get_site_path)
local apps_dir="$site_path/source/apps"
local app_file="$apps_dir/${slug}.md"
[ -d "$apps_dir" ] || mkdir -p "$apps_dir"
if [ -f "$app_file" ]; then
json_add_boolean "success" 0
json_add_string "error" "App with this slug already exists"
json_dump
return
fi
cat > "$app_file" << EOF
---
title: $title
date: $date
layout: app
icon: $icon
description: $description
url: $url
category: ${category:-tools}
featured: false
status: active
---
$content
EOF
json_add_boolean "success" 1
json_add_string "slug" "$slug"
json_add_string "message" "App created"
json_dump
}
# ============================================
# Build & Deploy Methods
# ============================================
do_generate() {
json_init
if ! is_running; then
json_add_boolean "success" 0
json_add_string "error" "Container not running"
json_dump
return
fi
local output=$("$HEXOCTL" exec sh -c "cd /opt/hexojs/site && hexo generate" 2>&1)
local result=$?
if [ "$result" -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "Site generated successfully"
else
json_add_boolean "success" 0
json_add_string "error" "$output"
fi
json_dump
}
do_clean() {
json_init
if ! is_running; then
json_add_boolean "success" 0
json_add_string "error" "Container not running"
json_dump
return
fi
local output=$("$HEXOCTL" exec sh -c "cd /opt/hexojs/site && hexo clean" 2>&1)
local result=$?
if [ "$result" -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "Site cleaned"
else
json_add_boolean "success" 0
json_add_string "error" "$output"
fi
json_dump
}
do_deploy() {
json_init
if ! is_running; then
json_add_boolean "success" 0
json_add_string "error" "Container not running"
json_dump
return
fi
local repo=$(uci_get deploy.repo)
if [ -z "$repo" ]; then
json_add_boolean "success" 0
json_add_string "error" "Deploy repository not configured"
json_dump
return
fi
# Run deploy in background
"$HEXOCTL" exec sh -c "cd /opt/hexojs/site && hexo deploy" > /tmp/hexo-deploy.log 2>&1 &
json_add_boolean "success" 1
json_add_string "message" "Deploy started"
json_add_string "log_file" "/tmp/hexo-deploy.log"
json_dump
}
get_deploy_status() {
json_init
if pgrep -f "hexo deploy" >/dev/null 2>&1; then
json_add_string "status" "running"
else
json_add_string "status" "idle"
fi
if [ -f /tmp/hexo-deploy.log ]; then
local log=$(tail -20 /tmp/hexo-deploy.log 2>/dev/null)
json_add_string "log" "$log"
fi
json_dump
}
# ============================================
# Preview Methods
# ============================================
preview_start() {
json_init
if ! is_running; then
json_add_boolean "success" 0
json_add_string "error" "Container not running. Start service first."
json_dump
return
fi
local http_port=$(uci_get main.http_port) || http_port="4000"
json_add_boolean "success" 1
json_add_string "message" "Preview server running"
json_add_int "port" "$http_port"
json_add_string "url" "http://$(uci -q get network.lan.ipaddr || echo "localhost"):$http_port"
json_dump
}
preview_status() {
json_init
local running=$(is_running && echo 1 || echo 0)
local http_port=$(uci_get main.http_port) || http_port="4000"
json_add_boolean "running" "$running"
json_add_int "port" "$http_port"
if [ "$running" = "1" ]; then
json_add_string "url" "http://$(uci -q get network.lan.ipaddr || echo "localhost"):$http_port"
fi
json_dump
}
# ============================================
# Configuration Methods
# ============================================
get_config() {
json_init
local enabled=$(uci_get main.enabled) || enabled="0"
local http_port=$(uci_get main.http_port) || http_port="4000"
local data_path=$(uci_get main.data_path) || data_path="$DATA_PATH"
local active_site=$(uci_get main.active_site) || active_site="default"
local memory_limit=$(uci_get main.memory_limit) || memory_limit="512M"
json_add_boolean "enabled" "$enabled"
json_add_int "http_port" "$http_port"
json_add_string "data_path" "$data_path"
json_add_string "active_site" "$active_site"
json_add_string "memory_limit" "$memory_limit"
# Site config
json_add_object "site"
json_add_string "title" "$(uci_get ${active_site}.title)"
json_add_string "subtitle" "$(uci_get ${active_site}.subtitle)"
json_add_string "author" "$(uci_get ${active_site}.author)"
json_add_string "language" "$(uci_get ${active_site}.language)"
json_add_string "theme" "$(uci_get ${active_site}.theme)"
json_add_string "url" "$(uci_get ${active_site}.url)"
json_close_object
# Deploy config
json_add_object "deploy"
json_add_string "type" "$(uci_get deploy.type)"
json_add_string "repo" "$(uci_get deploy.repo)"
json_add_string "branch" "$(uci_get deploy.branch)"
json_close_object
json_dump
}
save_config() {
read input
json_load "$input"
json_get_var enabled enabled
json_get_var http_port http_port
json_get_var title title
json_get_var subtitle subtitle
json_get_var author author
json_get_var language language
json_get_var url url
json_get_var deploy_repo deploy_repo
json_get_var deploy_branch deploy_branch
json_init
local active_site=$(uci_get main.active_site) || active_site="default"
[ -n "$enabled" ] && uci_set main.enabled "$enabled"
[ -n "$http_port" ] && uci_set main.http_port "$http_port"
[ -n "$title" ] && uci_set ${active_site}.title "$title"
[ -n "$subtitle" ] && uci_set ${active_site}.subtitle "$subtitle"
[ -n "$author" ] && uci_set ${active_site}.author "$author"
[ -n "$language" ] && uci_set ${active_site}.language "$language"
[ -n "$url" ] && uci_set ${active_site}.url "$url"
[ -n "$deploy_repo" ] && uci_set deploy.repo "$deploy_repo"
[ -n "$deploy_branch" ] && uci_set deploy.branch "$deploy_branch"
# Update site _config.yml
"$HEXOCTL" exec sh -c "cd /opt/hexojs && hexoctl update_site_config" 2>/dev/null || true
json_add_boolean "success" 1
json_add_string "message" "Configuration saved"
json_dump
}
get_theme_config() {
json_init
json_add_string "default_mode" "$(uci_get theme_config.default_mode)"
json_add_boolean "allow_toggle" "$(uci_get theme_config.allow_toggle)"
json_add_string "accent_color" "$(uci_get theme_config.accent_color)"
json_add_string "logo_symbol" "$(uci_get theme_config.logo_symbol)"
json_add_string "logo_text" "$(uci_get theme_config.logo_text)"
json_dump
}
save_theme_config() {
read input
json_load "$input"
json_get_var default_mode default_mode
json_get_var allow_toggle allow_toggle
json_get_var accent_color accent_color
json_get_var logo_symbol logo_symbol
json_get_var logo_text logo_text
json_init
[ -n "$default_mode" ] && uci_set theme_config.default_mode "$default_mode"
[ -n "$allow_toggle" ] && uci_set theme_config.allow_toggle "$allow_toggle"
[ -n "$accent_color" ] && uci_set theme_config.accent_color "$accent_color"
[ -n "$logo_symbol" ] && uci_set theme_config.logo_symbol "$logo_symbol"
[ -n "$logo_text" ] && uci_set theme_config.logo_text "$logo_text"
json_add_boolean "success" 1
json_add_string "message" "Theme configuration saved"
json_dump
}
list_presets() {
json_init
json_add_array "presets"
local presets_dir="/usr/share/hexojs/presets"
if [ -d "$presets_dir" ]; then
for f in "$presets_dir"/*.yml; do
[ -f "$f" ] || continue
local filename=$(basename "$f")
local id="${filename%.yml}"
local name=$(grep -m1 "^name:" "$f" | sed 's/^name:[[:space:]]*//')
local description=$(grep -m1 "^description:" "$f" | sed 's/^description:[[:space:]]*//')
local icon=$(grep -m1 "^icon:" "$f" | sed 's/^icon:[[:space:]]*//')
json_add_object
json_add_string "id" "$id"
json_add_string "name" "$name"
json_add_string "description" "$description"
json_add_string "icon" "$icon"
json_close_object
done
fi
json_close_array
json_dump
}
apply_preset() {
read input
json_load "$input"
json_get_var preset_id preset_id
json_init
if [ -z "$preset_id" ]; then
json_add_boolean "success" 0
json_add_string "error" "Preset ID required"
json_dump
return
fi
local preset_file="/usr/share/hexojs/presets/${preset_id}.yml"
if [ ! -f "$preset_file" ]; then
json_add_boolean "success" 0
json_add_string "error" "Preset not found"
json_dump
return
fi
local site_path=$(get_site_path)
local theme_config="$site_path/themes/cybermind/_config.yml"
if [ -f "$theme_config" ]; then
cp "$preset_file" "$theme_config"
fi
json_add_boolean "success" 1
json_add_string "message" "Preset applied"
json_dump
}
# ============================================
# GitHub Sync Methods
# ============================================
git_status() {
local site_path=$(get_site_path)
json_init
if [ ! -d "$site_path" ]; then
json_add_boolean "success" 0
json_add_string "error" "Site not found"
json_dump
return
fi
cd "$site_path" 2>/dev/null || {
json_add_boolean "success" 0
json_add_string "error" "Cannot access site directory"
json_dump
return
}
# Check if it's a git repo
if [ ! -d "$site_path/.git" ]; then
json_add_boolean "success" 1
json_add_boolean "is_repo" 0
json_add_string "message" "Not a git repository"
json_dump
return
fi
json_add_boolean "success" 1
json_add_boolean "is_repo" 1
# Get remote URL
local remote=$(git remote get-url origin 2>/dev/null || echo "")
json_add_string "remote" "$remote"
# Get current branch
local branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
json_add_string "branch" "$branch"
# Get status summary
local modified=$(git status --porcelain 2>/dev/null | grep -c '^.M' || echo 0)
local untracked=$(git status --porcelain 2>/dev/null | grep -c '^??' || echo 0)
local staged=$(git status --porcelain 2>/dev/null | grep -c '^M' || echo 0)
json_add_int "modified" "$modified"
json_add_int "untracked" "$untracked"
json_add_int "staged" "$staged"
# Check for unpushed commits
local ahead=$(git rev-list --count @{u}..HEAD 2>/dev/null || echo 0)
local behind=$(git rev-list --count HEAD..@{u} 2>/dev/null || echo 0)
json_add_int "ahead" "$ahead"
json_add_int "behind" "$behind"
# Last commit info
local last_commit=$(git log -1 --format="%h - %s (%cr)" 2>/dev/null || echo "No commits")
json_add_string "last_commit" "$last_commit"
# Full status output
local status_output=$(git status --short 2>/dev/null | head -20)
json_add_string "status_output" "$status_output"
json_dump
}
git_init() {
read input
json_load "$input"
json_get_var repo repo
json_get_var branch branch
json_init
local site_path=$(get_site_path)
if [ -z "$repo" ]; then
json_add_boolean "success" 0
json_add_string "error" "Repository URL required"
json_dump
return
fi
[ -z "$branch" ] && branch="main"
# Initialize git if not already a repo
if [ ! -d "$site_path/.git" ]; then
cd "$site_path" && git init >/dev/null 2>&1
fi
cd "$site_path"
# Remove existing origin if present
git remote remove origin 2>/dev/null || true
# Add remote
git remote add origin "$repo"
# Set default branch
git branch -M "$branch" 2>/dev/null || true
json_add_boolean "success" 1
json_add_string "message" "Repository initialized with remote: $repo"
json_dump
}
git_clone() {
read input
json_load "$input"
json_get_var repo repo
json_get_var branch branch
json_init
if [ -z "$repo" ]; then
json_add_boolean "success" 0
json_add_string "error" "Repository URL required"
json_dump
return
fi
[ -z "$branch" ] && branch="main"
local data_path=$(uci_get main.data_path) || data_path="$DATA_PATH"
local site_path="$data_path/site"
# Backup existing site if present
if [ -d "$site_path" ]; then
local backup="$data_path/site.backup.$(date +%Y%m%d%H%M%S)"
mv "$site_path" "$backup"
fi
# Clone repository
local output=$(git clone --branch "$branch" --single-branch "$repo" "$site_path" 2>&1)
local result=$?
if [ "$result" -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "Repository cloned successfully"
json_add_string "branch" "$branch"
else
# Restore backup on failure
[ -d "$backup" ] && mv "$backup" "$site_path"
json_add_boolean "success" 0
json_add_string "error" "Clone failed: $output"
fi
json_dump
}
git_pull() {
json_init
local site_path=$(get_site_path)
if [ ! -d "$site_path/.git" ]; then
json_add_boolean "success" 0
json_add_string "error" "Not a git repository"
json_dump
return
fi
cd "$site_path"
# Stash local changes first
git stash push -m "auto-stash-$(date +%Y%m%d%H%M%S)" 2>/dev/null
# Pull changes
local output=$(git pull --rebase 2>&1)
local result=$?
# Pop stash if it was created
git stash pop 2>/dev/null || true
if [ "$result" -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "Pull successful"
json_add_string "output" "$output"
else
json_add_boolean "success" 0
json_add_string "error" "Pull failed: $output"
fi
json_dump
}
git_push() {
read input
json_load "$input"
json_get_var message message
json_get_var force force
json_init
local site_path=$(get_site_path)
if [ ! -d "$site_path/.git" ]; then
json_add_boolean "success" 0
json_add_string "error" "Not a git repository"
json_dump
return
fi
cd "$site_path"
# Check if there's a remote
local remote=$(git remote get-url origin 2>/dev/null || echo "")
if [ -z "$remote" ]; then
json_add_boolean "success" 0
json_add_string "error" "No remote repository configured"
json_dump
return
fi
# Stage all changes
git add -A
# Commit if there are staged changes
local staged=$(git diff --cached --quiet; echo $?)
if [ "$staged" = "1" ]; then
local commit_msg="${message:-Auto-commit from SecuBox CMS $(date +%Y-%m-%d\ %H:%M:%S)}"
git commit -m "$commit_msg" 2>/dev/null
fi
# Push changes
local push_args=""
[ "$force" = "1" ] && push_args="--force"
local output=$(git push $push_args origin HEAD 2>&1)
local result=$?
if [ "$result" -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "Push successful"
json_add_string "output" "$output"
else
json_add_boolean "success" 0
json_add_string "error" "Push failed: $output"
fi
json_dump
}
git_fetch() {
json_init
local site_path=$(get_site_path)
if [ ! -d "$site_path/.git" ]; then
json_add_boolean "success" 0
json_add_string "error" "Not a git repository"
json_dump
return
fi
cd "$site_path"
local output=$(git fetch --all 2>&1)
local result=$?
if [ "$result" -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "Fetch successful"
json_add_string "output" "$output"
else
json_add_boolean "success" 0
json_add_string "error" "Fetch failed: $output"
fi
json_dump
}
git_log() {
json_init
local site_path=$(get_site_path)
if [ ! -d "$site_path/.git" ]; then
json_add_boolean "success" 0
json_add_string "error" "Not a git repository"
json_dump
return
fi
cd "$site_path"
json_add_boolean "success" 1
json_add_array "commits"
git log --oneline -20 2>/dev/null | while read line; do
local hash=$(echo "$line" | cut -d' ' -f1)
local msg=$(echo "$line" | cut -d' ' -f2-)
json_add_object
json_add_string "hash" "$hash"
json_add_string "message" "$msg"
json_close_object
done
json_close_array
json_dump
}
git_reset() {
read input
json_load "$input"
json_get_var hard hard
json_init
local site_path=$(get_site_path)
if [ ! -d "$site_path/.git" ]; then
json_add_boolean "success" 0
json_add_string "error" "Not a git repository"
json_dump
return
fi
cd "$site_path"
if [ "$hard" = "1" ]; then
local output=$(git reset --hard HEAD 2>&1)
local result=$?
if [ "$result" -eq 0 ]; then
git clean -fd 2>/dev/null
json_add_boolean "success" 1
json_add_string "message" "Hard reset complete. All local changes discarded."
else
json_add_boolean "success" 0
json_add_string "error" "Reset failed: $output"
fi
else
git reset HEAD 2>/dev/null
json_add_boolean "success" 1
json_add_string "message" "Unstaged all changes"
fi
json_dump
}
git_set_credentials() {
read input
json_load "$input"
json_get_var name name
json_get_var email email
json_init
local site_path=$(get_site_path)
if [ ! -d "$site_path/.git" ]; then
json_add_boolean "success" 0
json_add_string "error" "Not a git repository"
json_dump
return
fi
cd "$site_path"
[ -n "$name" ] && git config user.name "$name"
[ -n "$email" ] && git config user.email "$email"
json_add_boolean "success" 1
json_add_string "message" "Git credentials configured"
json_dump
}
git_get_credentials() {
json_init
local site_path=$(get_site_path)
if [ ! -d "$site_path/.git" ]; then
json_add_boolean "success" 0
json_add_string "error" "Not a git repository"
json_dump
return
fi
cd "$site_path"
local name=$(git config user.name 2>/dev/null || echo "")
local email=$(git config user.email 2>/dev/null || echo "")
json_add_boolean "success" 1
json_add_string "name" "$name"
json_add_string "email" "$email"
json_dump
}
# ============================================
# Gitea Integration Methods
# ============================================
gitea_status() {
json_init
local enabled=$(uci -q get hexojs.gitea.enabled) || enabled="0"
local gitea_url=$(uci -q get hexojs.gitea.url) || gitea_url=""
local gitea_user=$(uci -q get hexojs.gitea.user) || gitea_user=""
local content_repo=$(uci -q get hexojs.gitea.content_repo) || content_repo=""
local content_branch=$(uci -q get hexojs.gitea.content_branch) || content_branch="main"
local auto_sync=$(uci -q get hexojs.gitea.auto_sync) || auto_sync="0"
local data_path=$(uci_get main.data_path) || data_path="$DATA_PATH"
local content_path="$data_path/content"
local has_repo="false"
local last_commit=""
local branch=""
if [ -d "$content_path/.git" ]; then
has_repo="true"
cd "$content_path"
last_commit=$(git log -1 --format="%h %s" 2>/dev/null || echo "unknown")
branch=$(git branch --show-current 2>/dev/null || echo "unknown")
fi
json_add_boolean "enabled" "$enabled"
json_add_string "gitea_url" "$gitea_url"
json_add_string "gitea_user" "$gitea_user"
json_add_string "content_repo" "$content_repo"
json_add_string "content_branch" "$content_branch"
json_add_boolean "auto_sync" "$auto_sync"
json_add_boolean "has_local_repo" "$([ "$has_repo" = "true" ] && echo 1 || echo 0)"
json_add_string "local_branch" "$branch"
json_add_string "last_commit" "$last_commit"
json_dump
}
gitea_setup() {
json_init
local output=$("$HEXOCTL" gitea setup 2>&1)
local result=$?
if [ "$result" -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "Git credentials configured"
else
json_add_boolean "success" 0
json_add_string "error" "$output"
fi
json_dump
}
gitea_clone() {
json_init
local output=$("$HEXOCTL" gitea clone 2>&1)
local result=$?
if [ "$result" -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "Content cloned from Gitea"
else
json_add_boolean "success" 0
json_add_string "error" "$output"
fi
json_dump
}
gitea_sync() {
json_init
local output=$("$HEXOCTL" gitea sync 2>&1)
local result=$?
if [ "$result" -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "Content synced from Gitea"
else
json_add_boolean "success" 0
json_add_string "error" "$output"
fi
json_dump
}
gitea_save_config() {
read input
json_load "$input"
json_get_var enabled enabled
json_get_var gitea_url gitea_url
json_get_var gitea_user gitea_user
json_get_var gitea_token gitea_token
json_get_var content_repo content_repo
json_get_var content_branch content_branch
json_get_var auto_sync auto_sync
json_init
[ -n "$enabled" ] && uci set hexojs.gitea.enabled="$enabled"
[ -n "$gitea_url" ] && uci set hexojs.gitea.url="$gitea_url"
[ -n "$gitea_user" ] && uci set hexojs.gitea.user="$gitea_user"
[ -n "$gitea_token" ] && uci set hexojs.gitea.token="$gitea_token"
[ -n "$content_repo" ] && uci set hexojs.gitea.content_repo="$content_repo"
[ -n "$content_branch" ] && uci set hexojs.gitea.content_branch="$content_branch"
[ -n "$auto_sync" ] && uci set hexojs.gitea.auto_sync="$auto_sync"
uci commit hexojs
json_add_boolean "success" 1
json_add_string "message" "Gitea configuration saved"
json_dump
}
# Publish to /www (portal)
publish_to_www() {
read input
json_load "$input"
json_get_var path path
json_init
if ! is_running; then
json_add_boolean "success" 0
json_add_string "error" "Container not running"
json_dump
return
fi
# Default path is /www/blog
[ -z "$path" ] && path="/www/blog"
# Set portal path in UCI for hexoctl
uci set hexojs.portal=hexojs
uci set hexojs.portal.path="$path"
uci commit hexojs
# Run publish command
local output=$("$HEXOCTL" publish 2>&1)
local result=$?
if [ "$result" -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "Published to $path"
json_add_string "path" "$path"
else
json_add_boolean "success" 0
json_add_string "error" "$output"
fi
json_dump
}
# ============================================
# Service Control
# ============================================
service_start() {
json_init
/etc/init.d/hexojs start >/dev/null 2>&1 &
json_add_boolean "success" 1
json_add_string "message" "Service starting"
json_dump
}
service_stop() {
json_init
/etc/init.d/hexojs stop >/dev/null 2>&1
json_add_boolean "success" 1
json_add_string "message" "Service stopped"
json_dump
}
service_restart() {
json_init
/etc/init.d/hexojs restart >/dev/null 2>&1 &
json_add_boolean "success" 1
json_add_string "message" "Service restarting"
json_dump
}
# ============================================
# Main dispatcher
# ============================================
case "$1" in
list)
cat << 'EOF'
{
"status": {},
"site_stats": {},
"list_posts": {},
"get_post": {"slug": "str"},
"create_post": {"title": "str", "content": "str", "categories": "str", "tags": "str", "excerpt": "str"},
"update_post": {"slug": "str", "title": "str", "content": "str", "categories": "str", "tags": "str", "excerpt": "str", "cover": "str"},
"delete_post": {"slug": "str"},
"publish_post": {"slug": "str"},
"list_drafts": {},
"search_posts": {"query": "str", "category": "str", "tag": "str"},
"list_categories": {},
"list_tags": {},
"list_media": {},
"delete_media": {"path": "str"},
"list_apps": {},
"create_app": {"title": "str", "icon": "str", "description": "str", "url": "str", "category": "str", "content": "str"},
"generate": {},
"clean": {},
"deploy": {},
"deploy_status": {},
"preview_start": {},
"preview_status": {},
"get_config": {},
"save_config": {"enabled": "bool", "http_port": "int", "title": "str", "subtitle": "str", "author": "str", "language": "str", "url": "str", "deploy_repo": "str", "deploy_branch": "str"},
"get_theme_config": {},
"save_theme_config": {"default_mode": "str", "allow_toggle": "bool", "accent_color": "str", "logo_symbol": "str", "logo_text": "str"},
"list_presets": {},
"apply_preset": {"preset_id": "str"},
"service_start": {},
"service_stop": {},
"service_restart": {},
"git_status": {},
"git_init": {"repo": "str", "branch": "str"},
"git_clone": {"repo": "str", "branch": "str"},
"git_pull": {},
"git_push": {"message": "str", "force": "bool"},
"git_fetch": {},
"git_log": {},
"git_reset": {"hard": "bool"},
"git_set_credentials": {"name": "str", "email": "str"},
"git_get_credentials": {},
"gitea_status": {},
"gitea_setup": {},
"gitea_clone": {},
"gitea_sync": {},
"gitea_save_config": {"enabled": "bool", "gitea_url": "str", "gitea_user": "str", "gitea_token": "str", "content_repo": "str", "content_branch": "str", "auto_sync": "bool"},
"publish_to_www": {"path": "str"}
}
EOF
;;
call)
case "$2" in
status) get_status ;;
site_stats) get_site_stats ;;
list_posts) list_posts ;;
get_post) get_post ;;
create_post) create_post ;;
update_post) update_post ;;
delete_post) delete_post ;;
publish_post) publish_post ;;
list_drafts) list_drafts ;;
search_posts) search_posts ;;
list_categories) list_categories ;;
list_tags) list_tags ;;
list_media) list_media ;;
delete_media) delete_media ;;
list_apps) list_apps ;;
create_app) create_app ;;
generate) do_generate ;;
clean) do_clean ;;
deploy) do_deploy ;;
deploy_status) get_deploy_status ;;
preview_start) preview_start ;;
preview_status) preview_status ;;
get_config) get_config ;;
save_config) save_config ;;
get_theme_config) get_theme_config ;;
save_theme_config) save_theme_config ;;
list_presets) list_presets ;;
apply_preset) apply_preset ;;
service_start) service_start ;;
service_stop) service_stop ;;
service_restart) service_restart ;;
git_status) git_status ;;
git_init) git_init ;;
git_clone) git_clone ;;
git_pull) git_pull ;;
git_push) git_push ;;
git_fetch) git_fetch ;;
git_log) git_log ;;
git_reset) git_reset ;;
git_set_credentials) git_set_credentials ;;
git_get_credentials) git_get_credentials ;;
gitea_status) gitea_status ;;
gitea_setup) gitea_setup ;;
gitea_clone) gitea_clone ;;
gitea_sync) gitea_sync ;;
gitea_save_config) gitea_save_config ;;
publish_to_www) publish_to_www ;;
*) echo '{"error": "Unknown method"}' ;;
esac
;;
esac