secubox-openwrt/package/secubox/secubox-app-magicmirror2/files/usr/sbin/mm2ctl
CyberMind-FR 5d3222e26e fix(magicmirror2): Use MMPM for module installation with proper registry
- install_module now uses mmpmctl if available (has module registry)
- Fallback to manual git clone only with explicit URLs
- Add proper Node.js PATH for npm commands
- update_module also uses mmpmctl when available
- Fix npm PATH in both install and update functions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 06:45:17 +01:00

973 lines
25 KiB
Bash

#!/bin/sh
# SecuBox MagicMirror2 manager - LXC container support with module management
# Copyright (C) 2024-2026 CyberMind.fr
CONFIG="magicmirror2"
LXC_NAME="magicmirror2"
OPKG_UPDATED=0
# Paths
LXC_PATH="/srv/lxc"
LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs"
LXC_CONFIG="$LXC_PATH/$LXC_NAME/config"
# MagicMirror paths inside container
MM_PATH="/opt/magic_mirror"
MM_MODULES="$MM_PATH/modules"
MM_CONFIG="$MM_PATH/config"
# Third-party modules registry
MM_REGISTRY_URL="https://raw.githubusercontent.com/MagicMirrorOrg/MagicMirror-3rd-Party-Modules/master/modules.json"
usage() {
cat <<'EOF'
Usage: mm2ctl <command> [options]
Commands:
install Install prerequisites and create LXC container
update Update MagicMirror2 in container
status Show container and service status
logs Show MagicMirror2 logs (use -f to follow)
shell Open shell in container
config Generate/update config.js from UCI
service-run Internal: run container under procd
service-stop Stop container
Module Management:
module list List installed modules
module available [search] List available third-party modules
module install <name|url> Install a module (MMM-name or git URL)
module remove <name> Remove an installed module
module update [name] Update module(s)
Configuration:
set <key> <value> Set UCI configuration value
get <key> Get UCI configuration value
Examples:
mm2ctl install
mm2ctl module install MMM-WeatherChart
mm2ctl module install https://github.com/user/MMM-Custom
mm2ctl module list
mm2ctl config
Web Interface: http://<router-ip>:8082
EOF
}
require_root() { [ "$(id -u)" -eq 0 ] || { echo "Root required" >&2; exit 1; }; }
log_info() { echo "[INFO] $*"; }
log_warn() { echo "[WARN] $*" >&2; }
log_error() { echo "[ERROR] $*" >&2; }
uci_get() { uci -q get ${CONFIG}.$1; }
uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; }
# Load configuration with defaults
load_config() {
port="$(uci_get main.port || echo 8082)"
address="$(uci_get main.address || echo 0.0.0.0)"
data_path="$(uci_get main.data_path || echo /srv/magicmirror2)"
memory_limit="$(uci_get main.memory_limit || echo 512M)"
language="$(uci_get main.language || echo en)"
timezone="$(uci_get main.timezone || echo Europe/Paris)"
units="$(uci_get main.units || echo metric)"
electron_enabled="$(uci_get main.electron_enabled || echo 0)"
# Display settings
display_width="$(uci_get display.width || echo 1920)"
display_height="$(uci_get display.height || echo 1080)"
display_zoom="$(uci_get display.zoom || echo 1.0)"
# Weather settings
weather_enabled="$(uci_get weather.enabled || echo 0)"
weather_provider="$(uci_get weather.provider || echo openweathermap)"
weather_api_key="$(uci_get weather.api_key || echo '')"
weather_location="$(uci_get weather.location || echo '')"
weather_location_id="$(uci_get weather.location_id || echo '')"
# Clock settings
clock_enabled="$(uci_get clock.enabled || echo 1)"
clock_display_seconds="$(uci_get clock.display_seconds || echo 1)"
clock_show_date="$(uci_get clock.show_date || echo 1)"
# Calendar settings
calendar_enabled="$(uci_get calendar.enabled || echo 0)"
calendar_max_entries="$(uci_get calendar.max_entries || echo 10)"
# Newsfeed settings
newsfeed_enabled="$(uci_get newsfeed.enabled || echo 0)"
newsfeed_max_items="$(uci_get newsfeed.max_news_items || echo 5)"
# Compliments settings
compliments_enabled="$(uci_get compliments.enabled || echo 1)"
}
ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; }
has_lxc() {
command -v lxc-start >/dev/null 2>&1 && \
command -v lxc-stop >/dev/null 2>&1
}
# Ensure required packages are installed
ensure_packages() {
require_root
for pkg in "$@"; do
if ! opkg list-installed | grep -q "^$pkg "; then
if [ "$OPKG_UPDATED" -eq 0 ]; then
opkg update || return 1
OPKG_UPDATED=1
fi
opkg install "$pkg" || return 1
fi
done
}
# =============================================================================
# LXC CONTAINER FUNCTIONS
# =============================================================================
lxc_check_prereqs() {
log_info "Checking LXC prerequisites..."
ensure_packages lxc lxc-common lxc-attach lxc-start lxc-stop lxc-destroy || return 1
if [ ! -d /sys/fs/cgroup ]; then
log_error "cgroups not mounted at /sys/fs/cgroup"
return 1
fi
log_info "LXC ready"
}
lxc_create_rootfs() {
load_config
if [ -d "$LXC_ROOTFS" ] && [ -d "$LXC_ROOTFS/opt/magic_mirror" ]; then
log_info "LXC rootfs already exists with MagicMirror2"
return 0
fi
log_info "Creating LXC rootfs for MagicMirror2..."
ensure_dir "$LXC_PATH/$LXC_NAME"
lxc_create_docker_rootfs || return 1
lxc_create_config || return 1
log_info "LXC rootfs created successfully"
}
lxc_create_docker_rootfs() {
local rootfs="$LXC_ROOTFS"
local image="karsten13/magicmirror"
local tag="latest"
local registry="registry-1.docker.io"
local arch
local tmp_layer="/tmp/mm2_layer.tar"
# Detect architecture for Docker manifest
case "$(uname -m)" in
x86_64) arch="amd64" ;;
aarch64) arch="arm64" ;;
armv7l) arch="arm" ;;
*) arch="amd64" ;;
esac
log_info "Extracting MagicMirror2 Docker image ($arch)..."
ensure_dir "$rootfs"
# Get Docker Hub token
local token=$(wget -q -O - "https://auth.docker.io/token?service=registry.docker.io&scope=repository:$image:pull" | jsonfilter -e '@.token')
[ -z "$token" ] && { log_error "Failed to get Docker Hub token"; return 1; }
# Get manifest list
local manifest=$(wget -q -O - --header="Authorization: Bearer $token" \
--header="Accept: application/vnd.docker.distribution.manifest.list.v2+json" \
"https://$registry/v2/$image/manifests/$tag")
# Find digest for our architecture
local digest=$(echo "$manifest" | jsonfilter -e "@.manifests[@.platform.architecture='$arch'].digest")
[ -z "$digest" ] && { log_error "No manifest found for $arch"; return 1; }
# Get image manifest
local img_manifest=$(wget -q -O - --header="Authorization: Bearer $token" \
--header="Accept: application/vnd.docker.distribution.manifest.v2+json" \
"https://$registry/v2/$image/manifests/$digest")
# Extract layers and download them
log_info "Downloading and extracting layers..."
local layers=$(echo "$img_manifest" | jsonfilter -e '@.layers[*].digest')
for layer_digest in $layers; do
log_info " Layer: ${layer_digest:7:12}..."
# Download layer to temp file
wget -q -O "$tmp_layer" --header="Authorization: Bearer $token" \
"https://$registry/v2/$image/blobs/$layer_digest" || {
log_warn " Failed to download layer"
continue
}
# Try decompression methods in order (gzip most common, then zstd, then plain tar)
# Method 1: Try gzip
if gunzip -t "$tmp_layer" 2>/dev/null; then
gunzip -c "$tmp_layer" | tar xf - -C "$rootfs" 2>/dev/null || true
# Method 2: Try zstd
elif command -v zstd >/dev/null 2>&1 && zstd -t "$tmp_layer" 2>/dev/null; then
zstd -d -c "$tmp_layer" | tar xf - -C "$rootfs" 2>/dev/null || true
# Method 3: Try plain tar
elif tar tf "$tmp_layer" >/dev/null 2>&1; then
tar xf "$tmp_layer" -C "$rootfs" 2>/dev/null || true
else
# Last resort: try zstd even if test failed (might need to install it)
if ! command -v zstd >/dev/null 2>&1; then
log_warn " Installing zstd for compressed layers..."
opkg update >/dev/null 2>&1 && opkg install zstd >/dev/null 2>&1
fi
if command -v zstd >/dev/null 2>&1; then
zstd -d -c "$tmp_layer" 2>/dev/null | tar xf - -C "$rootfs" 2>/dev/null || \
gunzip -c "$tmp_layer" 2>/dev/null | tar xf - -C "$rootfs" 2>/dev/null || \
tar xf "$tmp_layer" -C "$rootfs" 2>/dev/null || true
fi
fi
done
rm -f "$tmp_layer"
# Configure container
echo "nameserver 8.8.8.8" > "$rootfs/etc/resolv.conf"
mkdir -p "$rootfs/opt/magic_mirror/config" "$rootfs/opt/magic_mirror/modules" "$rootfs/tmp"
# Ensure proper shell setup
log_info "Checking shell availability..."
if [ ! -x "$rootfs/bin/sh" ]; then
if [ -x "$rootfs/bin/bash" ]; then
ln -sf bash "$rootfs/bin/sh"
elif [ -x "$rootfs/bin/dash" ]; then
ln -sf dash "$rootfs/bin/sh"
fi
fi
# Create startup script (mimics Docker entrypoint.sh)
# Use printf for shebang to avoid shell escaping issues
printf '%s\n' '#!/bin/sh' > "$rootfs/opt/start-mm2.sh"
cat >> "$rootfs/opt/start-mm2.sh" << 'START'
export PATH="/usr/local/bin:/usr/bin:/bin:$PATH"
export NODE_ENV=production
export MM_PORT="${MM2_PORT:-8082}"
export MM_ADDRESS="${MM2_ADDRESS:-0.0.0.0}"
MM_DIR="/opt/magic_mirror"
modules_dir="${MM_DIR}/modules"
default_dir="${modules_dir}/default"
config_dir="${MM_DIR}/config"
css_dir="${MM_DIR}/css"
cd "$MM_DIR"
# Setup default modules symlink (like Docker entrypoint)
if [ -d "${MM_DIR}/__modules/default" ]; then
mkdir -p "${modules_dir}"
if [ ! -e "${default_dir}" ]; then
echo "Symlinking default modules..."
ln -sf "${MM_DIR}/__modules/default" "${modules_dir}/default"
fi
fi
# Setup CSS files
if [ -d "${MM_DIR}/__css" ] && [ ! -f "${css_dir}/main.css" ]; then
mkdir -p "${css_dir}"
echo "Copying CSS files..."
cp ${MM_DIR}/__css/* "${css_dir}/" 2>/dev/null || true
fi
# Ensure custom.css exists
[ ! -f "${css_dir}/custom.css" ] && touch "${css_dir}/custom.css"
# Wait for config to be available
for i in 1 2 3 4 5; do
[ -f "${config_dir}/config.js" ] && break
echo "Waiting for config.js..."
sleep 2
done
if [ ! -f "${config_dir}/config.js" ]; then
echo "ERROR: config.js not found, using default"
if [ -f "${MM_DIR}/__config/config.js.sample" ]; then
mkdir -p "${config_dir}"
cp "${MM_DIR}/__config/config.js.sample" "${config_dir}/config.js"
fi
fi
echo "Starting MagicMirror2 on port $MM_PORT..."
# Run MagicMirror in server-only mode
exec npm run server
START
chmod +x "$rootfs/opt/start-mm2.sh"
log_info "MagicMirror2 Docker image extracted successfully"
}
lxc_create_config() {
load_config
cat > "$LXC_CONFIG" << EOF
# MagicMirror2 LXC Configuration
lxc.uts.name = $LXC_NAME
# Root filesystem
lxc.rootfs.path = dir:$LXC_ROOTFS
# Network - use host network for simplicity
lxc.net.0.type = none
# Mounts
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
lxc.mount.entry = $data_path/config opt/magic_mirror/config none bind,create=dir 0 0
lxc.mount.entry = $data_path/modules opt/magic_mirror/modules none bind,create=dir 0 0
lxc.mount.entry = $data_path/css opt/magic_mirror/css/custom none bind,create=dir 0 0
# Environment variables
lxc.environment = MM2_PORT=$port
lxc.environment = MM2_ADDRESS=$address
lxc.environment = TZ=$timezone
lxc.environment = NODE_ENV=production
# Capabilities
lxc.cap.drop = sys_admin sys_module mac_admin mac_override
# cgroups limits
lxc.cgroup.memory.limit_in_bytes = $memory_limit
# Init
lxc.init.cmd = /opt/start-mm2.sh
# Console
lxc.console.size = 1024
lxc.pty.max = 1024
EOF
log_info "LXC config created at $LXC_CONFIG"
}
lxc_stop() {
if lxc-info -n "$LXC_NAME" >/dev/null 2>&1; then
lxc-stop -n "$LXC_NAME" -k >/dev/null 2>&1 || true
fi
}
lxc_run() {
load_config
lxc_stop
if [ ! -f "$LXC_CONFIG" ]; then
log_error "LXC not configured. Run 'mm2ctl install' first."
return 1
fi
# Regenerate config to pick up UCI changes
lxc_create_config
# Ensure mount points exist
ensure_dir "$data_path/config"
ensure_dir "$data_path/modules"
ensure_dir "$data_path/css"
# Generate MagicMirror config.js from UCI
generate_mm_config
log_info "Starting MagicMirror2 LXC container..."
log_info "Web interface: http://0.0.0.0:$port"
exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONFIG"
}
lxc_status() {
load_config
echo "=== MagicMirror2 Status ==="
echo ""
if lxc-info -n "$LXC_NAME" >/dev/null 2>&1; then
lxc-info -n "$LXC_NAME"
else
echo "LXC container '$LXC_NAME' not found or not configured"
fi
echo ""
echo "=== Configuration ==="
echo "Port: $port"
echo "Address: $address"
echo "Data path: $data_path"
echo "Language: $language"
echo "Timezone: $timezone"
echo ""
echo "=== Installed Modules ==="
list_installed_modules
}
lxc_logs() {
if lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"; then
if [ "$1" = "-f" ]; then
logread -f -e magicmirror2
else
logread -e magicmirror2 | tail -100
fi
else
log_warn "Container not running. Try: logread -e magicmirror2"
fi
}
lxc_shell() {
lxc-attach -n "$LXC_NAME" -- /bin/sh
}
lxc_destroy() {
lxc_stop
if [ -d "$LXC_PATH/$LXC_NAME" ]; then
rm -rf "$LXC_PATH/$LXC_NAME"
log_info "LXC container destroyed"
fi
}
# =============================================================================
# MAGICMIRROR CONFIGURATION
# =============================================================================
generate_mm_config() {
load_config
local config_file="$data_path/config/config.js"
log_info "Generating MagicMirror config.js..."
cat > "$config_file" << CONFIGJS
/* MagicMirror² Config - Generated by SecuBox mm2ctl */
let config = {
address: "$address",
port: $port,
basePath: "/",
ipWhitelist: [],
useHttps: false,
httpsPrivateKey: "",
httpsCertificate: "",
language: "$language",
locale: "$language",
logLevel: ["INFO", "LOG", "WARN", "ERROR"],
timeFormat: 24,
units: "$units",
modules: [
CONFIGJS
# Add clock module
if [ "$clock_enabled" = "1" ]; then
cat >> "$config_file" << CONFIGJS
{
module: "clock",
position: "top_left",
config: {
displaySeconds: $([ "$clock_display_seconds" = "1" ] && echo "true" || echo "false"),
showDate: $([ "$clock_show_date" = "1" ] && echo "true" || echo "false"),
}
},
CONFIGJS
fi
# Add weather module
if [ "$weather_enabled" = "1" ] && [ -n "$weather_api_key" ]; then
cat >> "$config_file" << CONFIGJS
{
module: "weather",
position: "top_right",
config: {
weatherProvider: "$weather_provider",
type: "current",
location: "$weather_location",
locationID: "$weather_location_id",
apiKey: "$weather_api_key",
units: "$units"
}
},
{
module: "weather",
position: "top_right",
header: "Weather Forecast",
config: {
weatherProvider: "$weather_provider",
type: "forecast",
location: "$weather_location",
locationID: "$weather_location_id",
apiKey: "$weather_api_key",
units: "$units"
}
},
CONFIGJS
fi
# Add calendar module
if [ "$calendar_enabled" = "1" ]; then
cat >> "$config_file" << CONFIGJS
{
module: "calendar",
header: "Calendar",
position: "top_left",
config: {
maximumEntries: $calendar_max_entries,
calendars: []
}
},
CONFIGJS
fi
# Add newsfeed module
if [ "$newsfeed_enabled" = "1" ]; then
cat >> "$config_file" << CONFIGJS
{
module: "newsfeed",
position: "bottom_bar",
config: {
feeds: [
{
title: "BBC News",
url: "https://feeds.bbci.co.uk/news/rss.xml"
}
],
showSourceTitle: true,
showPublishDate: true,
broadcastNewsFeeds: true,
broadcastNewsUpdates: true,
maxNewsItems: $newsfeed_max_items
}
},
CONFIGJS
fi
# Add compliments module
if [ "$compliments_enabled" = "1" ]; then
cat >> "$config_file" << CONFIGJS
{
module: "compliments",
position: "lower_third",
config: {
compliments: {
anytime: ["Welcome to SecuBox MagicMirror!"],
morning: ["Good morning!"],
afternoon: ["Good afternoon!"],
evening: ["Good evening!"]
}
}
},
CONFIGJS
fi
# Load custom modules from data directory
if [ -d "$data_path/modules" ]; then
for module_dir in "$data_path/modules"/MMM-*; do
if [ -d "$module_dir" ] && [ -f "$module_dir/package.json" ]; then
local module_name=$(basename "$module_dir")
# Check if module has a config file
if [ -f "$data_path/config/${module_name}.json" ]; then
local module_config=$(cat "$data_path/config/${module_name}.json")
cat >> "$config_file" << CONFIGJS
{
module: "$module_name",
position: "top_center",
config: $module_config
},
CONFIGJS
else
cat >> "$config_file" << CONFIGJS
{
module: "$module_name",
position: "top_center"
},
CONFIGJS
fi
fi
done
fi
# Close config
cat >> "$config_file" << CONFIGJS
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {module.exports = config;}
CONFIGJS
log_info "Config generated at $config_file"
}
# =============================================================================
# MODULE MANAGEMENT
# =============================================================================
list_installed_modules() {
load_config
local modules_dir="$data_path/modules"
if [ ! -d "$modules_dir" ]; then
echo "No modules directory found"
return
fi
echo ""
# List MMM-* modules
for module_dir in "$modules_dir"/MMM-*; do
[ -d "$module_dir" ] || continue
[ -f "$module_dir/package.json" ] || continue
local name=$(basename "$module_dir")
local version=$(jsonfilter -i "$module_dir/package.json" -e '@.version' 2>/dev/null || echo "unknown")
local desc=$(jsonfilter -i "$module_dir/package.json" -e '@.description' 2>/dev/null | head -c 60)
printf " %-30s v%-10s %s\n" "$name" "$version" "$desc"
done
# List mm-* modules
for module_dir in "$modules_dir"/mm-*; do
[ -d "$module_dir" ] || continue
[ -f "$module_dir/package.json" ] || continue
local name=$(basename "$module_dir")
local version=$(jsonfilter -i "$module_dir/package.json" -e '@.version' 2>/dev/null || echo "unknown")
local desc=$(jsonfilter -i "$module_dir/package.json" -e '@.description' 2>/dev/null | head -c 60)
printf " %-30s v%-10s %s\n" "$name" "$version" "$desc"
done
}
list_available_modules() {
local search="${1:-}"
local cache_file="/tmp/mm2_modules_cache.json"
# Download registry if not cached or old
if [ ! -f "$cache_file" ] || [ $(find "$cache_file" -mmin +60 2>/dev/null | wc -l) -gt 0 ]; then
log_info "Fetching module registry..."
wget -q -O "$cache_file" "$MM_REGISTRY_URL" || {
log_error "Failed to fetch module registry"
return 1
}
fi
echo "Available third-party modules:"
echo ""
if [ -n "$search" ]; then
grep -i "$search" "$cache_file" | head -50 || echo "No modules matching '$search'"
else
# Show first 30 modules
jsonfilter -i "$cache_file" -e '@[*].title' 2>/dev/null | head -30 | while read title; do
echo " $title"
done
echo ""
echo "Use 'mm2ctl module available <search>' to filter"
fi
}
install_module() {
local module_name="$1"
load_config
if [ -z "$module_name" ]; then
log_error "Module name required"
return 1
fi
# Check if container is running
if ! lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"; then
log_error "Container not running. Start it first: /etc/init.d/magicmirror2 start"
return 1
fi
# Use MMPM if available (has module registry)
if command -v mmpmctl >/dev/null 2>&1; then
log_info "Using MMPM to install: $module_name"
mmpmctl install-module "$module_name"
return $?
fi
# Fallback: manual git clone
local modules_dir="$data_path/modules"
ensure_dir "$modules_dir"
local git_url=""
local node_path="/opt/nodejs/node-v24.13.0/bin:/usr/local/bin:/usr/bin:/bin"
# Check if it's a URL
case "$module_name" in
http*|git@*)
git_url="$module_name"
module_name=$(basename "$git_url" .git)
;;
MMM-*|mm-*)
# Direct URL - no registry lookup
git_url=""
;;
*)
module_name="MMM-$module_name"
git_url=""
;;
esac
if [ -d "$modules_dir/$module_name" ]; then
log_warn "Module $module_name already installed"
return 0
fi
# If no URL provided, we need MMPM for registry lookup
if [ -z "$git_url" ]; then
log_error "Cannot install $module_name without URL"
log_error "Please provide full Git URL or install MMPM (opkg install secubox-app-mmpm)"
return 1
fi
log_info "Installing module: $module_name from $git_url"
# Clone the module with proper PATH
lxc-attach -n "$LXC_NAME" -- sh -c "export PATH=$node_path:\$PATH && cd /opt/magic_mirror/modules && git clone --depth 1 '$git_url' '$module_name'" || {
log_error "Failed to clone module"
return 1
}
# Install dependencies if package.json exists
if lxc-attach -n "$LXC_NAME" -- test -f "/opt/magic_mirror/modules/$module_name/package.json"; then
log_info "Installing module dependencies..."
lxc-attach -n "$LXC_NAME" -- sh -c "export PATH=$node_path:\$PATH && cd /opt/magic_mirror/modules/$module_name && npm install --production" || {
log_warn "Failed to install dependencies (module may still work)"
}
fi
log_info "Module $module_name installed successfully"
log_info "Restart MagicMirror2 to load the module: /etc/init.d/magicmirror2 restart"
}
remove_module() {
local module_name="$1"
load_config
if [ -z "$module_name" ]; then
log_error "Module name required"
return 1
fi
local module_path="$data_path/modules/$module_name"
if [ ! -d "$module_path" ]; then
log_error "Module $module_name not found"
return 1
fi
log_info "Removing module: $module_name"
rm -rf "$module_path"
# Remove config if exists
rm -f "$data_path/config/${module_name}.json"
log_info "Module $module_name removed"
log_info "Restart MagicMirror2 to apply: /etc/init.d/magicmirror2 restart"
}
update_module() {
local module_name="$1"
load_config
# Check if container is running
if ! lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"; then
log_error "Container not running. Start it first: /etc/init.d/magicmirror2 start"
return 1
fi
# Use MMPM if available
if command -v mmpmctl >/dev/null 2>&1; then
log_info "Using MMPM to update modules"
mmpmctl upgrade "$module_name"
return $?
fi
local node_path="/opt/nodejs/node-v24.13.0/bin:/usr/local/bin:/usr/bin:/bin"
if [ -z "$module_name" ]; then
# Update all modules
log_info "Updating all modules..."
# Update MMM-* modules
for module_dir in "$data_path/modules"/MMM-*; do
[ -d "$module_dir/.git" ] || continue
local name=$(basename "$module_dir")
log_info "Updating $name..."
lxc-attach -n "$LXC_NAME" -- sh -c "export PATH=$node_path:\$PATH && cd /opt/magic_mirror/modules/$name && git pull && npm install --production 2>/dev/null" || true
done
# Update mm-* modules
for module_dir in "$data_path/modules"/mm-*; do
[ -d "$module_dir/.git" ] || continue
local name=$(basename "$module_dir")
log_info "Updating $name..."
lxc-attach -n "$LXC_NAME" -- sh -c "export PATH=$node_path:\$PATH && cd /opt/magic_mirror/modules/$name && git pull && npm install --production 2>/dev/null" || true
done
else
local module_path="$data_path/modules/$module_name"
if [ ! -d "$module_path" ]; then
log_error "Module $module_name not found"
return 1
fi
log_info "Updating module: $module_name"
lxc-attach -n "$LXC_NAME" -- sh -c "export PATH=$node_path:\$PATH && cd /opt/magic_mirror/modules/$module_name && git pull && npm install --production 2>/dev/null" || {
log_error "Failed to update module"
return 1
}
fi
log_info "Update complete. Restart MagicMirror2 to apply."
}
# =============================================================================
# COMMANDS
# =============================================================================
cmd_install() {
require_root
load_config
if ! has_lxc; then
log_error "LXC not available. Install lxc packages first."
exit 1
fi
log_info "Installing MagicMirror2..."
# Create directories
ensure_dir "$data_path/config"
ensure_dir "$data_path/modules"
ensure_dir "$data_path/css"
lxc_check_prereqs || exit 1
lxc_create_rootfs || exit 1
# Generate initial config
generate_mm_config
uci_set main.enabled '1'
/etc/init.d/magicmirror2 enable
log_info "MagicMirror2 installed."
log_info "Start with: /etc/init.d/magicmirror2 start"
log_info "Web interface: http://<router-ip>:$port"
}
cmd_update() {
require_root
load_config
log_info "Updating MagicMirror2..."
lxc_destroy
lxc_create_rootfs || exit 1
if /etc/init.d/magicmirror2 enabled >/dev/null 2>&1; then
/etc/init.d/magicmirror2 restart
else
log_info "Update complete. Restart manually to apply."
fi
}
cmd_status() {
lxc_status
}
cmd_logs() {
lxc_logs "$@"
}
cmd_shell() {
lxc_shell
}
cmd_config() {
generate_mm_config
}
cmd_module() {
local action="$1"
shift
case "$action" in
list)
list_installed_modules
;;
available)
list_available_modules "$@"
;;
install)
install_module "$@"
;;
remove|uninstall)
remove_module "$@"
;;
update)
update_module "$@"
;;
*)
echo "Unknown module action: $action"
echo "Use: list, available, install, remove, update"
exit 1
;;
esac
}
cmd_service_run() {
require_root
load_config
if ! has_lxc; then
log_error "LXC not available"
exit 1
fi
lxc_check_prereqs || exit 1
lxc_run
}
cmd_service_stop() {
require_root
lxc_stop
}
cmd_set() {
local key="$1"
local value="$2"
if [ -z "$key" ] || [ -z "$value" ]; then
log_error "Usage: mm2ctl set <key> <value>"
exit 1
fi
uci_set "$key" "$value"
log_info "Set $key = $value"
}
cmd_get() {
local key="$1"
if [ -z "$key" ]; then
log_error "Usage: mm2ctl get <key>"
exit 1
fi
uci_get "$key"
}
# Main Entry Point
case "${1:-}" in
install) shift; cmd_install "$@" ;;
update) shift; cmd_update "$@" ;;
status) shift; cmd_status "$@" ;;
logs) shift; cmd_logs "$@" ;;
shell) shift; cmd_shell "$@" ;;
config) shift; cmd_config "$@" ;;
module) shift; cmd_module "$@" ;;
set) shift; cmd_set "$@" ;;
get) shift; cmd_get "$@" ;;
service-run) shift; cmd_service_run "$@" ;;
service-stop) shift; cmd_service_stop "$@" ;;
help|--help|-h|'') usage ;;
*) echo "Unknown command: $1" >&2; usage >&2; exit 1 ;;
esac