- core/framework/: flag.sh, hook.sh, help.sh, command.sh, mixin.sh - core/app/: wgctl-specific context.sh, json.sh - core/framework/mixins/: json_output, no_color mixins - core/core.sh: sources framework/core.sh + app/core.sh - PYTHONPATH exported in app/core.sh for lib/ module resolution - command::_load_mixins: uses _FRAMEWORK_DIR for mixin path
130 lines
3.2 KiB
Bash
130 lines
3.2 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
# ============================================
|
|
# Command Registry
|
|
# ============================================
|
|
|
|
declare -A _LOADED_COMMANDS=()
|
|
|
|
readonly _COMMAND_NAMESPACE="cmd"
|
|
readonly _COMMAND_AUTO_LOAD_HOOK="on_load"
|
|
_CURRENT_LOADING_CMD=""
|
|
|
|
# ============================================
|
|
# Helpers
|
|
# ============================================
|
|
|
|
function command::loaded() { [[ -n "${_LOADED_COMMANDS["$1"]:-}" ]]; }
|
|
|
|
# Convert path-style name to namespace
|
|
# e.g. service/wireguard -> service::wireguard
|
|
function command::to_namespace() { echo "${1//\//:}"; }
|
|
|
|
# Build fully qualified function name
|
|
# e.g. command::fn "add" "run" -> cmd::add::run
|
|
# e.g. command::fn "service/wg" "run" -> cmd::service::wg::run
|
|
function command::fn() {
|
|
local name namespace
|
|
namespace=$(command::to_namespace "$1")
|
|
echo "${_COMMAND_NAMESPACE}::${namespace}::${2}"
|
|
}
|
|
|
|
function command::has_function() { declare -F "$(command::fn "$1" "$2")" >/dev/null 2>&1; }
|
|
function command::is_auto_load() { declare -F "$(command::fn "$1" on_load)" >/dev/null 2>&1; }
|
|
function command::exists() { command::has_function "$1" run; }
|
|
|
|
# ============================================
|
|
# Runner
|
|
# ============================================
|
|
|
|
# function command::run() {
|
|
# local cmd="$1"
|
|
# shift
|
|
|
|
# command::_reset_mixin_state # reset values only, keep _ACTIVE_MIXINS
|
|
|
|
# local -a args=("$@")
|
|
# command::_preprocess_flags args
|
|
|
|
# local fn
|
|
# fn=$(command::fn "$cmd" run)
|
|
# core::call_function "$fn" ${args[@]+"${args[@]}"}
|
|
# }
|
|
|
|
function command::run() {
|
|
local cmd="$1"
|
|
shift
|
|
|
|
command::_reset_mixin_state
|
|
|
|
# Build default args from config
|
|
local -a default_args=()
|
|
local defaults="${_COMMAND_DEFAULTS[$cmd]:-}"
|
|
if [[ -n "$defaults" ]]; then
|
|
read -ra default_args <<< "$defaults"
|
|
fi
|
|
|
|
local -a user_args=("$@")
|
|
[[ $# -gt 0 ]] && user_args=("$@")
|
|
|
|
# Resolve exclusive group conflicts — user args override defaults
|
|
local groups="${_FLAG_EXCLUSIVE_GROUPS[$cmd]:-}"
|
|
if [[ -n "$groups" && ${#default_args[@]} -gt 0 && ${#user_args[@]} -gt 0 ]]; then
|
|
command::_resolve_conflicts default_args user_args "$groups"
|
|
fi
|
|
|
|
local -a cleaned_defaults=()
|
|
for _d in "${default_args[@]:-}"; do
|
|
[[ -n "$_d" ]] && cleaned_defaults+=("$_d")
|
|
done
|
|
default_args=("${cleaned_defaults[@]:-}")
|
|
|
|
local -a args=()
|
|
for _d in "${default_args[@]:-}"; do
|
|
[[ -n "$_d" ]] && args+=("$_d")
|
|
done
|
|
for _u in "${user_args[@]:-}"; do
|
|
[[ -n "$_u" ]] && args+=("$_u")
|
|
done
|
|
|
|
# Preprocess mixin flags (--json, --no-color etc)
|
|
command::_preprocess_flags args
|
|
|
|
local fn
|
|
fn=$(command::fn "$cmd" run)
|
|
core::call_function "$fn" ${args[@]+"${args[@]}"}
|
|
}
|
|
|
|
|
|
function core::call_function() {
|
|
local fn="$1"
|
|
shift
|
|
"$fn" "$@"
|
|
}
|
|
|
|
# ============================================
|
|
# Loader
|
|
# ============================================
|
|
|
|
function load_command() {
|
|
local name="$1"
|
|
|
|
command::loaded "$name" && return 0
|
|
|
|
local path
|
|
path="$(ctx::commands)/${name}.command.sh"
|
|
|
|
if [[ ! -f "$path" ]]; then
|
|
log::error "Command not found: ${name} (${path})"
|
|
return 1
|
|
fi
|
|
|
|
source "$path"
|
|
_LOADED_COMMANDS["$name"]=1
|
|
|
|
_CURRENT_LOADING_CMD="$name"
|
|
core::call_if_exists "$(command::fn "$name" on_load)"
|
|
_CURRENT_LOADING_CMD=""
|
|
|
|
return 0
|
|
}
|