#!/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 echo "DEBUG cmd=$cmd groups='$groups' defs='${default_args[*]}' user='${user_args[*]:-}'" >&2 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 }