secubox-openwrt/package/secubox/secubox-app-streamlit-forge/files/usr/sbin/slforge
CyberMind-FR d9bcf1c09b feat(streamlit-launcher): Add on-demand startup with idle shutdown
New package secubox-app-streamlit-launcher:
- Lazy loading: apps start only when accessed
- Idle shutdown: stop apps after configurable timeout (default 30min)
- Memory management: force-stop low-priority apps when memory low
- Priority system: higher priority = keep running longer
- Always-on mode for critical apps
- Procd daemon with respawn

CLI: streamlit-launcherctl
  - daemon: run background manager
  - status/list: show app states and idle times
  - start/stop: manual app control
  - priority: set app priority (1-100)
  - check/check-memory: manual checks

Updated slforge with launcher integration:
- slforge launcher status/priority/always-on commands
- Access tracking on app start
- README documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-14 07:55:47 +01:00

1047 lines
26 KiB
Bash

#!/bin/sh
# slforge - Streamlit Forge CLI
# Manage Streamlit apps: create, deploy, publish
. /lib/functions.sh
# Load config
config_load streamlit-forge
config_get GITEA_URL main gitea_url 'http://127.0.0.1:3001'
config_get GITEA_ORG main gitea_org 'streamlit-apps'
config_get APPS_DIR main apps_dir '/srv/streamlit/apps'
config_get PREVIEWS_DIR main previews_dir '/srv/streamlit/previews'
config_get TEMPLATES_DIR main templates_dir '/usr/share/streamlit-forge/templates'
config_get BASE_DOMAIN main base_domain 'apps.secubox.in'
config_get DEFAULT_PORT main default_port_start '8501'
config_get DEFAULT_MEMORY main default_memory '512M'
# Load Gitea token from gitea config if available
config_load gitea
config_get GITEA_TOKEN main api_token ''
config_get GITEA_URL main url "$GITEA_URL"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_ok() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_err() { echo -e "${RED}[ERROR]${NC} $1"; }
# Launcher integration
LAUNCHER_TRACKING_DIR="/tmp/streamlit-access"
# Track app access (for launcher idle detection)
track_access() {
local name="$1"
mkdir -p "$LAUNCHER_TRACKING_DIR"
touch "$LAUNCHER_TRACKING_DIR/$name"
}
# Check if launcher is managing this app
is_launcher_managed() {
local enabled
config_load streamlit-launcher 2>/dev/null || return 1
config_get enabled global enabled '0'
[ "$enabled" = "1" ]
}
usage() {
cat <<EOF
Streamlit Forge - App Publishing Platform
Usage: slforge <command> [options]
App Management:
create <name> [opts] Create new Streamlit app
--from-upload <zip> Create from uploaded ZIP file
--from-git <url> Create from Git repository
--from-template <tpl> Create from template (basic, dashboard, data-viewer)
list List all apps
info <app> Show app details
delete <app> Remove app and all data
Instance Control:
start <app> [--port N] Start app instance
stop <app> Stop app instance
restart <app> Restart app instance
status [app] Show instance status
logs <app> [-f] View app logs
shell <app> Open shell in app container
Configuration:
config <app> list List app config
config <app> get <key> Get config value
config <app> set <k> <v> Set config value
sync-config <app> Sync UCI <-> app config
Source Management:
edit <app> Open in Gitea web editor
pull <app> Pull latest from Gitea
push <app> Push changes to Gitea
Preview & Publishing:
preview <app> Generate preview screenshot
expose <app> [--domain] Create vhost + SSL
hide <app> Remove public access
Mesh AppStore:
publish <app> Publish to mesh catalog
unpublish <app> Remove from mesh catalog
catalog Browse mesh catalog
install <app@node> Install from mesh
Launcher Integration:
launcher status Show launcher status
launcher priority <app> <n> Set app priority (higher=keep longer)
launcher always-on <app> Mark app as always-on (never auto-stop)
Templates:
templates List available templates
Examples:
slforge create myapp --from-template dashboard
slforge start myapp --port 8510
slforge expose myapp --domain myapp.gk2.secubox.in
slforge publish myapp
EOF
}
# Get next available port
get_next_port() {
local port=$DEFAULT_PORT
while netstat -tln 2>/dev/null | grep -q ":$port " || \
grep -q "option port '$port'" /etc/config/streamlit-forge 2>/dev/null; do
port=$((port + 1))
done
echo "$port"
}
# Check if app exists
app_exists() {
local name="$1"
[ -d "$APPS_DIR/$name" ] || uci -q get streamlit-forge."$name" >/dev/null 2>&1
}
# Get app UCI section
get_app_config() {
local name="$1"
local key="$2"
uci -q get streamlit-forge."$name"."$key"
}
# Set app UCI config
set_app_config() {
local name="$1"
local key="$2"
local value="$3"
uci set streamlit-forge."$name"."$key"="$value"
uci commit streamlit-forge
}
# ============================================
# Gitea Integration Functions (Phase 2)
# ============================================
# Call Gitea API
gitea_api() {
local method="$1"
local endpoint="$2"
local data="$3"
if [ -z "$GITEA_TOKEN" ]; then
log_err "Gitea API token not configured"
return 1
fi
local url="${GITEA_URL}/api/v1${endpoint}"
if [ "$method" = "GET" ]; then
curl -s -H "Authorization: token $GITEA_TOKEN" "$url"
elif [ "$method" = "POST" ]; then
curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "$data" "$url"
elif [ "$method" = "DELETE" ]; then
curl -s -X DELETE -H "Authorization: token $GITEA_TOKEN" "$url"
fi
}
# Check if Gitea org exists, create if not
gitea_ensure_org() {
local org="$GITEA_ORG"
# Check if org exists
local result=$(gitea_api GET "/orgs/$org" 2>/dev/null)
if echo "$result" | grep -q '"id"'; then
return 0
fi
# Create org
log_info "Creating Gitea organization: $org"
gitea_api POST "/orgs" "{\"username\":\"$org\",\"description\":\"Streamlit Forge Apps\"}" >/dev/null
}
# Create Gitea repo for app
gitea_create_repo() {
local name="$1"
local app_dir="$APPS_DIR/$name"
gitea_ensure_org
# Check if repo exists
local result=$(gitea_api GET "/repos/$GITEA_ORG/$name" 2>/dev/null)
if echo "$result" | grep -q '"id"'; then
log_info "Repository already exists: $GITEA_ORG/$name"
return 0
fi
# Create repo
log_info "Creating repository: $GITEA_ORG/$name"
gitea_api POST "/orgs/$GITEA_ORG/repos" \
"{\"name\":\"$name\",\"description\":\"Streamlit app: $name\",\"private\":false,\"auto_init\":false}" >/dev/null
# Initialize git in app directory
cd "$app_dir/src"
if [ ! -d ".git" ]; then
git init -q
git add -A
git commit -q -m "Initial commit from Streamlit Forge"
fi
# Add remote
local remote_url="${GITEA_URL}/${GITEA_ORG}/${name}.git"
git remote remove origin 2>/dev/null
git remote add origin "$remote_url"
# Push
git push -u origin main 2>/dev/null || git push -u origin master 2>/dev/null
# Save repo info to UCI
set_app_config "$name" gitea_repo "$name"
set_app_config "$name" gitea_branch "$(git rev-parse --abbrev-ref HEAD)"
log_ok "Created and pushed to: $remote_url"
}
# Open Gitea editor
cmd_edit() {
local name="$1"
[ -z "$name" ] && { log_err "App name required"; return 1; }
if ! app_exists "$name"; then
log_err "App not found: $name"
return 1
fi
local gitea_repo=$(get_app_config "$name" gitea_repo)
# Create repo if doesn't exist
if [ -z "$gitea_repo" ]; then
log_info "Setting up Gitea repository..."
gitea_create_repo "$name"
gitea_repo="$name"
fi
local branch=$(get_app_config "$name" gitea_branch)
[ -z "$branch" ] && branch="main"
local edit_url="${GITEA_URL}/${GITEA_ORG}/${gitea_repo}/_edit/${branch}/app.py"
echo ""
log_ok "Edit your app at:"
echo " $edit_url"
echo ""
echo "After editing, run: slforge pull $name"
echo ""
}
# Pull from Gitea
cmd_pull() {
local name="$1"
[ -z "$name" ] && { log_err "App name required"; return 1; }
if ! app_exists "$name"; then
log_err "App not found: $name"
return 1
fi
local app_dir="$APPS_DIR/$name"
local gitea_repo=$(get_app_config "$name" gitea_repo)
if [ -z "$gitea_repo" ]; then
log_err "No Gitea repository configured for $name"
log_info "Run: slforge edit $name to set up the repository"
return 1
fi
cd "$app_dir/src"
if [ ! -d ".git" ]; then
log_err "Not a git repository. Setting up..."
git init -q
git remote add origin "${GITEA_URL}/${GITEA_ORG}/${gitea_repo}.git"
fi
log_info "Pulling latest changes..."
git fetch origin 2>/dev/null
git pull origin "$(git rev-parse --abbrev-ref HEAD)" 2>/dev/null || {
# Try with reset for forced sync
git reset --hard origin/"$(git rev-parse --abbrev-ref HEAD)" 2>/dev/null
}
# Update timestamp
set_app_config "$name" last_pull "$(date -Iseconds)"
# Check if app is running and restart
local port=$(get_app_config "$name" port)
if [ -n "$port" ] && netstat -tln 2>/dev/null | grep -q ":$port "; then
log_info "App is running, restarting to apply changes..."
cmd_restart "$name"
fi
log_ok "Pulled latest for $name"
}
# Push to Gitea
cmd_push() {
local name="$1"
shift
[ -z "$name" ] && { log_err "App name required"; return 1; }
if ! app_exists "$name"; then
log_err "App not found: $name"
return 1
fi
local message="Update from Streamlit Forge"
while [ $# -gt 0 ]; do
case "$1" in
-m|--message) message="$2"; shift 2 ;;
*) shift ;;
esac
done
local app_dir="$APPS_DIR/$name"
local gitea_repo=$(get_app_config "$name" gitea_repo)
if [ -z "$gitea_repo" ]; then
log_info "Setting up Gitea repository..."
gitea_create_repo "$name"
return 0
fi
cd "$app_dir/src"
if [ ! -d ".git" ]; then
log_err "Not a git repository"
return 1
fi
# Check for changes
if git diff --quiet && git diff --cached --quiet; then
log_info "No changes to push"
return 0
fi
log_info "Pushing changes..."
git add -A
git commit -m "$message" 2>/dev/null
git push origin "$(git rev-parse --abbrev-ref HEAD)" 2>/dev/null || {
log_err "Failed to push. Check remote access."
return 1
}
# Update timestamp
set_app_config "$name" last_push "$(date -Iseconds)"
log_ok "Pushed changes for $name"
}
# Generate preview screenshot
cmd_preview() {
local name="$1"
[ -z "$name" ] && { log_err "App name required"; return 1; }
if ! app_exists "$name"; then
log_err "App not found: $name"
return 1
fi
local port=$(get_app_config "$name" port)
# Check if running
if ! netstat -tln 2>/dev/null | grep -q ":$port "; then
log_warn "App not running. Starting temporarily..."
cmd_start "$name" --quiet
sleep 5
fi
mkdir -p "$PREVIEWS_DIR"
local preview_file="$PREVIEWS_DIR/${name}.png"
local preview_html="$PREVIEWS_DIR/${name}.html"
# Method 1: Try wkhtmltoimage if available (best quality)
if command -v wkhtmltoimage >/dev/null 2>&1; then
log_info "Generating preview with wkhtmltoimage..."
wkhtmltoimage --width 1280 --height 800 --quality 85 \
"http://127.0.0.1:$port" "$preview_file" 2>/dev/null
if [ -f "$preview_file" ]; then
log_ok "Preview saved: $preview_file"
set_app_config "$name" preview "$preview_file"
return 0
fi
fi
# Method 2: Capture HTML content for later rendering
log_info "Capturing page content..."
curl -s -m 10 "http://127.0.0.1:$port" > "$preview_html" 2>/dev/null
if [ -s "$preview_html" ]; then
log_ok "HTML preview saved: $preview_html"
set_app_config "$name" preview "$preview_html"
# Generate a simple placeholder PNG using SVG
cat > "${preview_file%.*}.svg" <<-SVGEOF
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300">
<rect width="100%" height="100%" fill="#1a1a2e"/>
<text x="50%" y="45%" text-anchor="middle" fill="#00ff88" font-size="24" font-family="monospace">📊 $name</text>
<text x="50%" y="60%" text-anchor="middle" fill="#666" font-size="14" font-family="sans-serif">Streamlit App</text>
<text x="50%" y="75%" text-anchor="middle" fill="#444" font-size="12" font-family="sans-serif">Port: $port</text>
</svg>
SVGEOF
log_info "SVG placeholder: ${preview_file%.*}.svg"
return 0
fi
log_err "Failed to generate preview"
return 1
}
# ============================================
# End Gitea Integration Functions
# ============================================
# Create app from template
cmd_create() {
local name="$1"
shift
[ -z "$name" ] && { log_err "App name required"; return 1; }
# Check if exists
app_exists "$name" && { log_err "App '$name' already exists"; return 1; }
local from_upload="" from_git="" from_template="basic"
while [ $# -gt 0 ]; do
case "$1" in
--from-upload) from_upload="$2"; shift 2 ;;
--from-git) from_git="$2"; shift 2 ;;
--from-template) from_template="$2"; shift 2 ;;
*) shift ;;
esac
done
local app_dir="$APPS_DIR/$name"
mkdir -p "$app_dir/src" "$app_dir/data"
if [ -n "$from_upload" ]; then
# Extract ZIP
log_info "Extracting from $from_upload..."
if [ -f "$from_upload" ]; then
unzip -q "$from_upload" -d "$app_dir/src" 2>/dev/null || {
log_err "Failed to extract ZIP"
rm -rf "$app_dir"
return 1
}
# Move files if extracted to subdirectory
local subdir=$(ls -1 "$app_dir/src" 2>/dev/null | head -1)
if [ -d "$app_dir/src/$subdir" ] && [ "$subdir" != "." ]; then
mv "$app_dir/src/$subdir"/* "$app_dir/src/" 2>/dev/null
rmdir "$app_dir/src/$subdir" 2>/dev/null
fi
else
log_err "File not found: $from_upload"
rm -rf "$app_dir"
return 1
fi
elif [ -n "$from_git" ]; then
# Clone from Git
log_info "Cloning from $from_git..."
git clone "$from_git" "$app_dir/src" 2>/dev/null || {
log_err "Failed to clone repository"
rm -rf "$app_dir"
return 1
}
else
# Use template
local tpl_dir="$TEMPLATES_DIR/$from_template"
if [ -d "$tpl_dir" ]; then
log_info "Creating from template: $from_template"
cp -r "$tpl_dir"/* "$app_dir/src/"
# Replace placeholders
find "$app_dir/src" -type f -name "*.py" -exec sed -i "s/APPNAME/$name/g" {} \;
find "$app_dir/src" -type f -name "*.md" -exec sed -i "s/APPNAME/$name/g" {} \;
else
# Create minimal app
log_info "Creating minimal Streamlit app"
cat > "$app_dir/src/app.py" <<-PYEOF
import streamlit as st
st.set_page_config(page_title="$name", page_icon="🚀")
st.title("$name")
st.write("Welcome to your new Streamlit app!")
st.sidebar.header("Navigation")
page = st.sidebar.radio("Go to", ["Home", "About"])
if page == "Home":
st.header("Home")
st.write("Edit src/app.py to customize this app.")
else:
st.header("About")
st.write("Created with Streamlit Forge")
PYEOF
cat > "$app_dir/src/requirements.txt" <<-REQEOF
streamlit>=1.30.0
REQEOF
fi
fi
# Create UCI config
local port=$(get_next_port)
uci set streamlit-forge."$name"=app
uci set streamlit-forge."$name".name="$name"
uci set streamlit-forge."$name".enabled='0'
uci set streamlit-forge."$name".port="$port"
uci set streamlit-forge."$name".entrypoint='app.py'
uci set streamlit-forge."$name".memory="$DEFAULT_MEMORY"
uci set streamlit-forge."$name".created="$(date +%Y-%m-%d)"
uci commit streamlit-forge
# Create Gitea repo if token available
if [ -n "$GITEA_TOKEN" ]; then
gitea_create_repo "$name" 2>/dev/null || log_warn "Gitea repo creation skipped"
fi
log_ok "Created app: $name"
log_info " Directory: $app_dir"
log_info " Port: $port"
log_info " Start with: slforge start $name"
}
# List all apps
cmd_list() {
echo ""
echo "Streamlit Apps"
echo "=============="
echo ""
local found=0
for app_dir in "$APPS_DIR"/*/; do
[ -d "$app_dir" ] || continue
local name=$(basename "$app_dir")
local enabled=$(get_app_config "$name" enabled)
local port=$(get_app_config "$name" port)
local domain=$(get_app_config "$name" domain)
local status="stopped"
# Check if running by port
if [ -n "$port" ] && netstat -tln 2>/dev/null | grep -q ":$port "; then
status="running"
fi
printf " %-20s %-8s port:%-5s %s\n" "$name" "[$status]" "${port:-?}" "${domain:-}"
found=1
done
[ "$found" = "0" ] && echo " No apps found. Create one with: slforge create <name>"
echo ""
}
# Show app info
cmd_info() {
local name="$1"
[ -z "$name" ] && { log_err "App name required"; return 1; }
if ! app_exists "$name"; then
log_err "App not found: $name"
return 1
fi
local app_dir="$APPS_DIR/$name"
echo ""
echo "App: $name"
echo "========================================="
echo ""
echo "Directory: $app_dir"
echo "Enabled: $(get_app_config "$name" enabled)"
echo "Port: $(get_app_config "$name" port)"
echo "Memory: $(get_app_config "$name" memory)"
echo "Domain: $(get_app_config "$name" domain)"
echo "Entrypoint: $(get_app_config "$name" entrypoint)"
echo "Created: $(get_app_config "$name" created)"
echo ""
# Check status by port
local port=$(get_app_config "$name" port)
if [ -n "$port" ] && netstat -tln 2>/dev/null | grep -q ":$port "; then
echo "Status: RUNNING on port $port"
else
echo "Status: STOPPED"
fi
# Show files
if [ -d "$app_dir/src" ]; then
echo ""
echo "Source Files:"
ls -la "$app_dir/src" 2>/dev/null | head -10
fi
echo ""
}
# Start app instance
cmd_start() {
local name="$1"
shift
[ -z "$name" ] && { log_err "App name required"; return 1; }
if ! app_exists "$name"; then
log_err "App not found: $name"
return 1
fi
local port=$(get_app_config "$name" port)
local quiet=0
while [ $# -gt 0 ]; do
case "$1" in
--port) port="$2"; shift 2 ;;
--quiet) quiet=1; shift ;;
*) shift ;;
esac
done
local app_dir="$APPS_DIR/$name"
local entrypoint=$(get_app_config "$name" entrypoint)
[ -z "$entrypoint" ] && entrypoint="app.py"
# Check if already running
if [ -f "/var/run/streamlit-$name.pid" ]; then
local pid=$(cat "/var/run/streamlit-$name.pid")
if kill -0 "$pid" 2>/dev/null; then
[ "$quiet" = "0" ] && log_warn "App $name already running (PID: $pid)"
return 0
fi
fi
[ "$quiet" = "0" ] && log_info "Starting $name on port $port..."
# Check for Streamlit in LXC container
if lxc-info -n streamlit 2>/dev/null | grep -q "RUNNING"; then
# Run in Streamlit LXC container
# Note: /srv/streamlit/apps is mounted at /srv/apps inside container
# Create wrapper script to avoid shell escaping issues
cat > "$APPS_DIR/.slforge-start.sh" <<-'STARTSCRIPT'
#!/bin/sh
cd "$1"
[ -f requirements.txt ] && pip3 install -q -r requirements.txt 2>/dev/null
nohup streamlit run "$2" --server.port "$3" --server.address 0.0.0.0 --server.headless true > /tmp/streamlit-$4.log 2>&1 &
echo $!
STARTSCRIPT
chmod +x "$APPS_DIR/.slforge-start.sh"
lxc-attach -n streamlit -- /srv/apps/.slforge-start.sh \
"/srv/apps/$name/src" "$entrypoint" "$port" "$name" \
> "/var/run/streamlit-$name.pid"
else
# Run directly on host
cd "$app_dir/src"
if [ -f requirements.txt ]; then
pip3 install -q -r requirements.txt 2>/dev/null
fi
nohup streamlit run "$entrypoint" \
--server.port "$port" \
--server.address 0.0.0.0 \
--server.headless true \
> "/var/log/streamlit-$name.log" 2>&1 &
echo $! > "/var/run/streamlit-$name.pid"
fi
# Update UCI
set_app_config "$name" enabled '1'
set_app_config "$name" port "$port"
# Wait for port to become available
sleep 3
if netstat -tln 2>/dev/null | grep -q ":$port "; then
[ "$quiet" = "0" ] && log_ok "Started $name"
[ "$quiet" = "0" ] && log_info "URL: http://192.168.255.1:$port"
# Track access for launcher idle detection
track_access "$name"
return 0
fi
[ "$quiet" = "0" ] && log_err "Failed to start $name"
return 1
}
# Stop app instance
cmd_stop() {
local name="$1"
[ -z "$name" ] && { log_err "App name required"; return 1; }
if [ -f "/var/run/streamlit-$name.pid" ]; then
local pid=$(cat "/var/run/streamlit-$name.pid")
if kill -0 "$pid" 2>/dev/null; then
log_info "Stopping $name (PID: $pid)..."
kill "$pid" 2>/dev/null
sleep 1
kill -9 "$pid" 2>/dev/null
fi
rm -f "/var/run/streamlit-$name.pid"
fi
set_app_config "$name" enabled '0'
log_ok "Stopped $name"
}
# Restart app
cmd_restart() {
local name="$1"
cmd_stop "$name"
sleep 1
cmd_start "$name"
}
# Show status
cmd_status() {
local name="$1"
if [ -n "$name" ]; then
cmd_info "$name"
else
cmd_list
fi
}
# View logs
cmd_logs() {
local name="$1"
local follow=0
[ -z "$name" ] && { log_err "App name required"; return 1; }
shift
[ "$1" = "-f" ] && follow=1
local logfile="/var/log/streamlit-$name.log"
if [ ! -f "$logfile" ]; then
log_err "No logs found for $name"
return 1
fi
if [ "$follow" = "1" ]; then
tail -f "$logfile"
else
tail -100 "$logfile"
fi
}
# Delete app
cmd_delete() {
local name="$1"
[ -z "$name" ] && { log_err "App name required"; return 1; }
if ! app_exists "$name"; then
log_err "App not found: $name"
return 1
fi
# Stop if running
cmd_stop "$name" 2>/dev/null
# Remove files
rm -rf "$APPS_DIR/$name"
rm -rf "$PREVIEWS_DIR/$name"
rm -f "/var/log/streamlit-$name.log"
# Remove UCI config
uci delete streamlit-forge."$name" 2>/dev/null
uci commit streamlit-forge
log_ok "Deleted app: $name"
}
# Expose app with vhost + SSL
cmd_expose() {
local name="$1"
shift
[ -z "$name" ] && { log_err "App name required"; return 1; }
if ! app_exists "$name"; then
log_err "App not found: $name"
return 1
fi
local domain=""
while [ $# -gt 0 ]; do
case "$1" in
--domain) domain="$2"; shift 2 ;;
*) shift ;;
esac
done
# Default domain
[ -z "$domain" ] && domain="$name.$BASE_DOMAIN"
local port=$(get_app_config "$name" port)
log_info "Exposing $name at $domain..."
# Create HAProxy vhost
if command -v haproxyctl >/dev/null 2>&1; then
haproxyctl vhost add "$domain" 2>/dev/null
# Add mitmproxy route
local routes_file="/srv/mitmproxy/haproxy-routes.json"
if [ -f "$routes_file" ]; then
# Add route using jsonfilter + sed
local tmp=$(mktemp)
cat "$routes_file" | sed "s/}$/,\"$domain\":[\"192.168.255.1\",$port]}/" > "$tmp"
mv "$tmp" "$routes_file"
fi
log_info "Requesting SSL certificate..."
haproxyctl ssl request "$domain" 2>/dev/null
else
log_warn "haproxyctl not available - manual vhost setup required"
fi
# Update UCI
set_app_config "$name" domain "$domain"
log_ok "Exposed at: https://$domain"
}
# Hide app (remove public access)
cmd_hide() {
local name="$1"
[ -z "$name" ] && { log_err "App name required"; return 1; }
local domain=$(get_app_config "$name" domain)
if [ -n "$domain" ] && command -v haproxyctl >/dev/null 2>&1; then
log_info "Removing vhost: $domain"
haproxyctl vhost remove "$domain" 2>/dev/null
fi
set_app_config "$name" domain ""
log_ok "Hidden app: $name"
}
# List templates
cmd_templates() {
echo ""
echo "Available Templates"
echo "==================="
echo ""
for tpl in "$TEMPLATES_DIR"/*/; do
[ -d "$tpl" ] || continue
local name=$(basename "$tpl")
local desc=""
[ -f "$tpl/README.md" ] && desc=$(head -1 "$tpl/README.md" | sed 's/^#\s*//')
printf " %-15s %s\n" "$name" "$desc"
done
echo ""
echo "Usage: slforge create myapp --from-template <template>"
echo ""
}
# Config management
cmd_config() {
local name="$1"
local action="$2"
local key="$3"
local value="$4"
[ -z "$name" ] && { log_err "App name required"; return 1; }
case "$action" in
list)
echo ""
echo "Config for: $name"
echo "================="
uci show streamlit-forge."$name" 2>/dev/null | sed "s/streamlit-forge.$name./ /"
echo ""
;;
get)
[ -z "$key" ] && { log_err "Key required"; return 1; }
get_app_config "$name" "$key"
;;
set)
[ -z "$key" ] && { log_err "Key required"; return 1; }
[ -z "$value" ] && { log_err "Value required"; return 1; }
set_app_config "$name" "$key" "$value"
log_ok "Set $name.$key = $value"
;;
*)
log_err "Unknown config action: $action"
return 1
;;
esac
}
# Publish to mesh catalog
cmd_publish() {
local name="$1"
[ -z "$name" ] && { log_err "App name required"; return 1; }
if ! app_exists "$name"; then
log_err "App not found: $name"
return 1
fi
log_info "Publishing $name to mesh catalog..."
# Generate manifest
local manifest_dir="/usr/share/secubox/plugins/catalog"
mkdir -p "$manifest_dir"
local port=$(get_app_config "$name" port)
local domain=$(get_app_config "$name" domain)
cat > "$manifest_dir/streamlit-$name.json" <<-MANIFEST
{
"id": "streamlit-$name",
"name": "$name",
"type": "streamlit-app",
"version": "1.0.0",
"category": "apps",
"runtime": "streamlit",
"maturity": "community",
"description": "Streamlit app: $name",
"packages": [],
"capabilities": ["streamlit", "web-ui"],
"requirements": {
"arch": ["aarch64", "x86_64"],
"min_ram_mb": 256
},
"network": {
"inbound_ports": [$port],
"protocols": ["http"]
},
"actions": {
"start": "slforge start $name",
"stop": "slforge stop $name",
"status": "slforge status $name"
}
}
MANIFEST
set_app_config "$name" published_mesh '1'
log_ok "Published $name to mesh catalog"
}
# Unpublish from mesh
cmd_unpublish() {
local name="$1"
[ -z "$name" ] && { log_err "App name required"; return 1; }
rm -f "/usr/share/secubox/plugins/catalog/streamlit-$name.json"
set_app_config "$name" published_mesh '0'
log_ok "Unpublished $name from mesh catalog"
}
# Launcher integration commands
cmd_launcher() {
local action="$1"
shift
case "$action" in
status)
if command -v streamlit-launcherctl >/dev/null 2>&1; then
streamlit-launcherctl status
else
log_warn "Launcher not installed"
log_info "Install with: opkg install secubox-app-streamlit-launcher"
fi
;;
priority)
local app="$1"
local priority="$2"
[ -z "$app" ] || [ -z "$priority" ] && {
log_err "Usage: slforge launcher priority <app> <priority>"
return 1
}
if command -v streamlit-launcherctl >/dev/null 2>&1; then
streamlit-launcherctl priority "$app" "$priority"
else
log_err "Launcher not installed"
return 1
fi
;;
always-on)
local app="$1"
[ -z "$app" ] && {
log_err "Usage: slforge launcher always-on <app>"
return 1
}
if command -v streamlit-launcherctl >/dev/null 2>&1; then
streamlit-launcherctl priority "$app" 100 1
log_ok "App $app marked as always-on"
else
log_err "Launcher not installed"
return 1
fi
;;
*)
log_err "Unknown launcher action: $action"
echo "Usage: slforge launcher <status|priority|always-on>"
return 1
;;
esac
}
# Main command router
case "$1" in
create) shift; cmd_create "$@" ;;
list) cmd_list ;;
info) shift; cmd_info "$@" ;;
delete) shift; cmd_delete "$@" ;;
start) shift; cmd_start "$@" ;;
stop) shift; cmd_stop "$@" ;;
restart) shift; cmd_restart "$@" ;;
status) shift; cmd_status "$@" ;;
logs) shift; cmd_logs "$@" ;;
config) shift; cmd_config "$@" ;;
edit) shift; cmd_edit "$@" ;;
pull) shift; cmd_pull "$@" ;;
push) shift; cmd_push "$@" ;;
preview) shift; cmd_preview "$@" ;;
expose) shift; cmd_expose "$@" ;;
hide) shift; cmd_hide "$@" ;;
publish) shift; cmd_publish "$@" ;;
unpublish) shift; cmd_unpublish "$@" ;;
templates) cmd_templates ;;
launcher) shift; cmd_launcher "$@" ;;
help|--help|-h|"")
usage ;;
*)
log_err "Unknown command: $1"
usage
exit 1 ;;
esac