343 lines
7.9 KiB
Bash
Executable File
343 lines
7.9 KiB
Bash
Executable File
#!/bin/sh
|
|
# SecuBox Apps CLI
|
|
# Lists/installs/removes plugin manifests declared under plugins/*/manifest.json
|
|
|
|
set -eu
|
|
|
|
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
|
DEFAULT_PLUGINS_DIR="/usr/share/secubox/plugins"
|
|
if [ -n "${SECUBOX_PLUGINS_DIR:-}" ]; then
|
|
PLUGINS_DIR="$SECUBOX_PLUGINS_DIR"
|
|
elif [ -d "$SCRIPT_DIR/../plugins" ]; then
|
|
PLUGINS_DIR=$(cd "$SCRIPT_DIR/../plugins" && pwd)
|
|
else
|
|
PLUGINS_DIR="$DEFAULT_PLUGINS_DIR"
|
|
fi
|
|
PKG_MGR=""
|
|
PKG_UPDATED=0
|
|
|
|
info() { printf '[INFO] %s\n' "$*"; }
|
|
warn() { printf '[WARN] %s\n' "$*" >&2; }
|
|
err() { printf '[ERROR] %s\n' "$*" >&2; }
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
SecuBox Apps CLI
|
|
Usage: secubox-app <command> [arguments]
|
|
|
|
Commands:
|
|
list Show available app manifests
|
|
show <app-id> Display manifest details
|
|
install <app-id> Install required packages + run install action
|
|
remove <app-id> Remove packages listed in manifest
|
|
status <app-id> Show install state and run status action if defined
|
|
update <app-id> Run plugin update action or opkg upgrade
|
|
validate Verify manifest schema/metadata
|
|
|
|
Environment:
|
|
SECUBOX_PLUGINS_DIR Override manifest directory (default: /usr/share/secubox/plugins)
|
|
USAGE
|
|
}
|
|
|
|
require_tool() {
|
|
command -v "$1" >/dev/null 2>&1 || { err "Missing dependency: $1"; exit 1; }
|
|
}
|
|
|
|
require_jsonfilter() { require_tool jsonfilter; }
|
|
|
|
ensure_pkg_mgr() {
|
|
if [ -n "$PKG_MGR" ]; then
|
|
return
|
|
fi
|
|
if command -v opkg >/dev/null 2>&1; then
|
|
PKG_MGR="opkg"
|
|
elif command -v apk >/dev/null 2>&1; then
|
|
PKG_MGR="apk"
|
|
else
|
|
err "Missing dependency: require opkg or apk"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
pkg_update_once() {
|
|
ensure_pkg_mgr
|
|
[ "$PKG_UPDATED" -eq 1 ] && return
|
|
case "$PKG_MGR" in
|
|
opkg) opkg update >/dev/null ;;
|
|
apk) apk update >/dev/null ;;
|
|
esac
|
|
PKG_UPDATED=1
|
|
}
|
|
|
|
pkg_is_installed() {
|
|
ensure_pkg_mgr
|
|
case "$PKG_MGR" in
|
|
opkg) opkg status "$1" >/dev/null 2>&1 ;;
|
|
apk) apk info -e "$1" >/dev/null 2>&1 ;;
|
|
esac
|
|
}
|
|
|
|
pkg_install() {
|
|
local pkg="$1"
|
|
if pkg_is_installed "$pkg"; then
|
|
info "$pkg already installed"
|
|
return
|
|
fi
|
|
info "Installing $pkg"
|
|
pkg_update_once
|
|
case "$PKG_MGR" in
|
|
opkg) opkg install "$pkg" ;;
|
|
apk) apk add "$pkg" ;;
|
|
esac
|
|
}
|
|
|
|
pkg_remove() {
|
|
local pkg="$1"
|
|
if ! pkg_is_installed "$pkg"; then
|
|
info "$pkg not installed"
|
|
return
|
|
fi
|
|
info "Removing $pkg"
|
|
case "$PKG_MGR" in
|
|
opkg) opkg remove "$pkg" ;;
|
|
apk) apk del "$pkg" ;;
|
|
esac
|
|
}
|
|
|
|
pkg_upgrade() {
|
|
case "$PKG_MGR" in
|
|
opkg) opkg upgrade "$1" ;;
|
|
apk) apk upgrade "$1" ;;
|
|
esac
|
|
}
|
|
|
|
manifest_files() {
|
|
if [ -d "$PLUGINS_DIR/catalog" ]; then
|
|
for file in "$PLUGINS_DIR"/catalog/*.json; do
|
|
[ -f "$file" ] || continue
|
|
echo "$file"
|
|
done
|
|
fi
|
|
for file in "$PLUGINS_DIR"/*/manifest.json; do
|
|
[ -f "$file" ] || continue
|
|
echo "$file"
|
|
done
|
|
}
|
|
|
|
manifest_path() {
|
|
local id="$1"
|
|
local catalog="$PLUGINS_DIR/catalog/$id.json"
|
|
local legacy="$PLUGINS_DIR/$id/manifest.json"
|
|
if [ -f "$catalog" ]; then
|
|
printf '%s' "$catalog"
|
|
return 0
|
|
fi
|
|
if [ -f "$legacy" ]; then
|
|
printf '%s' "$legacy"
|
|
return 0
|
|
fi
|
|
err "Manifest not found for '$id'"
|
|
exit 1
|
|
}
|
|
|
|
manifest_field() {
|
|
local file="$1"; shift
|
|
cat "$file" | jsonfilter -e "$1" 2>/dev/null || true
|
|
}
|
|
|
|
manifest_packages() {
|
|
local file="$1"
|
|
cat "$file" | jsonfilter -e '@.packages[*]' 2>/dev/null || true
|
|
}
|
|
|
|
manifest_action() {
|
|
local file="$1"; shift
|
|
cat "$file" | jsonfilter -e "@.actions.$1" 2>/dev/null || true
|
|
}
|
|
|
|
manifest_runtime() {
|
|
local file="$1"
|
|
local value
|
|
value=$(manifest_field "$file" '@.type')
|
|
[ -n "$value" ] || value=$(manifest_field "$file" '@.runtime')
|
|
printf '%s' "$value"
|
|
}
|
|
|
|
validate_manifest_file() {
|
|
require_jsonfilter
|
|
local file="$1"
|
|
local id name runtime packages category
|
|
id=$(manifest_field "$file" '@.id')
|
|
name=$(manifest_field "$file" '@.name')
|
|
category=$(manifest_field "$file" '@.category')
|
|
runtime=$(manifest_runtime "$file")
|
|
packages=$(manifest_packages "$file")
|
|
if [ -z "$id" ] || [ -z "$name" ] || [ -z "$runtime" ]; then
|
|
warn "Skipping manifest $file (missing id/name/runtime)"
|
|
return 1
|
|
fi
|
|
if [ -z "$packages" ]; then
|
|
warn "Manifest $id has no packages defined"
|
|
return 1
|
|
fi
|
|
if [ -z "$category" ]; then
|
|
warn "Manifest $id missing category"
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
plugin_state() {
|
|
local file="$1"
|
|
local pkgs pkg missing=0 installed=0 total=0
|
|
pkgs=$(manifest_packages "$file")
|
|
for pkg in $pkgs; do
|
|
total=$((total+1))
|
|
if pkg_is_installed "$pkg"; then
|
|
installed=$((installed+1))
|
|
else
|
|
missing=$((missing+1))
|
|
fi
|
|
done
|
|
if [ "$total" -eq 0 ]; then
|
|
echo "n/a"
|
|
elif [ "$missing" -eq 0 ]; then
|
|
echo "installed"
|
|
elif [ "$installed" -eq 0 ]; then
|
|
echo "missing"
|
|
else
|
|
echo "partial"
|
|
fi
|
|
}
|
|
|
|
list_plugins() {
|
|
require_jsonfilter
|
|
ensure_pkg_mgr
|
|
local seen_ids=""
|
|
printf '%-18s %-24s %-10s %-10s %-10s\n' "ID" "Name" "Runtime" "Category" "State"
|
|
printf '%-18s %-24s %-10s %-10s %-10s\n' "--" "----" "-------" "--------" "-----"
|
|
while IFS= read -r file; do
|
|
[ -f "$file" ] || continue
|
|
if ! validate_manifest_file "$file"; then
|
|
continue
|
|
fi
|
|
local id name runtime state category
|
|
id=$(manifest_field "$file" '@.id')
|
|
name=$(manifest_field "$file" '@.name')
|
|
runtime=$(manifest_runtime "$file")
|
|
category=$(manifest_field "$file" '@.category')
|
|
case " $seen_ids " in
|
|
*" $id "*) continue ;;
|
|
esac
|
|
seen_ids="$seen_ids $id"
|
|
state=$(plugin_state "$file")
|
|
printf '%-18s %-24s %-10s %-10s %-10s\n' "$id" "${name:-Unknown}" "${runtime:-unknown}" "${category:-?}" "$state"
|
|
done <<EOF
|
|
$(manifest_files | sort)
|
|
EOF
|
|
}
|
|
|
|
show_manifest() {
|
|
require_jsonfilter
|
|
local file=$(manifest_path "$1")
|
|
cat "$file"
|
|
}
|
|
|
|
install_plugin() {
|
|
require_jsonfilter
|
|
ensure_pkg_mgr
|
|
local file=$(manifest_path "$1")
|
|
local pkgs pkg
|
|
pkgs=$(manifest_packages "$file")
|
|
if [ -z "$pkgs" ]; then
|
|
warn "Manifest has no packages; nothing to install."
|
|
else
|
|
for pkg in $pkgs; do
|
|
pkg_install "$pkg"
|
|
done
|
|
fi
|
|
local install_cmd
|
|
install_cmd=$(manifest_action "$file" install)
|
|
if [ -n "$install_cmd" ]; then
|
|
info "Running install action: $install_cmd"
|
|
sh -c "$install_cmd"
|
|
fi
|
|
}
|
|
|
|
remove_plugin() {
|
|
require_jsonfilter
|
|
ensure_pkg_mgr
|
|
local file=$(manifest_path "$1")
|
|
local pkgs pkg
|
|
pkgs=$(manifest_packages "$file")
|
|
for pkg in $pkgs; do
|
|
pkg_remove "$pkg"
|
|
done
|
|
}
|
|
|
|
plugin_status_cmd() {
|
|
require_jsonfilter
|
|
local file=$(manifest_path "$1")
|
|
local status_cmd
|
|
status_cmd=$(manifest_action "$file" status)
|
|
local state
|
|
state=$(plugin_state "$file")
|
|
printf 'App: %s\nState: %s\n' "$1" "$state"
|
|
if [ -n "$status_cmd" ]; then
|
|
printf 'Status command output:\n'
|
|
sh -c "$status_cmd" || true
|
|
fi
|
|
}
|
|
|
|
update_plugin() {
|
|
require_jsonfilter
|
|
ensure_pkg_mgr
|
|
local file=$(manifest_path "$1")
|
|
local update_cmd pkgs pkg
|
|
update_cmd=$(manifest_action "$file" update)
|
|
if [ -n "$update_cmd" ]; then
|
|
info "Running update action: $update_cmd"
|
|
sh -c "$update_cmd"
|
|
else
|
|
pkgs=$(manifest_packages "$file")
|
|
for pkg in $pkgs; do
|
|
info "Upgrading $pkg"
|
|
pkg_update_once
|
|
pkg_upgrade "$pkg" || true
|
|
done
|
|
fi
|
|
}
|
|
|
|
validate_manifests_cmd() {
|
|
require_jsonfilter
|
|
local failed=0 passed=0 file
|
|
while IFS= read -r file; do
|
|
[ -f "$file" ] || continue
|
|
if validate_manifest_file "$file"; then
|
|
passed=$((passed + 1))
|
|
else
|
|
failed=$((failed + 1))
|
|
fi
|
|
done <<EOF
|
|
$(manifest_files)
|
|
EOF
|
|
info "Validated $passed manifest(s)"
|
|
if [ "$failed" -gt 0 ]; then
|
|
err "$failed manifest(s) failed validation"
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
case "${1:-}" in
|
|
list) shift; list_plugins ;;
|
|
show) shift; [ $# -ge 1 ] || { err "show requires an app id"; exit 1; }; show_manifest "$1" ;;
|
|
install) shift; [ $# -ge 1 ] || { err "install requires an app id"; exit 1; }; install_plugin "$1" ;;
|
|
remove) shift; [ $# -ge 1 ] || { err "remove requires an app id"; exit 1; }; remove_plugin "$1" ;;
|
|
status) shift; [ $# -ge 1 ] || { err "status requires an app id"; exit 1; }; plugin_status_cmd "$1" ;;
|
|
update) shift; [ $# -ge 1 ] || { err "update requires an app id"; exit 1; }; update_plugin "$1" ;;
|
|
validate) shift; if ! validate_manifests_cmd; then exit 1; fi ;;
|
|
help|--help|-h|'') usage ;;
|
|
*) err "Unknown command: $1"; usage; exit 1 ;;
|
|
esac
|