#!/usr/bin/env bash # core/command_mixins.sh # Mixin infrastructure — loads mixin files and provides command::mixin / flag::exclusive # ============================================ # Active mixin tracking (per-process) # ============================================ declare -a _ACTIVE_MIXINS=() # ============================================ # Auto-load all mixin files # ============================================ function command::_load_mixins() { local mixin_file local -a mixin_paths=( "${_FRAMEWORK_DIR}/mixins/"*.mixin.sh ) for mixin_file in "${mixin_paths[@]:-}"; do [[ -f "$mixin_file" ]] && source "$mixin_file" done } # ============================================ # command::mixin # Called from cmd::::on_load to opt into a mixin # ============================================ function command::mixin() { local name="${1:-}" [[ -z "$name" ]] && log::error "command::mixin: missing name" && return 1 local register_fn="command::mixin::${name}::register" if declare -f "$register_fn" >/dev/null 2>&1; then # Track for reset/process — avoid duplicates local m already=false for m in "${_ACTIVE_MIXINS[@]:-}"; do [[ "$m" == "$name" ]] && already=true && break done $already || _ACTIVE_MIXINS+=("$name") "$register_fn" else log::error "Unknown mixin: ${name} (no command::mixin::${name}::register found)" return 1 fi } # ============================================ # command::_reset_mixins # Called by command::run before each invocation # ============================================ function command::_reset_mixin_state() { # Reset values but keep _ACTIVE_MIXINS populated local m for m in "${_ACTIVE_MIXINS[@]:-}"; do local reset_fn="command::mixin::${m}::reset" declare -f "$reset_fn" >/dev/null 2>&1 && "$reset_fn" done } function command::_reset_mixins() { _ACTIVE_MIXINS=() local reset_fn mixin_file # Reset all known mixins regardless of active state for mixin_file in \ "${WGCTL_DIR}/core/mixins/"*.mixin.sh \ "${WGCTL_DIR}/commands/mixins/"*.mixin.sh; do [[ -f "$mixin_file" ]] || continue local mixin_name mixin_name=$(basename "$mixin_file" .mixin.sh) reset_fn="command::mixin::${mixin_name}::reset" declare -f "$reset_fn" >/dev/null 2>&1 && "$reset_fn" done } # ============================================ # command::_preprocess_flags # Called by command::run — strips mixin flags from args # ============================================ function command::_preprocess_flags() { local -n _args_ref="$1" local -a _filtered=() if [[ ${#_args_ref[@]} -eq 0 ]]; then return 0 # nothing to process fi for _arg in "${_args_ref[@]}"; do local _consumed=false for _mixin in "${_ACTIVE_MIXINS[@]:-}"; do local _process_fn="command::mixin::${_mixin}::process" if declare -f "$_process_fn" >/dev/null 2>&1; then if "$_process_fn" "$_arg"; then _consumed=true break fi fi done $_consumed || _filtered+=("$_arg") done if [[ ${#_filtered[@]} -gt 0 ]]; then _args_ref=("${_filtered[@]}") else _args_ref=() fi } # command::_resolve_conflicts # Removes conflicting defaults when user provides a member of an exclusive group function command::_resolve_conflicts() { local -n _def_ref="$1" local -n _usr_ref="$2" local groups="$3" [[ -z "$groups" ]] && return 0 [[ ${#_def_ref[@]} -eq 0 ]] && return 0 # Work on a copy — progressively filter across all groups local -a working=("${_def_ref[@]}") local group while IFS= read -r group; do [[ -z "$group" ]] && continue local -a members=() IFS=',' read -ra members <<< "$group" # Find which member (if any) the user passed from this group local user_member="" local member user_arg for member in "${members[@]}"; do for user_arg in "${_usr_ref[@]:-}"; do if [[ "$user_arg" == "$member" ]]; then user_member="$member" break 2 fi done done # No user member in this group — don't touch defaults [[ -z "$user_member" ]] && continue # User passed a member — remove all OTHER members from defaults # (keep the same flag if it was already in defaults) local -a new_working=() local def_arg for def_arg in "${working[@]:-}"; do local is_other_member=false for member in "${members[@]}"; do # It's another member if it's in the group AND not the same as user's choice if [[ "$def_arg" == "$member" && "$def_arg" != "$user_member" ]]; then is_other_member=true break fi done $is_other_member || new_working+=("$def_arg") done working=("${new_working[@]:-}") done < <(echo "$groups" | tr '|' '\n') # Write back if [[ ${#working[@]} -gt 0 ]]; then _def_ref=("${working[@]}") else _def_ref=() fi } # ============================================ # Flag Exclusive # ============================================ declare -gA _FLAG_EXCLUSIVE_GROUPS=() # flag::exclusive ... # Called from on_load — registers mutually exclusive flags for current command function flag::exclusive() { local cmd="${_CURRENT_LOADING_CMD:-}" [[ -z "$cmd" ]] && return 0 # Join flags with comma as one group local group group=$(IFS=','; echo "$*") if [[ -n "${_FLAG_EXCLUSIVE_GROUPS[$cmd]:-}" ]]; then _FLAG_EXCLUSIVE_GROUPS["$cmd"]+="${group}|" else _FLAG_EXCLUSIVE_GROUPS["$cmd"]="${group}|" fi }