#!/usr/bin/env bash # hosts.command.sh — manage host/IP display name mappings # ============================================ # Lifecycle # ============================================ function cmd::hosts::on_load() { flag::register --ip flag::register --subnet flag::register --port flag::register --name flag::register --desc flag::register --tag flag::register --tags flag::register --force } # ============================================ # Help # ============================================ function cmd::hosts::help() { cat < [options] Manage host display names for IP resolution in logs, watch, and activity. Maps IPs, subnets, and ports to human-readable names. Subcommands: list List all host entries show --ip Show host entry details show --subnet Show subnet entry details show --port Show port entry details add --ip --name Add a host entry add --subnet --name Add a subnet entry add --port --name Add a port entry rm --ip Remove a host entry rm --subnet Remove a subnet entry rm --port Remove a port entry Options for add: --ip IP address to map --subnet Subnet CIDR to map (e.g. 10.0.0.0/24) --port Port number to map (e.g. 443) --name Display name (e.g. vodafone-wan) --desc Optional description --tag Tag (repeatable) --tags Tags (comma-separated) Options for rm: --force Skip confirmation Examples: wgctl hosts list wgctl hosts add --ip 148.69.46.73 --name vodafone-wan --desc "Vodafone WAN" wgctl hosts add --ip 94.63.0.129 --name nuno-home --tags home,isp wgctl hosts add --subnet 10.0.0.0/24 --name lan --desc "Local LAN" wgctl hosts add --port 443 --name https wgctl hosts show --ip 148.69.46.73 wgctl hosts rm --ip 148.69.46.73 EOF } # ============================================ # Run # ============================================ function cmd::hosts::run() { local subcmd="${1:-list}" shift || true case "$subcmd" in list) cmd::hosts::list "$@" ;; show) cmd::hosts::show "$@" ;; add) cmd::hosts::add "$@" ;; rm|remove|del) cmd::hosts::rm "$@" ;; help) cmd::hosts::help ;; *) log::error "Unknown subcommand: '${subcmd}'" cmd::hosts::help return 1 ;; esac } # ============================================ # List # ============================================ function cmd::hosts::list() { local filter_tag="" while [[ $# -gt 0 ]]; do case "$1" in --tag) filter_tag="$2"; shift 2 ;; --help) cmd::hosts::help; return ;; *) log::error "Unknown flag: $1"; return 1 ;; esac done local hosts_file hosts_file="$(ctx::hosts)" if [[ ! -f "$hosts_file" ]]; then log::wg_warning "No hosts configured. Use 'wgctl hosts add' to add one." return 0 fi local data data=$(json::hosts_list "$hosts_file" 2>/dev/null) [[ -z "$data" ]] && log::wg_warning "No hosts configured." && return 0 # Apply tag filter to data first local filtered_data="" while IFS='|' read -r type key name desc tags; do [[ -z "$type" ]] && continue [[ -n "$filter_tag" && "$tags" != *"$filter_tag"* ]] && continue filtered_data+="${type}|${key}|${name}|${desc}|${tags}"$'\n' done <<< "$data" [[ -z "$filtered_data" ]] && log::wg_warning "No hosts found." && return 0 # Measure column widths from filtered data local w_key=15 w_name=16 w_desc=10 while IFS='|' read -r type key name desc tags; do [[ -z "$type" ]] && continue (( ${#key} > w_key )) && w_key=${#key} (( ${#name} > w_name )) && w_name=${#name} local desc_len=${#desc} [[ -z "$desc" ]] && desc_len=1 # "—" = 1 visible char (( desc_len > w_desc )) && w_desc=$desc_len done <<< "$filtered_data" (( w_key += 2 )) (( w_name += 2 )) (( w_desc += 2 )) log::section "Host Mappings" echo "" local last_type="" found=false while IFS='|' read -r type key name desc tags; do [[ -z "$type" ]] && continue found=true # Section header when type changes if [[ "$type" != "$last_type" ]]; then [[ -n "$last_type" ]] && echo "" ui::hosts::section_header "$type" last_type="$type" fi ui::hosts::list_row "$type" "$key" "$name" "$desc" "$tags" \ "$w_key" "$w_name" "$w_desc" done <<< "$filtered_data" $found || log::wg_warning "No hosts configured." echo "" } # Table version (kept for future display config) function cmd::hosts::_list_table() { local hosts_file="${1:-}" printf "\n %-6s %-18s %-16s %-30s %s\n" \ "TYPE" "KEY" "NAME" "DESCRIPTION" "TAGS" printf " %s\n" "$(printf '─%.0s' {1..80})" while IFS='|' read -r type key name desc tags; do [[ -z "$type" ]] && continue printf " %-6s %-18s %-16s %-30s %s\n" \ "$type" "$key" "$name" "${desc:-—}" "${tags:-—}" done < <(json::hosts_list "$hosts_file" 2>/dev/null) printf "\n" } # ============================================ # Show # ============================================ function cmd::hosts::show() { local ip="" subnet="" port="" while [[ $# -gt 0 ]]; do case "$1" in --ip) ip="$2"; shift 2 ;; --subnet) subnet="$2"; shift 2 ;; --port) port="$2"; shift 2 ;; --help) cmd::hosts::help; return ;; *) log::error "Unknown flag: $1"; return 1 ;; esac done local key entry_type if [[ -n "$ip" ]]; then key="$ip"; entry_type="host"; fi if [[ -n "$subnet" ]]; then key="$subnet"; entry_type="subnet"; fi if [[ -n "$port" ]]; then key="$port"; entry_type="port"; fi [[ -z "$key" ]] && log::error "Specify --ip, --subnet, or --port" && return 1 hosts::require_exists "$entry_type" "$key" || return 1 log::section "${entry_type^}: ${key}" printf "\n" while IFS='|' read -r field val; do case "$field" in name) ui::row "Name" "${val:-—}" ;; desc) ui::row "Description" "${val:-—}" ;; tags) ui::row "Tags" "${val:-—}" ;; esac done < <(json::hosts_show "$(ctx::hosts)" "$key" "$entry_type") printf "\n" } # ============================================ # Add # ============================================ function cmd::hosts::add() { local ip="" subnet="" port="" name="" desc="" tags=() while [[ $# -gt 0 ]]; do case "$1" in --ip) ip="$2"; shift 2 ;; --subnet) subnet="$2"; shift 2 ;; --port) port="$2"; shift 2 ;; --name) name="$2"; shift 2 ;; --desc) desc="$2"; shift 2 ;; --tag) tags+=("$2"); shift 2 ;; --tags) IFS=',' read -ra t <<< "$2"; tags+=("${t[@]}"); shift 2 ;; --help) cmd::hosts::help; return ;; *) log::error "Unknown flag: $1"; return 1 ;; esac done [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1 local key entry_type if [[ -n "$ip" ]]; then key="$ip"; entry_type="host"; fi if [[ -n "$subnet" ]]; then key="$subnet"; entry_type="subnet"; fi if [[ -n "$port" ]]; then key="$port"; entry_type="port"; fi [[ -z "$key" ]] && log::error "Specify --ip, --subnet, or --port" && return 1 local tags_str tags_str=$(IFS=','; echo "${tags[*]}") json::hosts_add "$(ctx::hosts)" "$entry_type" "$key" "$name" "$desc" "$tags_str" log::wg_success "Added ${entry_type}: ${key} → ${name}" } # ============================================ # Remove # ============================================ function cmd::hosts::rm() { local ip="" subnet="" port="" force=false while [[ $# -gt 0 ]]; do case "$1" in --ip) ip="$2"; shift 2 ;; --subnet) subnet="$2"; shift 2 ;; --port) port="$2"; shift 2 ;; --force) force=true; shift ;; --help) cmd::hosts::help; return ;; *) log::error "Unknown flag: $1"; return 1 ;; esac done local key entry_type if [[ -n "$ip" ]]; then key="$ip"; entry_type="host"; fi if [[ -n "$subnet" ]]; then key="$subnet"; entry_type="subnet"; fi if [[ -n "$port" ]]; then key="$port"; entry_type="port"; fi [[ -z "$key" ]] && log::error "Specify --ip, --subnet, or --port" && return 1 hosts::require_exists "$entry_type" "$key" || return 1 if ! $force; then read -r -p "Remove ${entry_type} '${key}'? [y/N] " confirm case "$confirm" in [yY]*) ;; *) log::info "Aborted"; return 0 ;; esac fi json::hosts_remove "$(ctx::hosts)" "$entry_type" "$key" log::wg_success "Removed ${entry_type}: ${key}" }