From 3fbb60c3e1c90bf714999ace9aec990ed76a71c9 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Fri, 29 May 2026 13:57:49 -0400 Subject: [PATCH 1/4] Skill for Namecheap DNS Management API --- skills/namecheap/SKILL.md | 109 +++ skills/namecheap/namecheap.sh | 919 +++++++++++++++++++ skills/namecheap/references/namecheap-api.md | 392 ++++++++ 3 files changed, 1420 insertions(+) create mode 100644 skills/namecheap/SKILL.md create mode 100755 skills/namecheap/namecheap.sh create mode 100644 skills/namecheap/references/namecheap-api.md diff --git a/skills/namecheap/SKILL.md b/skills/namecheap/SKILL.md new file mode 100644 index 000000000..426b3b484 --- /dev/null +++ b/skills/namecheap/SKILL.md @@ -0,0 +1,109 @@ +--- +name: namecheap +description: 'Manage DNS records for domains registered with Namecheap via their API. List domains, view/add/update/remove DNS host entries (A, AAAA, CNAME, MX, TXT, etc.), and guide users through API setup including public IP detection and credential configuration. Use when the user mentions Namecheap, DNS records, domain management, or wants to add/change/remove A records, CNAME records, MX records, or TXT records for their domains.' +--- + +# Namecheap DNS Management + +**UTILITY SKILL** — manages DNS records via the Namecheap API. +USE FOR: "add DNS record", "update A record", "manage Namecheap domains", "set CNAME", "add MX record", "add TXT record", "list my domains", "show DNS records", "namecheap setup", "configure namecheap API", "what is my public IP" +DO NOT USE FOR: domain registration/purchase, SSL certificate management, hosting configuration, non-Namecheap DNS providers + +## Workflow + +### First-time Setup + +Before executing any API commands, verify credentials are configured: + +1. **Check for existing config** — look for `~/.namecheap-api` +2. If not configured, guide the user through setup: + a. **Show public IP** — run `curl -s https://api.ipify.org` to display the user's public IP + b. **Instruct IP whitelisting** — tell the user to go to https://ap.www.namecheap.com/settings/tools/apiaccess/, enable API (select ON), and whitelist the displayed IP + c. **Collect credentials** — use `ask_user` to get their Namecheap username, then their API key + d. **Save config** — write credentials to `~/.namecheap-api` with `chmod 600` + e. **Validate** — run a test API call to confirm access works + +### DNS Operations + +Use the `namecheap.sh` script (bundled in this skill's directory) for all API interactions: + +```bash +# Show public IP (for setup) +bash namecheap.sh public-ip + +# Run setup flow +bash namecheap.sh setup + +# List domains +bash namecheap.sh domains.getList + +# Get nameservers for a domain (shows if using Namecheap DNS or custom) +bash namecheap.sh domains.dns.getList --domain example.com + +# Get DNS records for a domain +bash namecheap.sh domains.dns.getHosts --domain example.com + +# Add a single record (preserves existing records) +bash namecheap.sh dns.addHost --domain example.com --type A --name www --address 1.2.3.4 --ttl 1800 + +# Remove a single record +bash namecheap.sh dns.removeHost --domain example.com --type A --name www --address 1.2.3.4 + +# Replace all records from a JSON file +bash namecheap.sh domains.dns.setHosts --domain example.com --hosts records.json + +# Switch to Namecheap default DNS +bash namecheap.sh domains.dns.setDefault --domain example.com + +# Switch to custom nameservers +bash namecheap.sh domains.dns.setCustom --domain example.com --nameservers ns1.cloudflare.com,ns2.cloudflare.com + +# Get email forwarding rules +bash namecheap.sh domains.dns.getEmailForwarding --domain example.com + +# Set email forwarding (single rule) +bash namecheap.sh domains.dns.setEmailForwarding --domain example.com --mailbox info --forward-to user@gmail.com + +# Set email forwarding (from JSON file) +bash namecheap.sh domains.dns.setEmailForwarding --domain example.com --forwards forwards.json + +# Create a child nameserver (glue record) +bash namecheap.sh domains.ns.create --domain example.com --nameserver ns1.example.com --ip 1.2.3.4 + +# Delete a child nameserver +bash namecheap.sh domains.ns.delete --domain example.com --nameserver ns1.example.com + +# Get nameserver info +bash namecheap.sh domains.ns.getInfo --domain example.com --nameserver ns1.example.com + +# Update nameserver IP +bash namecheap.sh domains.ns.update --domain example.com --nameserver ns1.example.com --old-ip 1.2.3.4 --ip 5.6.7.8 +``` + +## Behavior + +- **Always check credentials first.** Before any API operation, verify `~/.namecheap-api` exists and is readable. If not, run the setup flow. +- **Show current records before modifying.** Before adding or removing records, always fetch and display the current DNS records so the user can confirm the change. +- **Use `ask_user` to confirm destructive changes.** Before removing records or replacing all records with `setHosts`, confirm with the user. +- **The Namecheap `setHosts` API replaces ALL records.** Never call `domains.dns.setHosts` directly unless you have fetched all existing records first. Use `dns.addHost` and `dns.removeHost` for safe single-record operations — they handle the fetch-modify-write cycle internally. +- **Explain TTL in human terms.** When the user asks about TTL, explain that 1800 = 30 minutes, 3600 = 1 hour, etc. +- **Handle multi-part TLDs.** Domains like `example.co.uk` have SLD=example and TLD=co.uk. The script handles this automatically. + +## Credential Storage + +Credentials are stored in `~/.namecheap-api`: + +```bash +NAMECHEAP_API_USER="username" +NAMECHEAP_API_KEY="api-key-here" +``` + +This file must have `600` permissions (owner read/write only). + +## Supported Record Types + +A, AAAA, CNAME, MX, MXE, TXT, URL, URL301, FRAME + +## References + +See `references/namecheap-api.md` for full API documentation including request/response formats. diff --git a/skills/namecheap/namecheap.sh b/skills/namecheap/namecheap.sh new file mode 100755 index 000000000..8cc246bcc --- /dev/null +++ b/skills/namecheap/namecheap.sh @@ -0,0 +1,919 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Namecheap API CLI wrapper +# Usage: ./namecheap.sh [options] + +NAMECHEAP_API_URL="https://api.namecheap.com/xml.response" +CONFIG_FILE="$HOME/.namecheap-api" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +print_error() { echo -e "${RED}Error:${NC} $1" >&2; } +print_success() { echo -e "${GREEN}✓${NC} $1"; } +print_info() { echo -e "${CYAN}ℹ${NC} $1"; } +print_warn() { echo -e "${YELLOW}⚠${NC} $1"; } + +# Load configuration +load_config() { + if [[ -f "$CONFIG_FILE" ]]; then + # shellcheck source=/dev/null + source "$CONFIG_FILE" + fi +} + +# Check if credentials are configured +check_credentials() { + load_config + if [[ -z "${NAMECHEAP_API_USER:-}" || -z "${NAMECHEAP_API_KEY:-}" ]]; then + print_error "Namecheap API credentials not configured." + echo "" + echo "Run './namecheap.sh setup' to configure your credentials." + echo "" + echo "You need:" + echo " 1. Your Namecheap username" + echo " 2. An API key from: https://ap.www.namecheap.com/settings/tools/apiaccess/" + echo " 3. Your public IP whitelisted in the API settings" + exit 1 + fi +} + +# Get public IP address +get_public_ip() { + curl -s https://api.ipify.org 2>/dev/null || curl -s https://ifconfig.me 2>/dev/null || echo "unknown" +} + +# Make API request +api_request() { + local command="$1" + shift + local extra_params=("$@") + + check_credentials + local client_ip + client_ip=$(get_public_ip) + + local url="${NAMECHEAP_API_URL}?ApiUser=${NAMECHEAP_API_USER}&ApiKey=${NAMECHEAP_API_KEY}&UserName=${NAMECHEAP_API_USER}&Command=namecheap.${command}&ClientIp=${client_ip}" + + for param in "${extra_params[@]}"; do + url="${url}&${param}" + done + + local response + response=$(curl -s "$url") + + # Check for errors in the response + if echo "$response" | grep -q 'Status="ERROR"'; then + local error_msg + error_msg=$(echo "$response" | grep -oP '(?<=]*>\K[^<]+' 2>/dev/null || echo "$response" | sed -n 's/.*]*>\(.*\)<\/Err>.*/\1/p') + print_error "API returned error: $error_msg" + return 1 + fi + + echo "$response" +} + +# Parse domain into SLD and TLD +parse_domain() { + local domain="$1" + local tld sld + + # Handle multi-part TLDs (e.g., co.uk, com.br) + if echo "$domain" | grep -qE '\.(co|com|net|org|gov)\.[a-z]{2}$'; then + tld=$(echo "$domain" | grep -oE '\.[^.]+\.[^.]+$' | sed 's/^\.//') + sld=$(echo "$domain" | sed "s/\.${tld}$//") + else + tld="${domain##*.}" + sld="${domain%.*}" + fi + + echo "$sld" "$tld" +} + +# Format XML DNS records as a table +format_dns_records() { + local xml="$1" + + # Extract host records + echo "" + printf "%-20s %-8s %-40s %-8s %-6s\n" "HOST" "TYPE" "ADDRESS" "TTL" "MXPREF" + printf "%-20s %-8s %-40s %-8s %-6s\n" "----" "----" "-------" "---" "------" + + echo "$xml" | grep -oP '' | while read -r line; do + local name type address ttl mxpref + name=$(echo "$line" | grep -oP 'Name="\K[^"]+' || echo "") + type=$(echo "$line" | grep -oP 'Type="\K[^"]+' || echo "") + address=$(echo "$line" | grep -oP 'Address="\K[^"]+' || echo "") + ttl=$(echo "$line" | grep -oP 'TTL="\K[^"]+' || echo "1800") + mxpref=$(echo "$line" | grep -oP 'MXPref="\K[^"]+' || echo "-") + + printf "%-20s %-8s %-40s %-8s %-6s\n" "$name" "$type" "$address" "$ttl" "$mxpref" + done + echo "" +} + +# Format domains list as a table +format_domains_list() { + local xml="$1" + + echo "" + printf "%-30s %-12s %-12s %-10s\n" "DOMAIN" "EXPIRES" "LOCKED" "AUTO-RENEW" + printf "%-30s %-12s %-12s %-10s\n" "------" "-------" "------" "----------" + + echo "$xml" | grep -oP '' | while read -r line; do + local name expires locked autorenew + name=$(echo "$line" | grep -oP 'Name="\K[^"]+' || echo "") + expires=$(echo "$line" | grep -oP 'Expires="\K[^"]+' || echo "") + locked=$(echo "$line" | grep -oP 'IsLocked="\K[^"]+' || echo "") + autorenew=$(echo "$line" | grep -oP 'AutoRenew="\K[^"]+' || echo "") + + printf "%-30s %-12s %-12s %-10s\n" "$name" "$expires" "$locked" "$autorenew" + done + echo "" +} + +# Commands + +cmd_setup() { + echo "=== Namecheap API Setup ===" + echo "" + + # Show public IP + local public_ip + public_ip=$(get_public_ip) + print_info "Your public IP address is: ${CYAN}${public_ip}${NC}" + echo "" + echo "Make sure this IP is whitelisted at:" + echo " https://ap.www.namecheap.com/settings/tools/apiaccess/" + echo "" + + # Check existing config + if [[ -f "$CONFIG_FILE" ]]; then + load_config + if [[ -n "${NAMECHEAP_API_USER:-}" ]]; then + print_info "Existing configuration found for user: ${NAMECHEAP_API_USER}" + echo "" + + # Test the connection + echo "Testing API connection..." + if api_request "domains.getList" "PageSize=1" > /dev/null 2>&1; then + print_success "API connection successful!" + else + print_error "API connection failed. Please check your credentials and IP whitelist." + fi + return 0 + fi + fi + + # Prompt for credentials + echo "Enter your Namecheap credentials:" + echo "" + read -rp " API Username: " api_user + read -rsp " API Key: " api_key + echo "" + echo "" + + if [[ -z "$api_user" || -z "$api_key" ]]; then + print_error "Both username and API key are required." + exit 1 + fi + + # Save configuration + cat > "$CONFIG_FILE" << EOF +NAMECHEAP_API_USER="${api_user}" +NAMECHEAP_API_KEY="${api_key}" +EOF + chmod 600 "$CONFIG_FILE" + print_success "Credentials saved to ${CONFIG_FILE}" + echo "" + + # Test connection + load_config + echo "Testing API connection..." + if api_request "domains.getList" "PageSize=1" > /dev/null 2>&1; then + print_success "API connection successful!" + else + print_warn "API connection failed. Please verify:" + echo " 1. API access is enabled (ON) at the Namecheap settings page" + echo " 2. IP address ${public_ip} is whitelisted" + echo " 3. Your API key is correct" + fi +} + +cmd_domains_list() { + local list_type="ALL" + local search_term="" + local page="1" + local page_size="20" + + while [[ $# -gt 0 ]]; do + case "$1" in + --type) list_type="$2"; shift 2 ;; + --search) search_term="$2"; shift 2 ;; + --page) page="$2"; shift 2 ;; + --page-size) page_size="$2"; shift 2 ;; + *) shift ;; + esac + done + + local params=("ListType=${list_type}" "Page=${page}" "PageSize=${page_size}") + if [[ -n "$search_term" ]]; then + params+=("SearchTerm=${search_term}") + fi + + print_info "Fetching domain list..." + local response + response=$(api_request "domains.getList" "${params[@]}") + format_domains_list "$response" +} + +cmd_dns_get_hosts() { + local domain="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --domain) domain="$2"; shift 2 ;; + *) shift ;; + esac + done + + if [[ -z "$domain" ]]; then + print_error "Domain is required. Usage: ./namecheap.sh domains.dns.getHosts --domain example.com" + exit 1 + fi + + local sld tld + read -r sld tld <<< "$(parse_domain "$domain")" + + print_info "Fetching DNS records for ${domain} (SLD=${sld}, TLD=${tld})..." + local response + response=$(api_request "domains.dns.getHosts" "SLD=${sld}" "TLD=${tld}") + format_dns_records "$response" +} + +cmd_dns_set_hosts() { + local domain="" + local hosts_file="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --domain) domain="$2"; shift 2 ;; + --hosts) hosts_file="$2"; shift 2 ;; + *) shift ;; + esac + done + + if [[ -z "$domain" || -z "$hosts_file" ]]; then + print_error "Both --domain and --hosts are required." + echo "Usage: ./namecheap.sh domains.dns.setHosts --domain example.com --hosts hosts.json" + exit 1 + fi + + if [[ ! -f "$hosts_file" ]]; then + print_error "Hosts file not found: ${hosts_file}" + exit 1 + fi + + local sld tld + read -r sld tld <<< "$(parse_domain "$domain")" + + # Build host parameters from JSON file + local params=("SLD=${sld}" "TLD=${tld}") + local i=1 + + while IFS= read -r line; do + local hostname recordtype address ttl mxpref + hostname=$(echo "$line" | grep -oP '"HostName"\s*:\s*"\K[^"]+' || echo "") + recordtype=$(echo "$line" | grep -oP '"RecordType"\s*:\s*"\K[^"]+' || echo "") + address=$(echo "$line" | grep -oP '"Address"\s*:\s*"\K[^"]+' || echo "") + ttl=$(echo "$line" | grep -oP '"TTL"\s*:\s*"\K[^"]+' || echo "1800") + mxpref=$(echo "$line" | grep -oP '"MXPref"\s*:\s*"\K[^"]+' || echo "") + + if [[ -n "$hostname" && -n "$recordtype" && -n "$address" ]]; then + params+=("HostName${i}=${hostname}") + params+=("RecordType${i}=${recordtype}") + params+=("Address${i}=${address}") + params+=("TTL${i}=${ttl}") + if [[ -n "$mxpref" ]]; then + params+=("MXPref${i}=${mxpref}") + fi + ((i++)) + fi + done < <(python3 -c " +import json, sys +with open('${hosts_file}') as f: + records = json.load(f) +for r in records: + print(json.dumps(r)) +" 2>/dev/null || jq -c '.[]' "$hosts_file") + + if [[ $i -eq 1 ]]; then + print_error "No valid host records found in ${hosts_file}" + exit 1 + fi + + print_info "Setting $((i-1)) DNS records for ${domain}..." + local response + response=$(api_request "domains.dns.setHosts" "${params[@]}") + + if echo "$response" | grep -q 'IsSuccess="true"'; then + print_success "DNS records updated successfully for ${domain}!" + else + print_error "Failed to update DNS records." + echo "$response" + fi +} + +cmd_dns_add_host() { + local domain="" record_type="" name="" address="" ttl="1800" mxpref="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --domain) domain="$2"; shift 2 ;; + --type) record_type="$2"; shift 2 ;; + --name) name="$2"; shift 2 ;; + --address) address="$2"; shift 2 ;; + --ttl) ttl="$2"; shift 2 ;; + --mxpref) mxpref="$2"; shift 2 ;; + *) shift ;; + esac + done + + if [[ -z "$domain" || -z "$record_type" || -z "$name" || -z "$address" ]]; then + print_error "Missing required parameters." + echo "Usage: ./namecheap.sh dns.addHost --domain example.com --type A --name \"@\" --address \"1.2.3.4\" [--ttl 1800] [--mxpref 10]" + exit 1 + fi + + local sld tld + read -r sld tld <<< "$(parse_domain "$domain")" + + # Fetch existing records + print_info "Fetching existing DNS records for ${domain}..." + local response + response=$(api_request "domains.dns.getHosts" "SLD=${sld}" "TLD=${tld}") + + # Build params with existing records + new one + local params=("SLD=${sld}" "TLD=${tld}") + local i=1 + + # Parse existing records + while IFS= read -r line; do + if [[ -z "$line" ]]; then continue; fi + local h_name h_type h_address h_ttl h_mxpref + h_name=$(echo "$line" | grep -oP 'Name="\K[^"]+' || echo "") + h_type=$(echo "$line" | grep -oP 'Type="\K[^"]+' || echo "") + h_address=$(echo "$line" | grep -oP 'Address="\K[^"]+' || echo "") + h_ttl=$(echo "$line" | grep -oP 'TTL="\K[^"]+' || echo "1800") + h_mxpref=$(echo "$line" | grep -oP 'MXPref="\K[^"]+' || echo "") + + if [[ -n "$h_name" && -n "$h_type" && -n "$h_address" ]]; then + params+=("HostName${i}=${h_name}") + params+=("RecordType${i}=${h_type}") + params+=("Address${i}=${h_address}") + params+=("TTL${i}=${h_ttl}") + if [[ -n "$h_mxpref" && "$h_mxpref" != "0" ]]; then + params+=("MXPref${i}=${h_mxpref}") + fi + ((i++)) + fi + done < <(echo "$response" | grep -oP '') + + # Add the new record + params+=("HostName${i}=${name}") + params+=("RecordType${i}=${record_type}") + params+=("Address${i}=${address}") + params+=("TTL${i}=${ttl}") + if [[ -n "$mxpref" ]]; then + params+=("MXPref${i}=${mxpref}") + fi + + print_info "Adding ${record_type} record: ${name} -> ${address}" + local set_response + set_response=$(api_request "domains.dns.setHosts" "${params[@]}") + + if echo "$set_response" | grep -q 'IsSuccess="true"'; then + print_success "DNS record added successfully!" + else + print_error "Failed to add DNS record." + echo "$set_response" + fi +} + +cmd_dns_remove_host() { + local domain="" record_type="" name="" address="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --domain) domain="$2"; shift 2 ;; + --type) record_type="$2"; shift 2 ;; + --name) name="$2"; shift 2 ;; + --address) address="$2"; shift 2 ;; + *) shift ;; + esac + done + + if [[ -z "$domain" || -z "$record_type" || -z "$name" ]]; then + print_error "Missing required parameters." + echo "Usage: ./namecheap.sh dns.removeHost --domain example.com --type A --name \"@\" [--address \"1.2.3.4\"]" + exit 1 + fi + + local sld tld + read -r sld tld <<< "$(parse_domain "$domain")" + + # Fetch existing records + print_info "Fetching existing DNS records for ${domain}..." + local response + response=$(api_request "domains.dns.getHosts" "SLD=${sld}" "TLD=${tld}") + + # Build params excluding the record to remove + local params=("SLD=${sld}" "TLD=${tld}") + local i=1 + local removed=false + + while IFS= read -r line; do + if [[ -z "$line" ]]; then continue; fi + local h_name h_type h_address h_ttl h_mxpref + h_name=$(echo "$line" | grep -oP 'Name="\K[^"]+' || echo "") + h_type=$(echo "$line" | grep -oP 'Type="\K[^"]+' || echo "") + h_address=$(echo "$line" | grep -oP 'Address="\K[^"]+' || echo "") + h_ttl=$(echo "$line" | grep -oP 'TTL="\K[^"]+' || echo "1800") + h_mxpref=$(echo "$line" | grep -oP 'MXPref="\K[^"]+' || echo "") + + # Check if this is the record to remove + if [[ "$h_name" == "$name" && "$h_type" == "$record_type" && "$removed" == "false" ]]; then + if [[ -z "$address" || "$h_address" == "$address" ]]; then + removed=true + print_info "Removing record: ${h_name} ${h_type} ${h_address}" + continue + fi + fi + + if [[ -n "$h_name" && -n "$h_type" && -n "$h_address" ]]; then + params+=("HostName${i}=${h_name}") + params+=("RecordType${i}=${h_type}") + params+=("Address${i}=${h_address}") + params+=("TTL${i}=${h_ttl}") + if [[ -n "$h_mxpref" && "$h_mxpref" != "0" ]]; then + params+=("MXPref${i}=${h_mxpref}") + fi + ((i++)) + fi + done < <(echo "$response" | grep -oP '') + + if [[ "$removed" == "false" ]]; then + print_error "No matching record found to remove." + exit 1 + fi + + # If no records left, we still need at least one (Namecheap requirement) + if [[ $i -eq 1 ]]; then + print_error "Cannot remove the last DNS record. Namecheap requires at least one record." + exit 1 + fi + + print_info "Updating DNS records for ${domain}..." + local set_response + set_response=$(api_request "domains.dns.setHosts" "${params[@]}") + + if echo "$set_response" | grep -q 'IsSuccess="true"'; then + print_success "DNS record removed successfully!" + else + print_error "Failed to remove DNS record." + echo "$set_response" + fi +} + +cmd_public_ip() { + local ip + ip=$(get_public_ip) + echo "$ip" +} + +cmd_dns_get_list() { + local domain="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --domain) domain="$2"; shift 2 ;; + *) shift ;; + esac + done + + if [[ -z "$domain" ]]; then + print_error "Domain is required. Usage: ./namecheap.sh domains.dns.getList --domain example.com" + exit 1 + fi + + local sld tld + read -r sld tld <<< "$(parse_domain "$domain")" + + print_info "Fetching nameservers for ${domain}..." + local response + response=$(api_request "domains.dns.getList" "SLD=${sld}" "TLD=${tld}") + + local using_our_dns + using_our_dns=$(echo "$response" | grep -oP 'IsUsingOurDNS="\K[^"]+' || echo "unknown") + echo "" + print_info "Using Namecheap DNS: ${using_our_dns}" + echo "" + echo "Nameservers:" + echo "$response" | grep -oP '\K[^<]+' | while read -r ns; do + echo " - ${ns}" + done + echo "" +} + +cmd_dns_set_default() { + local domain="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --domain) domain="$2"; shift 2 ;; + *) shift ;; + esac + done + + if [[ -z "$domain" ]]; then + print_error "Domain is required. Usage: ./namecheap.sh domains.dns.setDefault --domain example.com" + exit 1 + fi + + local sld tld + read -r sld tld <<< "$(parse_domain "$domain")" + + print_info "Setting ${domain} to use Namecheap default DNS..." + local response + response=$(api_request "domains.dns.setDefault" "SLD=${sld}" "TLD=${tld}") + + if echo "$response" | grep -q 'Updated="true"'; then + print_success "Domain ${domain} now uses Namecheap default DNS!" + else + print_error "Failed to set default DNS." + echo "$response" + fi +} + +cmd_dns_set_custom() { + local domain="" nameservers="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --domain) domain="$2"; shift 2 ;; + --nameservers) nameservers="$2"; shift 2 ;; + *) shift ;; + esac + done + + if [[ -z "$domain" || -z "$nameservers" ]]; then + print_error "Both --domain and --nameservers are required." + echo "Usage: ./namecheap.sh domains.dns.setCustom --domain example.com --nameservers ns1.cloudflare.com,ns2.cloudflare.com" + exit 1 + fi + + local sld tld + read -r sld tld <<< "$(parse_domain "$domain")" + + print_info "Setting ${domain} to use custom nameservers: ${nameservers}" + local response + response=$(api_request "domains.dns.setCustom" "SLD=${sld}" "TLD=${tld}" "Nameservers=${nameservers}") + + if echo "$response" | grep -q 'Updated="true"'; then + print_success "Domain ${domain} now uses custom nameservers!" + else + print_error "Failed to set custom nameservers." + echo "$response" + fi +} + +cmd_dns_get_email_forwarding() { + local domain="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --domain) domain="$2"; shift 2 ;; + *) shift ;; + esac + done + + if [[ -z "$domain" ]]; then + print_error "Domain is required. Usage: ./namecheap.sh domains.dns.getEmailForwarding --domain example.com" + exit 1 + fi + + print_info "Fetching email forwarding for ${domain}..." + local response + response=$(api_request "domains.dns.getEmailForwarding" "DomainName=${domain}") + + echo "" + printf "%-20s %-40s\n" "MAILBOX" "FORWARDS TO" + printf "%-20s %-40s\n" "-------" "-----------" + + echo "$response" | grep -oP '' | while read -r line; do + local mailbox forward_to + mailbox=$(echo "$line" | grep -oP 'mailbox="\K[^"]+' || echo "") + forward_to=$(echo "$line" | grep -oP 'ForwardTo="\K[^"]+' || echo "") + printf "%-20s %-40s\n" "${mailbox}@${domain}" "$forward_to" + done + echo "" +} + +cmd_dns_set_email_forwarding() { + local domain="" forwards_file="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --domain) domain="$2"; shift 2 ;; + --forwards) forwards_file="$2"; shift 2 ;; + --mailbox) + # Inline single forwarding rule + local inline_mailbox="$2"; shift 2 ;; + --forward-to) + local inline_forward_to="$2"; shift 2 ;; + *) shift ;; + esac + done + + if [[ -z "$domain" ]]; then + print_error "Domain is required." + echo "Usage: ./namecheap.sh domains.dns.setEmailForwarding --domain example.com --mailbox info --forward-to user@gmail.com" + echo " or: ./namecheap.sh domains.dns.setEmailForwarding --domain example.com --forwards forwards.json" + exit 1 + fi + + local params=("DomainName=${domain}") + + if [[ -n "${inline_mailbox:-}" && -n "${inline_forward_to:-}" ]]; then + # Single inline rule + params+=("MailBox1=${inline_mailbox}" "ForwardTo1=${inline_forward_to}") + elif [[ -n "$forwards_file" ]]; then + if [[ ! -f "$forwards_file" ]]; then + print_error "Forwards file not found: ${forwards_file}" + exit 1 + fi + local i=1 + while IFS= read -r line; do + local mailbox forward_to + mailbox=$(echo "$line" | grep -oP '"MailBox"\s*:\s*"\K[^"]+' || echo "") + forward_to=$(echo "$line" | grep -oP '"ForwardTo"\s*:\s*"\K[^"]+' || echo "") + if [[ -n "$mailbox" && -n "$forward_to" ]]; then + params+=("MailBox${i}=${mailbox}" "ForwardTo${i}=${forward_to}") + ((i++)) + fi + done < <(python3 -c " +import json, sys +with open('${forwards_file}') as f: + rules = json.load(f) +for r in rules: + print(json.dumps(r)) +" 2>/dev/null || jq -c '.[]' "$forwards_file") + else + print_error "Provide either --mailbox/--forward-to or --forwards " + exit 1 + fi + + print_info "Setting email forwarding for ${domain}..." + local response + response=$(api_request "domains.dns.setEmailForwarding" "${params[@]}") + + if echo "$response" | grep -q 'IsSuccess="true"'; then + print_success "Email forwarding updated for ${domain}!" + else + print_error "Failed to set email forwarding." + echo "$response" + fi +} + +cmd_ns_create() { + local domain="" nameserver="" ip="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --domain) domain="$2"; shift 2 ;; + --nameserver) nameserver="$2"; shift 2 ;; + --ip) ip="$2"; shift 2 ;; + *) shift ;; + esac + done + + if [[ -z "$domain" || -z "$nameserver" || -z "$ip" ]]; then + print_error "Missing required parameters." + echo "Usage: ./namecheap.sh domains.ns.create --domain example.com --nameserver ns1.example.com --ip 1.2.3.4" + exit 1 + fi + + local sld tld + read -r sld tld <<< "$(parse_domain "$domain")" + + print_info "Creating nameserver ${nameserver} -> ${ip}..." + local response + response=$(api_request "domains.ns.create" "SLD=${sld}" "TLD=${tld}" "Nameserver=${nameserver}" "IP=${ip}") + + if echo "$response" | grep -q 'IsSuccess="true"'; then + print_success "Nameserver ${nameserver} created!" + else + print_error "Failed to create nameserver." + echo "$response" + fi +} + +cmd_ns_delete() { + local domain="" nameserver="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --domain) domain="$2"; shift 2 ;; + --nameserver) nameserver="$2"; shift 2 ;; + *) shift ;; + esac + done + + if [[ -z "$domain" || -z "$nameserver" ]]; then + print_error "Missing required parameters." + echo "Usage: ./namecheap.sh domains.ns.delete --domain example.com --nameserver ns1.example.com" + exit 1 + fi + + local sld tld + read -r sld tld <<< "$(parse_domain "$domain")" + + print_info "Deleting nameserver ${nameserver}..." + local response + response=$(api_request "domains.ns.delete" "SLD=${sld}" "TLD=${tld}" "Nameserver=${nameserver}") + + if echo "$response" | grep -q 'IsSuccess="true"'; then + print_success "Nameserver ${nameserver} deleted!" + else + print_error "Failed to delete nameserver." + echo "$response" + fi +} + +cmd_ns_get_info() { + local domain="" nameserver="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --domain) domain="$2"; shift 2 ;; + --nameserver) nameserver="$2"; shift 2 ;; + *) shift ;; + esac + done + + if [[ -z "$domain" || -z "$nameserver" ]]; then + print_error "Missing required parameters." + echo "Usage: ./namecheap.sh domains.ns.getInfo --domain example.com --nameserver ns1.example.com" + exit 1 + fi + + local sld tld + read -r sld tld <<< "$(parse_domain "$domain")" + + print_info "Fetching info for nameserver ${nameserver}..." + local response + response=$(api_request "domains.ns.getInfo" "SLD=${sld}" "TLD=${tld}" "Nameserver=${nameserver}") + + local ns_ip + ns_ip=$(echo "$response" | grep -oP 'IP="\K[^"]+' || echo "unknown") + echo "" + echo "Nameserver: ${nameserver}" + echo "IP Address: ${ns_ip}" + local statuses + statuses=$(echo "$response" | grep -oP '\K[^<]+' | tr '\n' ', ' | sed 's/,$//') + if [[ -n "$statuses" ]]; then + echo "Status: ${statuses}" + fi + echo "" +} + +cmd_ns_update() { + local domain="" nameserver="" old_ip="" new_ip="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --domain) domain="$2"; shift 2 ;; + --nameserver) nameserver="$2"; shift 2 ;; + --old-ip) old_ip="$2"; shift 2 ;; + --ip) new_ip="$2"; shift 2 ;; + *) shift ;; + esac + done + + if [[ -z "$domain" || -z "$nameserver" || -z "$old_ip" || -z "$new_ip" ]]; then + print_error "Missing required parameters." + echo "Usage: ./namecheap.sh domains.ns.update --domain example.com --nameserver ns1.example.com --old-ip 1.2.3.4 --ip 5.6.7.8" + exit 1 + fi + + local sld tld + read -r sld tld <<< "$(parse_domain "$domain")" + + print_info "Updating nameserver ${nameserver}: ${old_ip} -> ${new_ip}..." + local response + response=$(api_request "domains.ns.update" "SLD=${sld}" "TLD=${tld}" "Nameserver=${nameserver}" "OldIP=${old_ip}" "IP=${new_ip}") + + if echo "$response" | grep -q 'IsSuccess="true"'; then + print_success "Nameserver ${nameserver} updated to ${new_ip}!" + else + print_error "Failed to update nameserver." + echo "$response" + fi +} + +# Help +cmd_help() { + echo "Namecheap DNS Management CLI" + echo "" + echo "Usage: ./namecheap.sh [options]" + echo "" + echo "Commands:" + echo " setup Configure API credentials and test connection" + echo " public-ip Show your public IP address" + echo "" + echo " domains.getList List your Namecheap domains" + echo "" + echo " domains.dns.getList Get nameservers for a domain" + echo " domains.dns.getHosts Get DNS records for a domain" + echo " domains.dns.setHosts Set all DNS records (from JSON file)" + echo " domains.dns.setDefault Use Namecheap default DNS" + echo " domains.dns.setCustom Use custom nameservers" + echo " domains.dns.getEmailForwarding Get email forwarding rules" + echo " domains.dns.setEmailForwarding Set email forwarding rules" + echo "" + echo " domains.ns.create Create a child nameserver (glue record)" + echo " domains.ns.delete Delete a child nameserver" + echo " domains.ns.getInfo Get nameserver info" + echo " domains.ns.update Update nameserver IP" + echo "" + echo " dns.addHost Add a single DNS record (preserves existing)" + echo " dns.removeHost Remove a single DNS record" + echo "" + echo "Options:" + echo " --domain Domain name (e.g., example.com)" + echo " --type Record type (A, AAAA, CNAME, MX, TXT, etc.)" + echo " --name Host name (e.g., @, www, mail)" + echo " --address Record value (IP or target)" + echo " --ttl TTL in seconds (default: 1800)" + echo " --mxpref MX preference (for MX records)" + echo " --hosts JSON file with host records" + echo " --nameservers Comma-separated nameservers" + echo " --nameserver Nameserver hostname" + echo " --ip
IP address for nameserver" + echo " --old-ip
Current IP (for ns.update)" + echo " --mailbox Email mailbox name" + echo " --forward-to Forward destination email" + echo " --forwards JSON file with forwarding rules" + echo " --search Search term for domain list" + echo " --page Page number for domain list" + echo " --page-size Page size for domain list (10-100)" + echo "" + echo "Examples:" + echo " ./namecheap.sh setup" + echo " ./namecheap.sh domains.getList" + echo " ./namecheap.sh domains.dns.getHosts --domain example.com" + echo " ./namecheap.sh dns.addHost --domain example.com --type A --name www --address 1.2.3.4" + echo " ./namecheap.sh dns.removeHost --domain example.com --type A --name www" + echo " ./namecheap.sh domains.dns.setCustom --domain example.com --nameservers ns1.cloudflare.com,ns2.cloudflare.com" + echo " ./namecheap.sh domains.dns.setEmailForwarding --domain example.com --mailbox info --forward-to user@gmail.com" + echo " ./namecheap.sh domains.ns.create --domain example.com --nameserver ns1.example.com --ip 1.2.3.4" +} + +# Main dispatch +main() { + local command="${1:-help}" + shift || true + + case "$command" in + setup) cmd_setup "$@" ;; + public-ip) cmd_public_ip "$@" ;; + domains.getList) cmd_domains_list "$@" ;; + domains.dns.getList) cmd_dns_get_list "$@" ;; + domains.dns.getHosts) cmd_dns_get_hosts "$@" ;; + domains.dns.setHosts) cmd_dns_set_hosts "$@" ;; + domains.dns.setDefault) cmd_dns_set_default "$@" ;; + domains.dns.setCustom) cmd_dns_set_custom "$@" ;; + domains.dns.getEmailForwarding) cmd_dns_get_email_forwarding "$@" ;; + domains.dns.setEmailForwarding) cmd_dns_set_email_forwarding "$@" ;; + domains.ns.create) cmd_ns_create "$@" ;; + domains.ns.delete) cmd_ns_delete "$@" ;; + domains.ns.getInfo) cmd_ns_get_info "$@" ;; + domains.ns.update) cmd_ns_update "$@" ;; + dns.addHost) cmd_dns_add_host "$@" ;; + dns.removeHost) cmd_dns_remove_host "$@" ;; + help|--help|-h) cmd_help ;; + *) + print_error "Unknown command: ${command}" + echo "" + cmd_help + exit 1 + ;; + esac +} + +main "$@" diff --git a/skills/namecheap/references/namecheap-api.md b/skills/namecheap/references/namecheap-api.md new file mode 100644 index 000000000..312121f06 --- /dev/null +++ b/skills/namecheap/references/namecheap-api.md @@ -0,0 +1,392 @@ +# Namecheap API Reference + +## Base URL + +``` +https://api.namecheap.com/xml.response +``` + +## Authentication + +All requests require these common parameters: + +| Parameter | Description | +|-----------|-------------| +| `ApiUser` | Namecheap username | +| `ApiKey` | API key from https://ap.www.namecheap.com/settings/tools/apiaccess/ | +| `UserName` | Same as ApiUser | +| `ClientIp` | The whitelisted public IP address of the client | +| `Command` | The API command prefixed with `namecheap.` | + +## Setup Requirements + +1. Log in to Namecheap +2. Go to https://ap.www.namecheap.com/settings/tools/apiaccess/ +3. Enable API Access (toggle to ON) +4. Add the client's public IP address to the whitelist +5. Copy the generated API key + +## Commands + +--- + +### namecheap.domains.getList + +Lists all domains in the account. + +**Additional Parameters:** + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `ListType` | No | `ALL` (default), `EXPIRING`, or `EXPIRED` | +| `SearchTerm` | No | Keyword to filter domains | +| `Page` | No | Page number (default: 1) | +| `PageSize` | No | Results per page, 10-100 (default: 20) | +| `SortBy` | No | `NAME`, `NAME_DESC`, `EXPIREDATE`, `EXPIREDATE_DESC`, `CREATEDATE`, `CREATEDATE_DESC` | + +**Response XML:** + +```xml + + + + + + 5120 + + +``` + +--- + +### namecheap.domains.dns.getList + +Gets the list of DNS servers associated with a domain (shows whether it uses Namecheap DNS or custom nameservers). + +**Additional Parameters:** + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `SLD` | Yes | Second-level domain (e.g., `example` for `example.com`) | +| `TLD` | Yes | Top-level domain (e.g., `com` for `example.com`) | + +**Response XML:** + +```xml + + + + dns1.registrar-servers.com + dns2.registrar-servers.com + + + +``` + +--- + +### namecheap.domains.dns.getHosts + +Gets DNS host records for a domain. + +**Additional Parameters:** + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `SLD` | Yes | Second-level domain (e.g., `example` for `example.com`) | +| `TLD` | Yes | Top-level domain (e.g., `com` for `example.com`) | + +**Response XML:** + +```xml + + + + + + + + + + +``` + +--- + +### namecheap.domains.dns.setHosts + +Sets (replaces) all DNS host records for a domain. + +**IMPORTANT:** This command replaces ALL existing records. Always fetch existing records first. + +**Additional Parameters:** + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `SLD` | Yes | Second-level domain | +| `TLD` | Yes | Top-level domain | +| `HostNameN` | Yes | Host name for record N (e.g., `@`, `www`, `mail`) | +| `RecordTypeN` | Yes | Record type for record N (A, AAAA, CNAME, MX, TXT, etc.) | +| `AddressN` | Yes | Value for record N (IP address or target hostname) | +| `MXPrefN` | No | MX priority for record N (required for MX records) | +| `TTLN` | No | TTL in seconds for record N (default: 1800) | + +Records are numbered starting from 1: `HostName1`, `RecordType1`, `Address1`, `HostName2`, `RecordType2`, `Address2`, etc. + +**Response XML:** + +```xml + + + + + +``` + +--- + +### namecheap.domains.dns.setDefault + +Sets a domain to use Namecheap's default DNS servers. + +**Additional Parameters:** + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `SLD` | Yes | Second-level domain | +| `TLD` | Yes | Top-level domain | + +**Response XML:** + +```xml + + + + + +``` + +--- + +### namecheap.domains.dns.setCustom + +Sets a domain to use custom nameservers (e.g., Cloudflare, Route53). + +**Additional Parameters:** + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `SLD` | Yes | Second-level domain | +| `TLD` | Yes | Top-level domain | +| `Nameservers` | Yes | Comma-separated list of nameservers (max 12, no spaces) | + +**Example:** `Nameservers=ns1.cloudflare.com,ns2.cloudflare.com` + +**Response XML:** + +```xml + + + + + +``` + +--- + +### namecheap.domains.dns.getEmailForwarding + +Gets email forwarding settings for a domain. + +**Additional Parameters:** + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `DomainName` | Yes | Full domain name (e.g., `example.com`) | + +**Response XML:** + +```xml + + + + + + + + +``` + +--- + +### namecheap.domains.dns.setEmailForwarding + +Sets email forwarding for a domain. Replaces all existing forwarding rules. + +**Additional Parameters:** + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `DomainName` | Yes | Full domain name (e.g., `example.com`) | +| `MailBoxN` | Yes | Mailbox name for rule N (e.g., `info`, `support`) | +| `ForwardToN` | Yes | Destination email for rule N | + +Rules are numbered starting from 1: `MailBox1`, `ForwardTo1`, `MailBox2`, `ForwardTo2`, etc. +Omitting all MailBox/ForwardTo parameters deletes all forwarding rules. + +**Response XML:** + +```xml + + + + + +``` + +--- + +### namecheap.domains.ns.create + +Creates a child nameserver (glue record) for a domain. + +**Additional Parameters:** + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `SLD` | Yes | Second-level domain | +| `TLD` | Yes | Top-level domain | +| `Nameserver` | Yes | Nameserver hostname to create (e.g., `ns1.example.com`) | +| `IP` | Yes | IP address for the nameserver | + +**Response XML:** + +```xml + + + + + +``` + +--- + +### namecheap.domains.ns.delete + +Deletes a child nameserver. + +**Additional Parameters:** + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `SLD` | Yes | Second-level domain | +| `TLD` | Yes | Top-level domain | +| `Nameserver` | Yes | Nameserver hostname to delete | + +**Response XML:** + +```xml + + + + + +``` + +--- + +### namecheap.domains.ns.getInfo + +Gets information about a child nameserver. + +**Additional Parameters:** + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `SLD` | Yes | Second-level domain | +| `TLD` | Yes | Top-level domain | +| `Nameserver` | Yes | Nameserver hostname to query | + +**Response XML:** + +```xml + + + + + OK + + + + +``` + +--- + +### namecheap.domains.ns.update + +Updates the IP address of a child nameserver. + +**Additional Parameters:** + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `SLD` | Yes | Second-level domain | +| `TLD` | Yes | Top-level domain | +| `Nameserver` | Yes | Nameserver hostname to update | +| `OldIP` | Yes | Current IP address of the nameserver | +| `IP` | Yes | New IP address for the nameserver | + +**Response XML:** + +```xml + + + + + +``` + +## Error Responses + +```xml + + + Domain not found + + +``` + +Common error codes: +- `1011102` — Invalid API key +- `1011148` — IP not whitelisted +- `2019166` — Domain not found +- `2016166` — Domain not using Namecheap DNS + +## Record Types + +| Type | Description | Address Format | +|------|-------------|---------------| +| `A` | IPv4 address | `1.2.3.4` | +| `AAAA` | IPv6 address | `2001:db8::1` | +| `CNAME` | Canonical name | `target.example.com.` | +| `MX` | Mail exchange | `mail.example.com.` (requires MXPref) | +| `MXE` | MX equivalent (IP) | `1.2.3.4` | +| `TXT` | Text record | Any text value | +| `URL` | URL redirect (unmasked) | `http://example.com` | +| `URL301` | Permanent redirect | `http://example.com` | +| `FRAME` | URL redirect (masked) | `http://example.com` | + +## TTL Values + +| Seconds | Human Readable | +|---------|---------------| +| 60 | 1 minute | +| 300 | 5 minutes | +| 1800 | 30 minutes (default) | +| 3600 | 1 hour | +| 14400 | 4 hours | +| 43200 | 12 hours | +| 86400 | 1 day | From 65c205fb961e2f3c237b9e9728ec8b64e5f513b5 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Fri, 29 May 2026 13:57:49 -0400 Subject: [PATCH 2/4] Skill for Namecheap DNS Management API --- docs/README.skills.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/README.skills.md b/docs/README.skills.md index d7e9a764b..82cb05f26 100644 --- a/docs/README.skills.md +++ b/docs/README.skills.md @@ -245,6 +245,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to | [mvvm-toolkit](../skills/mvvm-toolkit/SKILL.md)
`gh skills install github/awesome-copilot mvvm-toolkit` | CommunityToolkit.Mvvm (the MVVM Toolkit) core: source generators ([ObservableProperty], [RelayCommand], [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyDataErrorInfo]), base classes (ObservableObject / ObservableValidator / ObservableRecipient), commands (RelayCommand / AsyncRelayCommand), and validation. Companion skills: mvvm-toolkit-messenger for pub/sub, mvvm-toolkit-di for Microsoft.Extensions.DependencyInjection wiring. Works across WPF, WinUI 3, MAUI, Uno, and Avalonia. | `references/end-to-end-walkthrough.md`
`references/relaycommand-cookbook.md`
`references/source-generators.md`
`references/troubleshooting.md`
`references/validation.md` | | [mvvm-toolkit-di](../skills/mvvm-toolkit-di/SKILL.md)
`gh skills install github/awesome-copilot mvvm-toolkit-di` | Wire CommunityToolkit.Mvvm ViewModels into Microsoft.Extensions.DependencyInjection. Covers the .NET Generic Host composition root, constructor injection, service lifetimes (Singleton / Transient / Scoped), IMessenger registration, resolving ViewModels in Views, keyed services, testing seams, and the legacy Ioc.Default escape hatch. Use across WPF, WinUI 3, .NET MAUI, Uno, and Avalonia. | `references/dependency-injection.md` | | [mvvm-toolkit-messenger](../skills/mvvm-toolkit-messenger/SKILL.md)
`gh skills install github/awesome-copilot mvvm-toolkit-messenger` | CommunityToolkit.Mvvm Messenger pub/sub for decoupled communication between ViewModels (or any objects). Covers WeakReferenceMessenger vs StrongReferenceMessenger, IRecipient, RequestMessage / AsyncRequestMessage / CollectionRequestMessage, ValueChangedMessage, channels (tokens), and the ObservableRecipient activation lifecycle. Use across WPF, WinUI 3, .NET MAUI, Uno, and Avalonia. | `references/messenger-patterns.md` | +| [namecheap](../skills/namecheap/SKILL.md)
`gh skills install github/awesome-copilot namecheap` | Manage DNS records for domains registered with Namecheap via their API. List domains, view/add/update/remove DNS host entries (A, AAAA, CNAME, MX, TXT, etc.), and guide users through API setup including public IP detection and credential configuration. Use when the user mentions Namecheap, DNS records, domain management, or wants to add/change/remove A records, CNAME records, MX records, or TXT records for their domains. | `namecheap.sh`
`references/namecheap-api.md` | | [nano-banana-pro-openrouter](../skills/nano-banana-pro-openrouter/SKILL.md)
`gh skills install github/awesome-copilot nano-banana-pro-openrouter` | Generate or edit images via OpenRouter with the Gemini 3 Pro Image model. Use for prompt-only image generation, image edits, and multi-image compositing; supports 1K/2K/4K output. | `assets/SYSTEM_TEMPLATE`
`scripts/generate_image.py` | | [napkin](../skills/napkin/SKILL.md)
`gh skills install github/awesome-copilot napkin` | Visual whiteboard collaboration for Copilot CLI. Creates an interactive whiteboard that opens in your browser — draw, sketch, add sticky notes, then share everything back with Copilot. Copilot sees your drawings and text, and responds with analysis, suggestions, and ideas. | `assets/napkin.html`
`assets/step1-activate.svg`
`assets/step2-whiteboard.svg`
`assets/step3-draw.svg`
`assets/step4-share.svg`
`assets/step5-response.svg` | | [next-intl-add-language](../skills/next-intl-add-language/SKILL.md)
`gh skills install github/awesome-copilot next-intl-add-language` | Add new language to a Next.js + next-intl application | None | From b839f3e71b0d7885974d6c8148f78a1ede011efd Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Fri, 29 May 2026 15:23:06 -0400 Subject: [PATCH 3/4] Rewrite namecheap skill in Python to address PR review Replace the Bash namecheap.sh with a stdlib-only Python CLI (namecheap.py) that resolves all Copilot PR review feedback: - Cache the public IP once per run instead of per request - Build API requests in-process via urllib so the API key never appears in process argv or shell history - Broaden multi-part TLD detection (co.uk, com.au, etc.) and document the limitation - Allow setup to update existing stored credentials - Stop soliciting the API key via chat; use terminal getpass or env vars - Remove non-portable Bash constructs (inline local, grep -oP) Also normalize domain casing, preserve MX priority 0, accept case-insensitive record types, and handle mixed-case email-forwarding attributes. Update SKILL.md and regenerate the skills README. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/README.skills.md | 2 +- skills/namecheap/SKILL.md | 47 +- skills/namecheap/namecheap.py | 697 ++++++++++++++++++++++++++ skills/namecheap/namecheap.sh | 919 ---------------------------------- 4 files changed, 721 insertions(+), 944 deletions(-) create mode 100644 skills/namecheap/namecheap.py delete mode 100755 skills/namecheap/namecheap.sh diff --git a/docs/README.skills.md b/docs/README.skills.md index 82cb05f26..9c448fc5c 100644 --- a/docs/README.skills.md +++ b/docs/README.skills.md @@ -245,7 +245,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to | [mvvm-toolkit](../skills/mvvm-toolkit/SKILL.md)
`gh skills install github/awesome-copilot mvvm-toolkit` | CommunityToolkit.Mvvm (the MVVM Toolkit) core: source generators ([ObservableProperty], [RelayCommand], [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyDataErrorInfo]), base classes (ObservableObject / ObservableValidator / ObservableRecipient), commands (RelayCommand / AsyncRelayCommand), and validation. Companion skills: mvvm-toolkit-messenger for pub/sub, mvvm-toolkit-di for Microsoft.Extensions.DependencyInjection wiring. Works across WPF, WinUI 3, MAUI, Uno, and Avalonia. | `references/end-to-end-walkthrough.md`
`references/relaycommand-cookbook.md`
`references/source-generators.md`
`references/troubleshooting.md`
`references/validation.md` | | [mvvm-toolkit-di](../skills/mvvm-toolkit-di/SKILL.md)
`gh skills install github/awesome-copilot mvvm-toolkit-di` | Wire CommunityToolkit.Mvvm ViewModels into Microsoft.Extensions.DependencyInjection. Covers the .NET Generic Host composition root, constructor injection, service lifetimes (Singleton / Transient / Scoped), IMessenger registration, resolving ViewModels in Views, keyed services, testing seams, and the legacy Ioc.Default escape hatch. Use across WPF, WinUI 3, .NET MAUI, Uno, and Avalonia. | `references/dependency-injection.md` | | [mvvm-toolkit-messenger](../skills/mvvm-toolkit-messenger/SKILL.md)
`gh skills install github/awesome-copilot mvvm-toolkit-messenger` | CommunityToolkit.Mvvm Messenger pub/sub for decoupled communication between ViewModels (or any objects). Covers WeakReferenceMessenger vs StrongReferenceMessenger, IRecipient, RequestMessage / AsyncRequestMessage / CollectionRequestMessage, ValueChangedMessage, channels (tokens), and the ObservableRecipient activation lifecycle. Use across WPF, WinUI 3, .NET MAUI, Uno, and Avalonia. | `references/messenger-patterns.md` | -| [namecheap](../skills/namecheap/SKILL.md)
`gh skills install github/awesome-copilot namecheap` | Manage DNS records for domains registered with Namecheap via their API. List domains, view/add/update/remove DNS host entries (A, AAAA, CNAME, MX, TXT, etc.), and guide users through API setup including public IP detection and credential configuration. Use when the user mentions Namecheap, DNS records, domain management, or wants to add/change/remove A records, CNAME records, MX records, or TXT records for their domains. | `namecheap.sh`
`references/namecheap-api.md` | +| [namecheap](../skills/namecheap/SKILL.md)
`gh skills install github/awesome-copilot namecheap` | Manage DNS records for domains registered with Namecheap via their API. List domains, view/add/update/remove DNS host entries (A, AAAA, CNAME, MX, TXT, etc.), and guide users through API setup including public IP detection and credential configuration. Use when the user mentions Namecheap, DNS records, domain management, or wants to add/change/remove A records, CNAME records, MX records, or TXT records for their domains. | `namecheap.py`
`references/namecheap-api.md` | | [nano-banana-pro-openrouter](../skills/nano-banana-pro-openrouter/SKILL.md)
`gh skills install github/awesome-copilot nano-banana-pro-openrouter` | Generate or edit images via OpenRouter with the Gemini 3 Pro Image model. Use for prompt-only image generation, image edits, and multi-image compositing; supports 1K/2K/4K output. | `assets/SYSTEM_TEMPLATE`
`scripts/generate_image.py` | | [napkin](../skills/napkin/SKILL.md)
`gh skills install github/awesome-copilot napkin` | Visual whiteboard collaboration for Copilot CLI. Creates an interactive whiteboard that opens in your browser — draw, sketch, add sticky notes, then share everything back with Copilot. Copilot sees your drawings and text, and responds with analysis, suggestions, and ideas. | `assets/napkin.html`
`assets/step1-activate.svg`
`assets/step2-whiteboard.svg`
`assets/step3-draw.svg`
`assets/step4-share.svg`
`assets/step5-response.svg` | | [next-intl-add-language](../skills/next-intl-add-language/SKILL.md)
`gh skills install github/awesome-copilot next-intl-add-language` | Add new language to a Next.js + next-intl application | None | diff --git a/skills/namecheap/SKILL.md b/skills/namecheap/SKILL.md index 426b3b484..54669247d 100644 --- a/skills/namecheap/SKILL.md +++ b/skills/namecheap/SKILL.md @@ -17,67 +17,66 @@ Before executing any API commands, verify credentials are configured: 1. **Check for existing config** — look for `~/.namecheap-api` 2. If not configured, guide the user through setup: - a. **Show public IP** — run `curl -s https://api.ipify.org` to display the user's public IP + a. **Show public IP** — run `python3 namecheap.py public-ip` to display the user's public IP b. **Instruct IP whitelisting** — tell the user to go to https://ap.www.namecheap.com/settings/tools/apiaccess/, enable API (select ON), and whitelist the displayed IP - c. **Collect credentials** — use `ask_user` to get their Namecheap username, then their API key - d. **Save config** — write credentials to `~/.namecheap-api` with `chmod 600` - e. **Validate** — run a test API call to confirm access works + c. **Have the user run setup themselves** — ask the user to run `python3 namecheap.py setup` directly **in their own terminal**. The script prompts for the username and reads the API key with a hidden prompt (`getpass`), writes `~/.namecheap-api` with `chmod 600`, and validates the connection. **Never ask the user to paste their API key into the chat, and never log, echo, or display the API key value.** If you cannot run an interactive terminal for the user, instruct them to run `setup` themselves, or to export `NAMECHEAP_API_USER` and `NAMECHEAP_API_KEY` as environment variables in their own shell — rather than collecting the secret via `ask_user`. + d. **Confirm** — once the user reports setup succeeded, proceed with DNS operations. ### DNS Operations -Use the `namecheap.sh` script (bundled in this skill's directory) for all API interactions: +Use the `namecheap.py` script (bundled in this skill's directory) for all API interactions. It requires only Python 3 (standard library only — no `pip install` needed) and works the same on macOS, Linux, and Windows: ```bash # Show public IP (for setup) -bash namecheap.sh public-ip +python3 namecheap.py public-ip # Run setup flow -bash namecheap.sh setup +python3 namecheap.py setup # List domains -bash namecheap.sh domains.getList +python3 namecheap.py domains.getList # Get nameservers for a domain (shows if using Namecheap DNS or custom) -bash namecheap.sh domains.dns.getList --domain example.com +python3 namecheap.py domains.dns.getList --domain example.com # Get DNS records for a domain -bash namecheap.sh domains.dns.getHosts --domain example.com +python3 namecheap.py domains.dns.getHosts --domain example.com # Add a single record (preserves existing records) -bash namecheap.sh dns.addHost --domain example.com --type A --name www --address 1.2.3.4 --ttl 1800 +python3 namecheap.py dns.addHost --domain example.com --type A --name www --address 1.2.3.4 --ttl 1800 # Remove a single record -bash namecheap.sh dns.removeHost --domain example.com --type A --name www --address 1.2.3.4 +python3 namecheap.py dns.removeHost --domain example.com --type A --name www --address 1.2.3.4 # Replace all records from a JSON file -bash namecheap.sh domains.dns.setHosts --domain example.com --hosts records.json +python3 namecheap.py domains.dns.setHosts --domain example.com --hosts records.json # Switch to Namecheap default DNS -bash namecheap.sh domains.dns.setDefault --domain example.com +python3 namecheap.py domains.dns.setDefault --domain example.com # Switch to custom nameservers -bash namecheap.sh domains.dns.setCustom --domain example.com --nameservers ns1.cloudflare.com,ns2.cloudflare.com +python3 namecheap.py domains.dns.setCustom --domain example.com --nameservers ns1.cloudflare.com,ns2.cloudflare.com # Get email forwarding rules -bash namecheap.sh domains.dns.getEmailForwarding --domain example.com +python3 namecheap.py domains.dns.getEmailForwarding --domain example.com # Set email forwarding (single rule) -bash namecheap.sh domains.dns.setEmailForwarding --domain example.com --mailbox info --forward-to user@gmail.com +python3 namecheap.py domains.dns.setEmailForwarding --domain example.com --mailbox info --forward-to user@gmail.com # Set email forwarding (from JSON file) -bash namecheap.sh domains.dns.setEmailForwarding --domain example.com --forwards forwards.json +python3 namecheap.py domains.dns.setEmailForwarding --domain example.com --forwards forwards.json # Create a child nameserver (glue record) -bash namecheap.sh domains.ns.create --domain example.com --nameserver ns1.example.com --ip 1.2.3.4 +python3 namecheap.py domains.ns.create --domain example.com --nameserver ns1.example.com --ip 1.2.3.4 # Delete a child nameserver -bash namecheap.sh domains.ns.delete --domain example.com --nameserver ns1.example.com +python3 namecheap.py domains.ns.delete --domain example.com --nameserver ns1.example.com # Get nameserver info -bash namecheap.sh domains.ns.getInfo --domain example.com --nameserver ns1.example.com +python3 namecheap.py domains.ns.getInfo --domain example.com --nameserver ns1.example.com # Update nameserver IP -bash namecheap.sh domains.ns.update --domain example.com --nameserver ns1.example.com --old-ip 1.2.3.4 --ip 5.6.7.8 +python3 namecheap.py domains.ns.update --domain example.com --nameserver ns1.example.com --old-ip 1.2.3.4 --ip 5.6.7.8 ``` ## Behavior @@ -87,7 +86,7 @@ bash namecheap.sh domains.ns.update --domain example.com --nameserver ns1.exampl - **Use `ask_user` to confirm destructive changes.** Before removing records or replacing all records with `setHosts`, confirm with the user. - **The Namecheap `setHosts` API replaces ALL records.** Never call `domains.dns.setHosts` directly unless you have fetched all existing records first. Use `dns.addHost` and `dns.removeHost` for safe single-record operations — they handle the fetch-modify-write cycle internally. - **Explain TTL in human terms.** When the user asks about TTL, explain that 1800 = 30 minutes, 3600 = 1 hour, etc. -- **Handle multi-part TLDs.** Domains like `example.co.uk` have SLD=example and TLD=co.uk. The script handles this automatically. +- **Handle multi-part TLDs.** Domains like `example.co.uk` have SLD=example and TLD=co.uk. The script recognizes a built-in list of common second-level suffixes (e.g. `co.uk`, `com.au`, `co.jp`, `com.br`). This list is best-effort and not a full public-suffix database — if a domain with an unlisted multi-part suffix returns a `2019166` ("Domain not found") error, the SLD/TLD split was likely wrong. In that case, confirm the registered domain with the user and report the limitation. ## Credential Storage @@ -98,7 +97,7 @@ NAMECHEAP_API_USER="username" NAMECHEAP_API_KEY="api-key-here" ``` -This file must have `600` permissions (owner read/write only). +This file must have `600` permissions (owner read/write only). Alternatively, the script reads credentials from the `NAMECHEAP_API_USER` and `NAMECHEAP_API_KEY` environment variables, which take precedence over the file when both are set. ## Supported Record Types diff --git a/skills/namecheap/namecheap.py b/skills/namecheap/namecheap.py new file mode 100644 index 000000000..bb7d1dffd --- /dev/null +++ b/skills/namecheap/namecheap.py @@ -0,0 +1,697 @@ +#!/usr/bin/env python3 +"""Namecheap API CLI wrapper for DNS management. + +Uses only the Python standard library (no third-party dependencies). Credentials +are read from ``~/.namecheap-api`` (or env vars) and are never passed on the +command line, so they cannot leak via ``ps``/shell history. +""" + +import argparse +import getpass +import json +import os +import re +import stat +import sys +import urllib.error +import urllib.parse +import urllib.request +import xml.etree.ElementTree as ET + +API_URL = "https://api.namecheap.com/xml.response" +CONFIG_FILE = os.path.join(os.path.expanduser("~"), ".namecheap-api") + +# Known multi-part (second-level) public suffixes. Best-effort list, not a full +# public-suffix database. For unlisted suffixes the domain is split on the last +# dot, which is correct for single-label TLDs (.com, .io, .dev, ...). +MULTI_PART_SUFFIXES = { + "co.uk", "org.uk", "me.uk", "ac.uk", "gov.uk", "net.uk", "ltd.uk", "plc.uk", + "com.au", "net.au", "org.au", "id.au", + "co.nz", "net.nz", "org.nz", + "co.za", "org.za", + "co.jp", "ne.jp", "or.jp", "ac.jp", "go.jp", + "co.kr", "or.kr", "ne.kr", + "com.br", "net.br", "org.br", + "com.cn", "net.cn", "org.cn", + "co.in", "net.in", "org.in", + "com.mx", "org.mx", + "com.sg", "edu.sg", + "com.tr", + "co.il", "org.il", +} + +# Cached public IP for the lifetime of the process. +_public_ip = None + + +# --- Output helpers ------------------------------------------------------- + +_USE_COLOR = sys.stdout.isatty() + + +def _c(code, text): + return f"\033[{code}m{text}\033[0m" if _USE_COLOR else text + + +def err(msg): + print(_c("0;31", "Error:") + " " + msg, file=sys.stderr) + + +def success(msg): + print(_c("0;32", "\u2713") + " " + msg) + + +def info(msg): + print(_c("0;36", "\u2139") + " " + msg) + + +def warn(msg): + print(_c("1;33", "\u26a0") + " " + msg) + + +class NamecheapError(Exception): + """Raised when the API returns a Status="ERROR" response.""" + + +# --- Configuration -------------------------------------------------------- + +def load_config(): + """Return (api_user, api_key), preferring env vars then the config file.""" + api_user = os.environ.get("NAMECHEAP_API_USER") + api_key = os.environ.get("NAMECHEAP_API_KEY") + if api_user and api_key: + return api_user, api_key + + if os.path.isfile(CONFIG_FILE): + with open(CONFIG_FILE, "r", encoding="utf-8") as fh: + content = fh.read() + # File uses shell-style KEY="value" lines for backward compatibility. + pattern = re.compile(r'^\s*([A-Z_]+)\s*=\s*"?([^"\n]*)"?\s*$', re.MULTILINE) + values = {m.group(1): m.group(2) for m in pattern.finditer(content)} + api_user = api_user or values.get("NAMECHEAP_API_USER") + api_key = api_key or values.get("NAMECHEAP_API_KEY") + + return api_user, api_key + + +def check_credentials(): + api_user, api_key = load_config() + if not api_user or not api_key: + err("Namecheap API credentials not configured.") + print() + print("Run 'python3 namecheap.py setup' to configure your credentials.") + print() + print("You need:") + print(" 1. Your Namecheap username") + print(" 2. An API key from: https://ap.www.namecheap.com/settings/tools/apiaccess/") + print(" 3. Your public IP whitelisted in the API settings") + sys.exit(1) + return api_user, api_key + + +def save_config(api_user, api_key): + with open(CONFIG_FILE, "w", encoding="utf-8") as fh: + fh.write(f'NAMECHEAP_API_USER="{api_user}"\n') + fh.write(f'NAMECHEAP_API_KEY="{api_key}"\n') + os.chmod(CONFIG_FILE, stat.S_IRUSR | stat.S_IWUSR) # 600 + + +# --- Networking ----------------------------------------------------------- + +def _http_get(url, timeout=15): + req = urllib.request.Request(url, headers={"User-Agent": "namecheap-skill/1.0"}) + with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310 (https only) + return resp.read().decode("utf-8", errors="replace") + + +def get_public_ip(): + """Resolve the public IP once and cache it for subsequent calls.""" + global _public_ip + if _public_ip: + return _public_ip + for url in ("https://api.ipify.org", "https://ifconfig.me"): + try: + ip = _http_get(url, timeout=10).strip() + if ip: + _public_ip = ip + return ip + except Exception: + continue + _public_ip = "unknown" + return _public_ip + + +def _strip_namespaces(root): + for elem in root.iter(): + if isinstance(elem.tag, str) and "}" in elem.tag: + elem.tag = elem.tag.split("}", 1)[1] + return root + + +def _check_error(root): + if (root.attrib.get("Status") or "").upper() == "ERROR": + messages = [] + for e in root.iter("Err"): + code = e.attrib.get("Number") or e.attrib.get("Code") or "" + text = (e.text or "").strip() + messages.append(f"[{code}] {text}" if code else text) + raise NamecheapError("; ".join(m for m in messages if m) or "Unknown API error") + + +def api_request(command, params=None): + """Issue a Namecheap API GET request and return the parsed (ns-stripped) root. + + The API key is encoded into the request URL inside this process; it is never + passed as a command-line argument, so it cannot leak via ``ps`` or shell + history. Values are URL-encoded by ``urllib``. + """ + api_user, api_key = check_credentials() + query = { + "ApiUser": api_user, + "ApiKey": api_key, + "UserName": api_user, + "Command": f"namecheap.{command}", + "ClientIp": get_public_ip(), + } + for key, value in (params or {}).items(): + if value is not None and value != "": + query[key] = value + + url = f"{API_URL}?{urllib.parse.urlencode(query)}" + body = _http_get(url, timeout=30) + root = _strip_namespaces(ET.fromstring(body)) + _check_error(root) + return root + + +def _attr(root, tag, name, default=""): + for elem in root.iter(tag): + return elem.attrib.get(name, default) + return default + + +# --- Domain parsing ------------------------------------------------------- + +def parse_domain(domain): + """Split a registered domain into (SLD, TLD), handling multi-part TLDs.""" + domain = domain.strip().rstrip(".").lower() + labels = domain.split(".") + if len(labels) >= 3 and ".".join(labels[-2:]) in MULTI_PART_SUFFIXES: + tld = ".".join(labels[-2:]) + sld = ".".join(labels[:-2]) + elif len(labels) >= 2: + tld = labels[-1] + sld = ".".join(labels[:-1]) + else: + tld = "" + sld = domain + return sld, tld + + +# --- Commands ------------------------------------------------------------- + +def cmd_public_ip(_args): + print(get_public_ip()) + + +def cmd_setup(_args): + print("=== Namecheap API Setup ===\n") + public_ip = get_public_ip() + info("Your public IP address is: " + _c("0;36", public_ip)) + print() + print("Make sure this IP is whitelisted at:") + print(" https://ap.www.namecheap.com/settings/tools/apiaccess/") + print() + + existing_user, existing_key = load_config() + if existing_user and existing_key: + info(f"Existing configuration found for user: {existing_user}") + print("\nTesting API connection...") + try: + api_request("domains.getList", {"PageSize": "1"}) + success("API connection successful!") + except Exception as exc: # noqa: BLE001 + err(f"API connection failed: {exc}") + print() + answer = input("Update stored credentials? [y/N]: ").strip().lower() + if answer not in ("y", "yes"): + info("Keeping existing credentials.") + return + print() + + print("Enter your Namecheap credentials:\n") + api_user = input(" API Username: ").strip() + api_key = getpass.getpass(" API Key (hidden): ").strip() + print() + + if not api_user or not api_key: + err("Both username and API key are required.") + sys.exit(1) + + save_config(api_user, api_key) + success(f"Credentials saved to {CONFIG_FILE}") + print("\nTesting API connection...") + try: + # Use the just-entered credentials directly for the validation call. + os.environ["NAMECHEAP_API_USER"] = api_user + os.environ["NAMECHEAP_API_KEY"] = api_key + api_request("domains.getList", {"PageSize": "1"}) + success("API connection successful!") + except Exception as exc: # noqa: BLE001 + warn("API connection failed. Please verify:") + print(" 1. API access is enabled (ON) at the Namecheap settings page") + print(f" 2. IP address {public_ip} is whitelisted") + print(" 3. Your API key is correct") + print(f" (details: {exc})") + + +def cmd_domains_list(args): + params = {"ListType": args.type, "Page": str(args.page), "PageSize": str(args.page_size)} + if args.search: + params["SearchTerm"] = args.search + info("Fetching domain list...") + root = api_request("domains.getList", params) + + print() + print(f"{'DOMAIN':<30} {'EXPIRES':<12} {'LOCKED':<12} {'AUTO-RENEW':<10}") + print(f"{'------':<30} {'-------':<12} {'------':<12} {'----------':<10}") + for d in root.iter("Domain"): + print("{:<30} {:<12} {:<12} {:<10}".format( + d.attrib.get("Name", ""), + d.attrib.get("Expires", ""), + d.attrib.get("IsLocked", ""), + d.attrib.get("AutoRenew", ""), + )) + print() + + +def _print_hosts(root): + print() + print(f"{'HOST':<20} {'TYPE':<8} {'ADDRESS':<40} {'TTL':<8} {'MXPREF':<6}") + print(f"{'----':<20} {'----':<8} {'-------':<40} {'---':<8} {'------':<6}") + for h in root.iter("host"): + print("{:<20} {:<8} {:<40} {:<8} {:<6}".format( + h.attrib.get("Name", ""), + h.attrib.get("Type", ""), + h.attrib.get("Address", ""), + h.attrib.get("TTL", "1800"), + h.attrib.get("MXPref", "-"), + )) + print() + + +def cmd_dns_get_hosts(args): + sld, tld = parse_domain(args.domain) + info(f"Fetching DNS records for {args.domain} (SLD={sld}, TLD={tld})...") + root = api_request("domains.dns.getHosts", {"SLD": sld, "TLD": tld}) + _print_hosts(root) + + +def _existing_hosts(root): + """Return existing host records as a list of dicts.""" + records = [] + for h in root.iter("host"): + records.append({ + "name": h.attrib.get("Name", ""), + "type": h.attrib.get("Type", ""), + "address": h.attrib.get("Address", ""), + "ttl": h.attrib.get("TTL", "1800"), + "mxpref": h.attrib.get("MXPref", ""), + }) + return records + + +def _hosts_to_params(sld, tld, records): + params = {"SLD": sld, "TLD": tld} + i = 1 + for r in records: + if not (r["name"] and r["type"] and r["address"]): + continue + params[f"HostName{i}"] = r["name"] + params[f"RecordType{i}"] = r["type"] + params[f"Address{i}"] = r["address"] + params[f"TTL{i}"] = r.get("ttl") or "1800" + mxpref = r.get("mxpref") + if mxpref not in (None, ""): + # MX priority 0 is valid, so always send MXPref for MX records; + # for other record types only forward a non-zero value. + if r["type"].upper() == "MX" or mxpref != "0": + params[f"MXPref{i}"] = mxpref + i += 1 + return params, i - 1 + + +def cmd_dns_set_hosts(args): + if not os.path.isfile(args.hosts): + err(f"Hosts file not found: {args.hosts}") + sys.exit(1) + with open(args.hosts, "r", encoding="utf-8") as fh: + raw = json.load(fh) + + records = [] + for r in raw: + records.append({ + "name": r.get("HostName", ""), + "type": r.get("RecordType", ""), + "address": r.get("Address", ""), + "ttl": str(r.get("TTL", "") or ""), + "mxpref": str(r.get("MXPref", "") or ""), + }) + + sld, tld = parse_domain(args.domain) + params, count = _hosts_to_params(sld, tld, records) + if count == 0: + err(f"No valid host records found in {args.hosts}") + sys.exit(1) + + info(f"Setting {count} DNS records for {args.domain}...") + root = api_request("domains.dns.setHosts", params) + if _attr(root, "DomainDNSSetHostsResult", "IsSuccess").lower() == "true": + success(f"DNS records updated successfully for {args.domain}!") + else: + err("Failed to update DNS records.") + sys.exit(1) + + +def cmd_dns_add_host(args): + sld, tld = parse_domain(args.domain) + info(f"Fetching existing DNS records for {args.domain}...") + root = api_request("domains.dns.getHosts", {"SLD": sld, "TLD": tld}) + records = _existing_hosts(root) + records.append({ + "name": args.name, + "type": args.type.upper(), + "address": args.address, + "ttl": args.ttl, + "mxpref": args.mxpref or "", + }) + params, _ = _hosts_to_params(sld, tld, records) + + info(f"Adding {args.type.upper()} record: {args.name} -> {args.address}") + result = api_request("domains.dns.setHosts", params) + if _attr(result, "DomainDNSSetHostsResult", "IsSuccess").lower() == "true": + success(f"DNS record added: {args.name} {args.type} {args.address}") + else: + err("Failed to add DNS record.") + sys.exit(1) + + +def cmd_dns_remove_host(args): + sld, tld = parse_domain(args.domain) + info(f"Fetching existing DNS records for {args.domain}...") + root = api_request("domains.dns.getHosts", {"SLD": sld, "TLD": tld}) + + kept = [] + removed = False + for r in _existing_hosts(root): + if (not removed and r["name"] == args.name + and r["type"].upper() == args.type.upper() + and (not args.address or r["address"] == args.address)): + removed = True + info(f"Removing record: {r['name']} {r['type']} {r['address']}") + continue + kept.append(r) + + if not removed: + err("No matching record found to remove.") + sys.exit(1) + + params, count = _hosts_to_params(sld, tld, kept) + if count == 0: + err("Cannot remove the last DNS record. Namecheap requires at least one record.") + sys.exit(1) + + info(f"Updating DNS records for {args.domain}...") + result = api_request("domains.dns.setHosts", params) + if _attr(result, "DomainDNSSetHostsResult", "IsSuccess").lower() == "true": + success("DNS record removed successfully!") + else: + err("Failed to remove DNS record.") + sys.exit(1) + + +def cmd_dns_get_list(args): + sld, tld = parse_domain(args.domain) + info(f"Fetching nameservers for {args.domain}...") + root = api_request("domains.dns.getList", {"SLD": sld, "TLD": tld}) + + using = _attr(root, "DomainDNSGetListResult", "IsUsingOurDNS", "unknown") + print() + info(f"Using Namecheap DNS: {using}") + print("\nNameservers:") + for ns in root.iter("Nameserver"): + if ns.text: + print(f" - {ns.text.strip()}") + print() + + +def cmd_dns_set_default(args): + sld, tld = parse_domain(args.domain) + info(f"Setting {args.domain} to use Namecheap default DNS...") + root = api_request("domains.dns.setDefault", {"SLD": sld, "TLD": tld}) + if _attr(root, "DomainDNSSetDefaultResult", "Updated").lower() == "true": + success(f"Domain {args.domain} now uses Namecheap default DNS!") + else: + err("Failed to set default DNS.") + sys.exit(1) + + +def cmd_dns_set_custom(args): + sld, tld = parse_domain(args.domain) + info(f"Setting {args.domain} to use custom nameservers: {args.nameservers}") + root = api_request( + "domains.dns.setCustom", + {"SLD": sld, "TLD": tld, "Nameservers": args.nameservers}, + ) + if _attr(root, "DomainDNSSetCustomResult", "Updated").lower() == "true": + success(f"Domain {args.domain} now uses custom nameservers!") + else: + err("Failed to set custom nameservers.") + sys.exit(1) + + +def cmd_dns_get_email_forwarding(args): + info(f"Fetching email forwarding for {args.domain}...") + root = api_request("domains.dns.getEmailForwarding", {"DomainName": args.domain}) + + print() + print(f"{'MAILBOX':<20} {'FORWARDS TO':<40}") + print(f"{'-------':<20} {'-----------':<40}") + for fwd in root.iter("Forward"): + mailbox = (fwd.attrib.get("mailbox") or fwd.attrib.get("MailBox") + or fwd.attrib.get("Mailbox") or "") + forward_to = (fwd.attrib.get("ForwardTo") or fwd.attrib.get("forwardto") + or (fwd.text or "").strip()) + print(f"{mailbox + '@' + args.domain:<20} {forward_to:<40}") + print() + + +def cmd_dns_set_email_forwarding(args): + params = {"DomainName": args.domain} + + if args.mailbox and args.forward_to: + params["MailBox1"] = args.mailbox + params["ForwardTo1"] = args.forward_to + elif args.forwards: + if not os.path.isfile(args.forwards): + err(f"Forwards file not found: {args.forwards}") + sys.exit(1) + with open(args.forwards, "r", encoding="utf-8") as fh: + rules = json.load(fh) + i = 1 + for rule in rules: + mailbox = rule.get("MailBox") or rule.get("mailbox") or "" + forward_to = rule.get("ForwardTo") or rule.get("forwardto") or "" + if mailbox and forward_to: + params[f"MailBox{i}"] = mailbox + params[f"ForwardTo{i}"] = forward_to + i += 1 + else: + err("Provide either --mailbox/--forward-to or --forwards ") + sys.exit(1) + + info(f"Setting email forwarding for {args.domain}...") + root = api_request("domains.dns.setEmailForwarding", params) + if _attr(root, "DomainDNSSetEmailForwardingResult", "IsSuccess").lower() == "true": + success(f"Email forwarding updated for {args.domain}!") + else: + err("Failed to set email forwarding.") + sys.exit(1) + + +def cmd_ns_create(args): + sld, tld = parse_domain(args.domain) + info(f"Creating nameserver {args.nameserver} -> {args.ip}...") + root = api_request( + "domains.ns.create", + {"SLD": sld, "TLD": tld, "Nameserver": args.nameserver, "IP": args.ip}, + ) + if _attr(root, "DomainNSCreateResult", "IsSuccess").lower() == "true": + success(f"Nameserver {args.nameserver} created!") + else: + err("Failed to create nameserver.") + sys.exit(1) + + +def cmd_ns_delete(args): + sld, tld = parse_domain(args.domain) + info(f"Deleting nameserver {args.nameserver}...") + root = api_request( + "domains.ns.delete", + {"SLD": sld, "TLD": tld, "Nameserver": args.nameserver}, + ) + if _attr(root, "DomainNSDeleteResult", "IsSuccess").lower() == "true": + success(f"Nameserver {args.nameserver} deleted!") + else: + err("Failed to delete nameserver.") + sys.exit(1) + + +def cmd_ns_get_info(args): + sld, tld = parse_domain(args.domain) + info(f"Fetching info for nameserver {args.nameserver}...") + root = api_request( + "domains.ns.getInfo", + {"SLD": sld, "TLD": tld, "Nameserver": args.nameserver}, + ) + ns_ip = _attr(root, "DomainNSInfoResult", "IP", "unknown") + print() + print(f"Nameserver: {args.nameserver}") + print(f"IP Address: {ns_ip}") + statuses = [s.text.strip() for s in root.iter("Status") if s.text and s.text.strip()] + if statuses: + print(f"Status: {', '.join(statuses)}") + print() + + +def cmd_ns_update(args): + sld, tld = parse_domain(args.domain) + info(f"Updating nameserver {args.nameserver}: {args.old_ip} -> {args.ip}...") + root = api_request( + "domains.ns.update", + {"SLD": sld, "TLD": tld, "Nameserver": args.nameserver, + "OldIP": args.old_ip, "IP": args.ip}, + ) + if _attr(root, "DomainNSUpdateResult", "IsSuccess").lower() == "true": + success(f"Nameserver {args.nameserver} updated to {args.ip}!") + else: + err("Failed to update nameserver.") + sys.exit(1) + + +# --- Argument parsing ----------------------------------------------------- + +def build_parser(): + parser = argparse.ArgumentParser( + prog="namecheap.py", + description="Namecheap DNS Management CLI", + ) + sub = parser.add_subparsers(dest="command", metavar="") + + sub.add_parser("setup", help="Configure API credentials and test connection").set_defaults(func=cmd_setup) + sub.add_parser("public-ip", help="Show your public IP address").set_defaults(func=cmd_public_ip) + + p = sub.add_parser("domains.getList", help="List your Namecheap domains") + p.add_argument("--type", default="ALL") + p.add_argument("--search", default="") + p.add_argument("--page", type=int, default=1) + p.add_argument("--page-size", type=int, default=20) + p.set_defaults(func=cmd_domains_list) + + p = sub.add_parser("domains.dns.getList", help="Get nameservers for a domain") + p.add_argument("--domain", required=True) + p.set_defaults(func=cmd_dns_get_list) + + p = sub.add_parser("domains.dns.getHosts", help="Get DNS records for a domain") + p.add_argument("--domain", required=True) + p.set_defaults(func=cmd_dns_get_hosts) + + p = sub.add_parser("domains.dns.setHosts", help="Set all DNS records (from JSON file)") + p.add_argument("--domain", required=True) + p.add_argument("--hosts", required=True) + p.set_defaults(func=cmd_dns_set_hosts) + + p = sub.add_parser("domains.dns.setDefault", help="Use Namecheap default DNS") + p.add_argument("--domain", required=True) + p.set_defaults(func=cmd_dns_set_default) + + p = sub.add_parser("domains.dns.setCustom", help="Use custom nameservers") + p.add_argument("--domain", required=True) + p.add_argument("--nameservers", required=True) + p.set_defaults(func=cmd_dns_set_custom) + + p = sub.add_parser("domains.dns.getEmailForwarding", help="Get email forwarding rules") + p.add_argument("--domain", required=True) + p.set_defaults(func=cmd_dns_get_email_forwarding) + + p = sub.add_parser("domains.dns.setEmailForwarding", help="Set email forwarding rules") + p.add_argument("--domain", required=True) + p.add_argument("--mailbox", default="") + p.add_argument("--forward-to", default="") + p.add_argument("--forwards", default="") + p.set_defaults(func=cmd_dns_set_email_forwarding) + + p = sub.add_parser("domains.ns.create", help="Create a child nameserver (glue record)") + p.add_argument("--domain", required=True) + p.add_argument("--nameserver", required=True) + p.add_argument("--ip", required=True) + p.set_defaults(func=cmd_ns_create) + + p = sub.add_parser("domains.ns.delete", help="Delete a child nameserver") + p.add_argument("--domain", required=True) + p.add_argument("--nameserver", required=True) + p.set_defaults(func=cmd_ns_delete) + + p = sub.add_parser("domains.ns.getInfo", help="Get nameserver info") + p.add_argument("--domain", required=True) + p.add_argument("--nameserver", required=True) + p.set_defaults(func=cmd_ns_get_info) + + p = sub.add_parser("domains.ns.update", help="Update nameserver IP") + p.add_argument("--domain", required=True) + p.add_argument("--nameserver", required=True) + p.add_argument("--old-ip", required=True) + p.add_argument("--ip", required=True) + p.set_defaults(func=cmd_ns_update) + + p = sub.add_parser("dns.addHost", help="Add a single DNS record (preserves existing)") + p.add_argument("--domain", required=True) + p.add_argument("--type", required=True) + p.add_argument("--name", required=True) + p.add_argument("--address", required=True) + p.add_argument("--ttl", default="1800") + p.add_argument("--mxpref", default="") + p.set_defaults(func=cmd_dns_add_host) + + p = sub.add_parser("dns.removeHost", help="Remove a single DNS record") + p.add_argument("--domain", required=True) + p.add_argument("--type", required=True) + p.add_argument("--name", required=True) + p.add_argument("--address", default="") + p.set_defaults(func=cmd_dns_remove_host) + + return parser + + +def main(argv=None): + parser = build_parser() + args = parser.parse_args(argv) + if not getattr(args, "command", None): + parser.print_help() + return 1 + try: + args.func(args) + except NamecheapError as exc: + err(f"API returned error: {exc}") + return 1 + except urllib.error.URLError as exc: + err(f"Network error: {exc}") + return 1 + except (OSError, ET.ParseError, json.JSONDecodeError) as exc: + err(str(exc)) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/skills/namecheap/namecheap.sh b/skills/namecheap/namecheap.sh deleted file mode 100755 index 8cc246bcc..000000000 --- a/skills/namecheap/namecheap.sh +++ /dev/null @@ -1,919 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Namecheap API CLI wrapper -# Usage: ./namecheap.sh [options] - -NAMECHEAP_API_URL="https://api.namecheap.com/xml.response" -CONFIG_FILE="$HOME/.namecheap-api" - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -NC='\033[0m' # No Color - -print_error() { echo -e "${RED}Error:${NC} $1" >&2; } -print_success() { echo -e "${GREEN}✓${NC} $1"; } -print_info() { echo -e "${CYAN}ℹ${NC} $1"; } -print_warn() { echo -e "${YELLOW}⚠${NC} $1"; } - -# Load configuration -load_config() { - if [[ -f "$CONFIG_FILE" ]]; then - # shellcheck source=/dev/null - source "$CONFIG_FILE" - fi -} - -# Check if credentials are configured -check_credentials() { - load_config - if [[ -z "${NAMECHEAP_API_USER:-}" || -z "${NAMECHEAP_API_KEY:-}" ]]; then - print_error "Namecheap API credentials not configured." - echo "" - echo "Run './namecheap.sh setup' to configure your credentials." - echo "" - echo "You need:" - echo " 1. Your Namecheap username" - echo " 2. An API key from: https://ap.www.namecheap.com/settings/tools/apiaccess/" - echo " 3. Your public IP whitelisted in the API settings" - exit 1 - fi -} - -# Get public IP address -get_public_ip() { - curl -s https://api.ipify.org 2>/dev/null || curl -s https://ifconfig.me 2>/dev/null || echo "unknown" -} - -# Make API request -api_request() { - local command="$1" - shift - local extra_params=("$@") - - check_credentials - local client_ip - client_ip=$(get_public_ip) - - local url="${NAMECHEAP_API_URL}?ApiUser=${NAMECHEAP_API_USER}&ApiKey=${NAMECHEAP_API_KEY}&UserName=${NAMECHEAP_API_USER}&Command=namecheap.${command}&ClientIp=${client_ip}" - - for param in "${extra_params[@]}"; do - url="${url}&${param}" - done - - local response - response=$(curl -s "$url") - - # Check for errors in the response - if echo "$response" | grep -q 'Status="ERROR"'; then - local error_msg - error_msg=$(echo "$response" | grep -oP '(?<=]*>\K[^<]+' 2>/dev/null || echo "$response" | sed -n 's/.*]*>\(.*\)<\/Err>.*/\1/p') - print_error "API returned error: $error_msg" - return 1 - fi - - echo "$response" -} - -# Parse domain into SLD and TLD -parse_domain() { - local domain="$1" - local tld sld - - # Handle multi-part TLDs (e.g., co.uk, com.br) - if echo "$domain" | grep -qE '\.(co|com|net|org|gov)\.[a-z]{2}$'; then - tld=$(echo "$domain" | grep -oE '\.[^.]+\.[^.]+$' | sed 's/^\.//') - sld=$(echo "$domain" | sed "s/\.${tld}$//") - else - tld="${domain##*.}" - sld="${domain%.*}" - fi - - echo "$sld" "$tld" -} - -# Format XML DNS records as a table -format_dns_records() { - local xml="$1" - - # Extract host records - echo "" - printf "%-20s %-8s %-40s %-8s %-6s\n" "HOST" "TYPE" "ADDRESS" "TTL" "MXPREF" - printf "%-20s %-8s %-40s %-8s %-6s\n" "----" "----" "-------" "---" "------" - - echo "$xml" | grep -oP '' | while read -r line; do - local name type address ttl mxpref - name=$(echo "$line" | grep -oP 'Name="\K[^"]+' || echo "") - type=$(echo "$line" | grep -oP 'Type="\K[^"]+' || echo "") - address=$(echo "$line" | grep -oP 'Address="\K[^"]+' || echo "") - ttl=$(echo "$line" | grep -oP 'TTL="\K[^"]+' || echo "1800") - mxpref=$(echo "$line" | grep -oP 'MXPref="\K[^"]+' || echo "-") - - printf "%-20s %-8s %-40s %-8s %-6s\n" "$name" "$type" "$address" "$ttl" "$mxpref" - done - echo "" -} - -# Format domains list as a table -format_domains_list() { - local xml="$1" - - echo "" - printf "%-30s %-12s %-12s %-10s\n" "DOMAIN" "EXPIRES" "LOCKED" "AUTO-RENEW" - printf "%-30s %-12s %-12s %-10s\n" "------" "-------" "------" "----------" - - echo "$xml" | grep -oP '' | while read -r line; do - local name expires locked autorenew - name=$(echo "$line" | grep -oP 'Name="\K[^"]+' || echo "") - expires=$(echo "$line" | grep -oP 'Expires="\K[^"]+' || echo "") - locked=$(echo "$line" | grep -oP 'IsLocked="\K[^"]+' || echo "") - autorenew=$(echo "$line" | grep -oP 'AutoRenew="\K[^"]+' || echo "") - - printf "%-30s %-12s %-12s %-10s\n" "$name" "$expires" "$locked" "$autorenew" - done - echo "" -} - -# Commands - -cmd_setup() { - echo "=== Namecheap API Setup ===" - echo "" - - # Show public IP - local public_ip - public_ip=$(get_public_ip) - print_info "Your public IP address is: ${CYAN}${public_ip}${NC}" - echo "" - echo "Make sure this IP is whitelisted at:" - echo " https://ap.www.namecheap.com/settings/tools/apiaccess/" - echo "" - - # Check existing config - if [[ -f "$CONFIG_FILE" ]]; then - load_config - if [[ -n "${NAMECHEAP_API_USER:-}" ]]; then - print_info "Existing configuration found for user: ${NAMECHEAP_API_USER}" - echo "" - - # Test the connection - echo "Testing API connection..." - if api_request "domains.getList" "PageSize=1" > /dev/null 2>&1; then - print_success "API connection successful!" - else - print_error "API connection failed. Please check your credentials and IP whitelist." - fi - return 0 - fi - fi - - # Prompt for credentials - echo "Enter your Namecheap credentials:" - echo "" - read -rp " API Username: " api_user - read -rsp " API Key: " api_key - echo "" - echo "" - - if [[ -z "$api_user" || -z "$api_key" ]]; then - print_error "Both username and API key are required." - exit 1 - fi - - # Save configuration - cat > "$CONFIG_FILE" << EOF -NAMECHEAP_API_USER="${api_user}" -NAMECHEAP_API_KEY="${api_key}" -EOF - chmod 600 "$CONFIG_FILE" - print_success "Credentials saved to ${CONFIG_FILE}" - echo "" - - # Test connection - load_config - echo "Testing API connection..." - if api_request "domains.getList" "PageSize=1" > /dev/null 2>&1; then - print_success "API connection successful!" - else - print_warn "API connection failed. Please verify:" - echo " 1. API access is enabled (ON) at the Namecheap settings page" - echo " 2. IP address ${public_ip} is whitelisted" - echo " 3. Your API key is correct" - fi -} - -cmd_domains_list() { - local list_type="ALL" - local search_term="" - local page="1" - local page_size="20" - - while [[ $# -gt 0 ]]; do - case "$1" in - --type) list_type="$2"; shift 2 ;; - --search) search_term="$2"; shift 2 ;; - --page) page="$2"; shift 2 ;; - --page-size) page_size="$2"; shift 2 ;; - *) shift ;; - esac - done - - local params=("ListType=${list_type}" "Page=${page}" "PageSize=${page_size}") - if [[ -n "$search_term" ]]; then - params+=("SearchTerm=${search_term}") - fi - - print_info "Fetching domain list..." - local response - response=$(api_request "domains.getList" "${params[@]}") - format_domains_list "$response" -} - -cmd_dns_get_hosts() { - local domain="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --domain) domain="$2"; shift 2 ;; - *) shift ;; - esac - done - - if [[ -z "$domain" ]]; then - print_error "Domain is required. Usage: ./namecheap.sh domains.dns.getHosts --domain example.com" - exit 1 - fi - - local sld tld - read -r sld tld <<< "$(parse_domain "$domain")" - - print_info "Fetching DNS records for ${domain} (SLD=${sld}, TLD=${tld})..." - local response - response=$(api_request "domains.dns.getHosts" "SLD=${sld}" "TLD=${tld}") - format_dns_records "$response" -} - -cmd_dns_set_hosts() { - local domain="" - local hosts_file="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --domain) domain="$2"; shift 2 ;; - --hosts) hosts_file="$2"; shift 2 ;; - *) shift ;; - esac - done - - if [[ -z "$domain" || -z "$hosts_file" ]]; then - print_error "Both --domain and --hosts are required." - echo "Usage: ./namecheap.sh domains.dns.setHosts --domain example.com --hosts hosts.json" - exit 1 - fi - - if [[ ! -f "$hosts_file" ]]; then - print_error "Hosts file not found: ${hosts_file}" - exit 1 - fi - - local sld tld - read -r sld tld <<< "$(parse_domain "$domain")" - - # Build host parameters from JSON file - local params=("SLD=${sld}" "TLD=${tld}") - local i=1 - - while IFS= read -r line; do - local hostname recordtype address ttl mxpref - hostname=$(echo "$line" | grep -oP '"HostName"\s*:\s*"\K[^"]+' || echo "") - recordtype=$(echo "$line" | grep -oP '"RecordType"\s*:\s*"\K[^"]+' || echo "") - address=$(echo "$line" | grep -oP '"Address"\s*:\s*"\K[^"]+' || echo "") - ttl=$(echo "$line" | grep -oP '"TTL"\s*:\s*"\K[^"]+' || echo "1800") - mxpref=$(echo "$line" | grep -oP '"MXPref"\s*:\s*"\K[^"]+' || echo "") - - if [[ -n "$hostname" && -n "$recordtype" && -n "$address" ]]; then - params+=("HostName${i}=${hostname}") - params+=("RecordType${i}=${recordtype}") - params+=("Address${i}=${address}") - params+=("TTL${i}=${ttl}") - if [[ -n "$mxpref" ]]; then - params+=("MXPref${i}=${mxpref}") - fi - ((i++)) - fi - done < <(python3 -c " -import json, sys -with open('${hosts_file}') as f: - records = json.load(f) -for r in records: - print(json.dumps(r)) -" 2>/dev/null || jq -c '.[]' "$hosts_file") - - if [[ $i -eq 1 ]]; then - print_error "No valid host records found in ${hosts_file}" - exit 1 - fi - - print_info "Setting $((i-1)) DNS records for ${domain}..." - local response - response=$(api_request "domains.dns.setHosts" "${params[@]}") - - if echo "$response" | grep -q 'IsSuccess="true"'; then - print_success "DNS records updated successfully for ${domain}!" - else - print_error "Failed to update DNS records." - echo "$response" - fi -} - -cmd_dns_add_host() { - local domain="" record_type="" name="" address="" ttl="1800" mxpref="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --domain) domain="$2"; shift 2 ;; - --type) record_type="$2"; shift 2 ;; - --name) name="$2"; shift 2 ;; - --address) address="$2"; shift 2 ;; - --ttl) ttl="$2"; shift 2 ;; - --mxpref) mxpref="$2"; shift 2 ;; - *) shift ;; - esac - done - - if [[ -z "$domain" || -z "$record_type" || -z "$name" || -z "$address" ]]; then - print_error "Missing required parameters." - echo "Usage: ./namecheap.sh dns.addHost --domain example.com --type A --name \"@\" --address \"1.2.3.4\" [--ttl 1800] [--mxpref 10]" - exit 1 - fi - - local sld tld - read -r sld tld <<< "$(parse_domain "$domain")" - - # Fetch existing records - print_info "Fetching existing DNS records for ${domain}..." - local response - response=$(api_request "domains.dns.getHosts" "SLD=${sld}" "TLD=${tld}") - - # Build params with existing records + new one - local params=("SLD=${sld}" "TLD=${tld}") - local i=1 - - # Parse existing records - while IFS= read -r line; do - if [[ -z "$line" ]]; then continue; fi - local h_name h_type h_address h_ttl h_mxpref - h_name=$(echo "$line" | grep -oP 'Name="\K[^"]+' || echo "") - h_type=$(echo "$line" | grep -oP 'Type="\K[^"]+' || echo "") - h_address=$(echo "$line" | grep -oP 'Address="\K[^"]+' || echo "") - h_ttl=$(echo "$line" | grep -oP 'TTL="\K[^"]+' || echo "1800") - h_mxpref=$(echo "$line" | grep -oP 'MXPref="\K[^"]+' || echo "") - - if [[ -n "$h_name" && -n "$h_type" && -n "$h_address" ]]; then - params+=("HostName${i}=${h_name}") - params+=("RecordType${i}=${h_type}") - params+=("Address${i}=${h_address}") - params+=("TTL${i}=${h_ttl}") - if [[ -n "$h_mxpref" && "$h_mxpref" != "0" ]]; then - params+=("MXPref${i}=${h_mxpref}") - fi - ((i++)) - fi - done < <(echo "$response" | grep -oP '') - - # Add the new record - params+=("HostName${i}=${name}") - params+=("RecordType${i}=${record_type}") - params+=("Address${i}=${address}") - params+=("TTL${i}=${ttl}") - if [[ -n "$mxpref" ]]; then - params+=("MXPref${i}=${mxpref}") - fi - - print_info "Adding ${record_type} record: ${name} -> ${address}" - local set_response - set_response=$(api_request "domains.dns.setHosts" "${params[@]}") - - if echo "$set_response" | grep -q 'IsSuccess="true"'; then - print_success "DNS record added successfully!" - else - print_error "Failed to add DNS record." - echo "$set_response" - fi -} - -cmd_dns_remove_host() { - local domain="" record_type="" name="" address="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --domain) domain="$2"; shift 2 ;; - --type) record_type="$2"; shift 2 ;; - --name) name="$2"; shift 2 ;; - --address) address="$2"; shift 2 ;; - *) shift ;; - esac - done - - if [[ -z "$domain" || -z "$record_type" || -z "$name" ]]; then - print_error "Missing required parameters." - echo "Usage: ./namecheap.sh dns.removeHost --domain example.com --type A --name \"@\" [--address \"1.2.3.4\"]" - exit 1 - fi - - local sld tld - read -r sld tld <<< "$(parse_domain "$domain")" - - # Fetch existing records - print_info "Fetching existing DNS records for ${domain}..." - local response - response=$(api_request "domains.dns.getHosts" "SLD=${sld}" "TLD=${tld}") - - # Build params excluding the record to remove - local params=("SLD=${sld}" "TLD=${tld}") - local i=1 - local removed=false - - while IFS= read -r line; do - if [[ -z "$line" ]]; then continue; fi - local h_name h_type h_address h_ttl h_mxpref - h_name=$(echo "$line" | grep -oP 'Name="\K[^"]+' || echo "") - h_type=$(echo "$line" | grep -oP 'Type="\K[^"]+' || echo "") - h_address=$(echo "$line" | grep -oP 'Address="\K[^"]+' || echo "") - h_ttl=$(echo "$line" | grep -oP 'TTL="\K[^"]+' || echo "1800") - h_mxpref=$(echo "$line" | grep -oP 'MXPref="\K[^"]+' || echo "") - - # Check if this is the record to remove - if [[ "$h_name" == "$name" && "$h_type" == "$record_type" && "$removed" == "false" ]]; then - if [[ -z "$address" || "$h_address" == "$address" ]]; then - removed=true - print_info "Removing record: ${h_name} ${h_type} ${h_address}" - continue - fi - fi - - if [[ -n "$h_name" && -n "$h_type" && -n "$h_address" ]]; then - params+=("HostName${i}=${h_name}") - params+=("RecordType${i}=${h_type}") - params+=("Address${i}=${h_address}") - params+=("TTL${i}=${h_ttl}") - if [[ -n "$h_mxpref" && "$h_mxpref" != "0" ]]; then - params+=("MXPref${i}=${h_mxpref}") - fi - ((i++)) - fi - done < <(echo "$response" | grep -oP '') - - if [[ "$removed" == "false" ]]; then - print_error "No matching record found to remove." - exit 1 - fi - - # If no records left, we still need at least one (Namecheap requirement) - if [[ $i -eq 1 ]]; then - print_error "Cannot remove the last DNS record. Namecheap requires at least one record." - exit 1 - fi - - print_info "Updating DNS records for ${domain}..." - local set_response - set_response=$(api_request "domains.dns.setHosts" "${params[@]}") - - if echo "$set_response" | grep -q 'IsSuccess="true"'; then - print_success "DNS record removed successfully!" - else - print_error "Failed to remove DNS record." - echo "$set_response" - fi -} - -cmd_public_ip() { - local ip - ip=$(get_public_ip) - echo "$ip" -} - -cmd_dns_get_list() { - local domain="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --domain) domain="$2"; shift 2 ;; - *) shift ;; - esac - done - - if [[ -z "$domain" ]]; then - print_error "Domain is required. Usage: ./namecheap.sh domains.dns.getList --domain example.com" - exit 1 - fi - - local sld tld - read -r sld tld <<< "$(parse_domain "$domain")" - - print_info "Fetching nameservers for ${domain}..." - local response - response=$(api_request "domains.dns.getList" "SLD=${sld}" "TLD=${tld}") - - local using_our_dns - using_our_dns=$(echo "$response" | grep -oP 'IsUsingOurDNS="\K[^"]+' || echo "unknown") - echo "" - print_info "Using Namecheap DNS: ${using_our_dns}" - echo "" - echo "Nameservers:" - echo "$response" | grep -oP '\K[^<]+' | while read -r ns; do - echo " - ${ns}" - done - echo "" -} - -cmd_dns_set_default() { - local domain="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --domain) domain="$2"; shift 2 ;; - *) shift ;; - esac - done - - if [[ -z "$domain" ]]; then - print_error "Domain is required. Usage: ./namecheap.sh domains.dns.setDefault --domain example.com" - exit 1 - fi - - local sld tld - read -r sld tld <<< "$(parse_domain "$domain")" - - print_info "Setting ${domain} to use Namecheap default DNS..." - local response - response=$(api_request "domains.dns.setDefault" "SLD=${sld}" "TLD=${tld}") - - if echo "$response" | grep -q 'Updated="true"'; then - print_success "Domain ${domain} now uses Namecheap default DNS!" - else - print_error "Failed to set default DNS." - echo "$response" - fi -} - -cmd_dns_set_custom() { - local domain="" nameservers="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --domain) domain="$2"; shift 2 ;; - --nameservers) nameservers="$2"; shift 2 ;; - *) shift ;; - esac - done - - if [[ -z "$domain" || -z "$nameservers" ]]; then - print_error "Both --domain and --nameservers are required." - echo "Usage: ./namecheap.sh domains.dns.setCustom --domain example.com --nameservers ns1.cloudflare.com,ns2.cloudflare.com" - exit 1 - fi - - local sld tld - read -r sld tld <<< "$(parse_domain "$domain")" - - print_info "Setting ${domain} to use custom nameservers: ${nameservers}" - local response - response=$(api_request "domains.dns.setCustom" "SLD=${sld}" "TLD=${tld}" "Nameservers=${nameservers}") - - if echo "$response" | grep -q 'Updated="true"'; then - print_success "Domain ${domain} now uses custom nameservers!" - else - print_error "Failed to set custom nameservers." - echo "$response" - fi -} - -cmd_dns_get_email_forwarding() { - local domain="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --domain) domain="$2"; shift 2 ;; - *) shift ;; - esac - done - - if [[ -z "$domain" ]]; then - print_error "Domain is required. Usage: ./namecheap.sh domains.dns.getEmailForwarding --domain example.com" - exit 1 - fi - - print_info "Fetching email forwarding for ${domain}..." - local response - response=$(api_request "domains.dns.getEmailForwarding" "DomainName=${domain}") - - echo "" - printf "%-20s %-40s\n" "MAILBOX" "FORWARDS TO" - printf "%-20s %-40s\n" "-------" "-----------" - - echo "$response" | grep -oP '' | while read -r line; do - local mailbox forward_to - mailbox=$(echo "$line" | grep -oP 'mailbox="\K[^"]+' || echo "") - forward_to=$(echo "$line" | grep -oP 'ForwardTo="\K[^"]+' || echo "") - printf "%-20s %-40s\n" "${mailbox}@${domain}" "$forward_to" - done - echo "" -} - -cmd_dns_set_email_forwarding() { - local domain="" forwards_file="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --domain) domain="$2"; shift 2 ;; - --forwards) forwards_file="$2"; shift 2 ;; - --mailbox) - # Inline single forwarding rule - local inline_mailbox="$2"; shift 2 ;; - --forward-to) - local inline_forward_to="$2"; shift 2 ;; - *) shift ;; - esac - done - - if [[ -z "$domain" ]]; then - print_error "Domain is required." - echo "Usage: ./namecheap.sh domains.dns.setEmailForwarding --domain example.com --mailbox info --forward-to user@gmail.com" - echo " or: ./namecheap.sh domains.dns.setEmailForwarding --domain example.com --forwards forwards.json" - exit 1 - fi - - local params=("DomainName=${domain}") - - if [[ -n "${inline_mailbox:-}" && -n "${inline_forward_to:-}" ]]; then - # Single inline rule - params+=("MailBox1=${inline_mailbox}" "ForwardTo1=${inline_forward_to}") - elif [[ -n "$forwards_file" ]]; then - if [[ ! -f "$forwards_file" ]]; then - print_error "Forwards file not found: ${forwards_file}" - exit 1 - fi - local i=1 - while IFS= read -r line; do - local mailbox forward_to - mailbox=$(echo "$line" | grep -oP '"MailBox"\s*:\s*"\K[^"]+' || echo "") - forward_to=$(echo "$line" | grep -oP '"ForwardTo"\s*:\s*"\K[^"]+' || echo "") - if [[ -n "$mailbox" && -n "$forward_to" ]]; then - params+=("MailBox${i}=${mailbox}" "ForwardTo${i}=${forward_to}") - ((i++)) - fi - done < <(python3 -c " -import json, sys -with open('${forwards_file}') as f: - rules = json.load(f) -for r in rules: - print(json.dumps(r)) -" 2>/dev/null || jq -c '.[]' "$forwards_file") - else - print_error "Provide either --mailbox/--forward-to or --forwards " - exit 1 - fi - - print_info "Setting email forwarding for ${domain}..." - local response - response=$(api_request "domains.dns.setEmailForwarding" "${params[@]}") - - if echo "$response" | grep -q 'IsSuccess="true"'; then - print_success "Email forwarding updated for ${domain}!" - else - print_error "Failed to set email forwarding." - echo "$response" - fi -} - -cmd_ns_create() { - local domain="" nameserver="" ip="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --domain) domain="$2"; shift 2 ;; - --nameserver) nameserver="$2"; shift 2 ;; - --ip) ip="$2"; shift 2 ;; - *) shift ;; - esac - done - - if [[ -z "$domain" || -z "$nameserver" || -z "$ip" ]]; then - print_error "Missing required parameters." - echo "Usage: ./namecheap.sh domains.ns.create --domain example.com --nameserver ns1.example.com --ip 1.2.3.4" - exit 1 - fi - - local sld tld - read -r sld tld <<< "$(parse_domain "$domain")" - - print_info "Creating nameserver ${nameserver} -> ${ip}..." - local response - response=$(api_request "domains.ns.create" "SLD=${sld}" "TLD=${tld}" "Nameserver=${nameserver}" "IP=${ip}") - - if echo "$response" | grep -q 'IsSuccess="true"'; then - print_success "Nameserver ${nameserver} created!" - else - print_error "Failed to create nameserver." - echo "$response" - fi -} - -cmd_ns_delete() { - local domain="" nameserver="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --domain) domain="$2"; shift 2 ;; - --nameserver) nameserver="$2"; shift 2 ;; - *) shift ;; - esac - done - - if [[ -z "$domain" || -z "$nameserver" ]]; then - print_error "Missing required parameters." - echo "Usage: ./namecheap.sh domains.ns.delete --domain example.com --nameserver ns1.example.com" - exit 1 - fi - - local sld tld - read -r sld tld <<< "$(parse_domain "$domain")" - - print_info "Deleting nameserver ${nameserver}..." - local response - response=$(api_request "domains.ns.delete" "SLD=${sld}" "TLD=${tld}" "Nameserver=${nameserver}") - - if echo "$response" | grep -q 'IsSuccess="true"'; then - print_success "Nameserver ${nameserver} deleted!" - else - print_error "Failed to delete nameserver." - echo "$response" - fi -} - -cmd_ns_get_info() { - local domain="" nameserver="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --domain) domain="$2"; shift 2 ;; - --nameserver) nameserver="$2"; shift 2 ;; - *) shift ;; - esac - done - - if [[ -z "$domain" || -z "$nameserver" ]]; then - print_error "Missing required parameters." - echo "Usage: ./namecheap.sh domains.ns.getInfo --domain example.com --nameserver ns1.example.com" - exit 1 - fi - - local sld tld - read -r sld tld <<< "$(parse_domain "$domain")" - - print_info "Fetching info for nameserver ${nameserver}..." - local response - response=$(api_request "domains.ns.getInfo" "SLD=${sld}" "TLD=${tld}" "Nameserver=${nameserver}") - - local ns_ip - ns_ip=$(echo "$response" | grep -oP 'IP="\K[^"]+' || echo "unknown") - echo "" - echo "Nameserver: ${nameserver}" - echo "IP Address: ${ns_ip}" - local statuses - statuses=$(echo "$response" | grep -oP '\K[^<]+' | tr '\n' ', ' | sed 's/,$//') - if [[ -n "$statuses" ]]; then - echo "Status: ${statuses}" - fi - echo "" -} - -cmd_ns_update() { - local domain="" nameserver="" old_ip="" new_ip="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --domain) domain="$2"; shift 2 ;; - --nameserver) nameserver="$2"; shift 2 ;; - --old-ip) old_ip="$2"; shift 2 ;; - --ip) new_ip="$2"; shift 2 ;; - *) shift ;; - esac - done - - if [[ -z "$domain" || -z "$nameserver" || -z "$old_ip" || -z "$new_ip" ]]; then - print_error "Missing required parameters." - echo "Usage: ./namecheap.sh domains.ns.update --domain example.com --nameserver ns1.example.com --old-ip 1.2.3.4 --ip 5.6.7.8" - exit 1 - fi - - local sld tld - read -r sld tld <<< "$(parse_domain "$domain")" - - print_info "Updating nameserver ${nameserver}: ${old_ip} -> ${new_ip}..." - local response - response=$(api_request "domains.ns.update" "SLD=${sld}" "TLD=${tld}" "Nameserver=${nameserver}" "OldIP=${old_ip}" "IP=${new_ip}") - - if echo "$response" | grep -q 'IsSuccess="true"'; then - print_success "Nameserver ${nameserver} updated to ${new_ip}!" - else - print_error "Failed to update nameserver." - echo "$response" - fi -} - -# Help -cmd_help() { - echo "Namecheap DNS Management CLI" - echo "" - echo "Usage: ./namecheap.sh [options]" - echo "" - echo "Commands:" - echo " setup Configure API credentials and test connection" - echo " public-ip Show your public IP address" - echo "" - echo " domains.getList List your Namecheap domains" - echo "" - echo " domains.dns.getList Get nameservers for a domain" - echo " domains.dns.getHosts Get DNS records for a domain" - echo " domains.dns.setHosts Set all DNS records (from JSON file)" - echo " domains.dns.setDefault Use Namecheap default DNS" - echo " domains.dns.setCustom Use custom nameservers" - echo " domains.dns.getEmailForwarding Get email forwarding rules" - echo " domains.dns.setEmailForwarding Set email forwarding rules" - echo "" - echo " domains.ns.create Create a child nameserver (glue record)" - echo " domains.ns.delete Delete a child nameserver" - echo " domains.ns.getInfo Get nameserver info" - echo " domains.ns.update Update nameserver IP" - echo "" - echo " dns.addHost Add a single DNS record (preserves existing)" - echo " dns.removeHost Remove a single DNS record" - echo "" - echo "Options:" - echo " --domain Domain name (e.g., example.com)" - echo " --type Record type (A, AAAA, CNAME, MX, TXT, etc.)" - echo " --name Host name (e.g., @, www, mail)" - echo " --address Record value (IP or target)" - echo " --ttl TTL in seconds (default: 1800)" - echo " --mxpref MX preference (for MX records)" - echo " --hosts JSON file with host records" - echo " --nameservers Comma-separated nameservers" - echo " --nameserver Nameserver hostname" - echo " --ip
IP address for nameserver" - echo " --old-ip
Current IP (for ns.update)" - echo " --mailbox Email mailbox name" - echo " --forward-to Forward destination email" - echo " --forwards JSON file with forwarding rules" - echo " --search Search term for domain list" - echo " --page Page number for domain list" - echo " --page-size Page size for domain list (10-100)" - echo "" - echo "Examples:" - echo " ./namecheap.sh setup" - echo " ./namecheap.sh domains.getList" - echo " ./namecheap.sh domains.dns.getHosts --domain example.com" - echo " ./namecheap.sh dns.addHost --domain example.com --type A --name www --address 1.2.3.4" - echo " ./namecheap.sh dns.removeHost --domain example.com --type A --name www" - echo " ./namecheap.sh domains.dns.setCustom --domain example.com --nameservers ns1.cloudflare.com,ns2.cloudflare.com" - echo " ./namecheap.sh domains.dns.setEmailForwarding --domain example.com --mailbox info --forward-to user@gmail.com" - echo " ./namecheap.sh domains.ns.create --domain example.com --nameserver ns1.example.com --ip 1.2.3.4" -} - -# Main dispatch -main() { - local command="${1:-help}" - shift || true - - case "$command" in - setup) cmd_setup "$@" ;; - public-ip) cmd_public_ip "$@" ;; - domains.getList) cmd_domains_list "$@" ;; - domains.dns.getList) cmd_dns_get_list "$@" ;; - domains.dns.getHosts) cmd_dns_get_hosts "$@" ;; - domains.dns.setHosts) cmd_dns_set_hosts "$@" ;; - domains.dns.setDefault) cmd_dns_set_default "$@" ;; - domains.dns.setCustom) cmd_dns_set_custom "$@" ;; - domains.dns.getEmailForwarding) cmd_dns_get_email_forwarding "$@" ;; - domains.dns.setEmailForwarding) cmd_dns_set_email_forwarding "$@" ;; - domains.ns.create) cmd_ns_create "$@" ;; - domains.ns.delete) cmd_ns_delete "$@" ;; - domains.ns.getInfo) cmd_ns_get_info "$@" ;; - domains.ns.update) cmd_ns_update "$@" ;; - dns.addHost) cmd_dns_add_host "$@" ;; - dns.removeHost) cmd_dns_remove_host "$@" ;; - help|--help|-h) cmd_help ;; - *) - print_error "Unknown command: ${command}" - echo "" - cmd_help - exit 1 - ;; - esac -} - -main "$@" From 3cd14cedd0c2052c13e5ab3470d49706e47b0bb7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 21:58:31 +0000 Subject: [PATCH 4/4] docs(namecheap): document JSON schemas for setHosts and forwarding files Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- skills/namecheap/SKILL.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/skills/namecheap/SKILL.md b/skills/namecheap/SKILL.md index 54669247d..1fae5cea6 100644 --- a/skills/namecheap/SKILL.md +++ b/skills/namecheap/SKILL.md @@ -79,6 +79,27 @@ python3 namecheap.py domains.ns.getInfo --domain example.com --nameserver ns1.ex python3 namecheap.py domains.ns.update --domain example.com --nameserver ns1.example.com --old-ip 1.2.3.4 --ip 5.6.7.8 ``` +### JSON file formats + +`domains.dns.setHosts --hosts records.json` expects an array of objects with Namecheap API field names: + +```json +[ + { "HostName": "@", "RecordType": "A", "Address": "1.2.3.4", "TTL": 1800 }, + { "HostName": "www", "RecordType": "CNAME", "Address": "@", "TTL": 1800 }, + { "HostName": "@", "RecordType": "MX", "Address": "mail.example.com.", "TTL": 1800, "MXPref": 10 } +] +``` + +`domains.dns.setEmailForwarding --forwards forwards.json` expects an array of mailbox rules: + +```json +[ + { "MailBox": "info", "ForwardTo": "team@example.net" }, + { "MailBox": "sales", "ForwardTo": "owner@example.net" } +] +``` + ## Behavior - **Always check credentials first.** Before any API operation, verify `~/.namecheap-api` exists and is readable. If not, run the setup flow.