diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md index 39d66877c..f91b85c3e 100644 --- a/.agents/AGENTS.md +++ b/.agents/AGENTS.md @@ -98,7 +98,8 @@ Read subagents on-demand. Full index: `subagent-index.toon`. | Email | `tools/ui/react-email.md`, `services/email/email-testing.md` | | Payments | `services/payments/revenuecat.md`, `services/payments/stripe.md` | | Security/Encryption | `tools/security/tirith.md`, `tools/credentials/encryption-stack.md` | -| Infrastructure | `tools/infrastructure/cloud-gpu.md`, `tools/containers/orbstack.md`, `tools/containers/remote-dispatch.md`, `services/hosting/local-hosting.md` | +| Local Development | `services/hosting/local-hosting.md` | +| Infrastructure | `tools/infrastructure/cloud-gpu.md`, `tools/containers/orbstack.md`, `tools/containers/remote-dispatch.md` | | Accessibility | `services/accessibility/accessibility-audit.md` | | OpenAPI exploration | `tools/context/openapi-search.md` | | Model routing | `tools/context/model-routing.md`, `reference/orchestration.md` | diff --git a/.agents/scripts/generate-skills.sh b/.agents/scripts/generate-skills.sh index 1ba4b0c4e..b1e887790 100755 --- a/.agents/scripts/generate-skills.sh +++ b/.agents/scripts/generate-skills.sh @@ -34,20 +34,20 @@ CLEAN=false # Parse arguments while [[ $# -gt 0 ]]; do - case $1 in - --dry-run) - DRY_RUN=true - shift - ;; - --clean) - CLEAN=true - shift - ;; - *) - echo -e "${RED}Unknown option: $1${NC}" - exit 1 - ;; - esac + case $1 in + --dry-run) + DRY_RUN=true + shift + ;; + --clean) + CLEAN=true + shift + ;; + *) + echo -e "${RED}Unknown option: $1${NC}" + exit 1 + ;; + esac done # ============================================================================= @@ -55,36 +55,36 @@ done # ============================================================================= log_info() { - echo -e "${BLUE}$1${NC}" - return 0 + echo -e "${BLUE}$1${NC}" + return 0 } log_success() { - echo -e "${GREEN}✓${NC} $1" - return 0 + echo -e "${GREEN}✓${NC} $1" + return 0 } log_warning() { - echo -e "${YELLOW}⚠${NC} $1" - return 0 + echo -e "${YELLOW}⚠${NC} $1" + return 0 } log_error() { - echo -e "${RED}✗${NC} $1" - return 0 + echo -e "${RED}✗${NC} $1" + return 0 } # Extract frontmatter field from markdown file extract_frontmatter_field() { - local file="$1" - local field="$2" - - if [[ ! -f "$file" ]]; then - return 1 - fi - - # Extract value between --- markers - awk -v field="$field" ' + local file="$1" + local field="$2" + + if [[ ! -f "$file" ]]; then + return 1 + fi + + # Extract value between --- markers + awk -v field="$field" ' /^---$/ { in_frontmatter = !in_frontmatter; next } in_frontmatter && $0 ~ "^" field ":" { sub("^" field ": *", "") @@ -93,134 +93,111 @@ extract_frontmatter_field() { exit } ' "$file" - return 0 + return 0 } # Extract description from file - tries frontmatter first, then first heading extract_description() { - local file="$1" - local desc - - # Try frontmatter first - desc=$(extract_frontmatter_field "$file" "description") - if [[ -n "$desc" ]]; then - echo "$desc" - return - fi - - # Try first heading (# Title - Description pattern or just # Title) - local heading - heading=$(grep -m1 "^# " "$file" 2>/dev/null | sed 's/^# //') - if [[ -n "$heading" ]]; then - # If heading has " - ", take the part after - if [[ "$heading" == *" - "* ]]; then - echo "${heading#* - }" - else - echo "$heading" - fi - return - fi - - # Fallback to filename - echo "$(basename "$file" .md) skill" + local file="$1" + local desc + + # Try frontmatter first + desc=$(extract_frontmatter_field "$file" "description") + if [[ -n "$desc" ]]; then + echo "$desc" + return + fi + + # Try first heading (# Title - Description pattern or just # Title) + local heading + heading=$(grep -m1 "^# " "$file" 2>/dev/null | sed 's/^# //') + if [[ -n "$heading" ]]; then + # If heading has " - ", take the part after + if [[ "$heading" == *" - "* ]]; then + echo "${heading#* - }" + else + echo "$heading" + fi + return + fi + + # Fallback to filename + echo "$(basename "$file" .md) skill" } # Convert name to valid skill name (lowercase, hyphens only) to_skill_name() { - local name="$1" - echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g' - return 0 + local name="$1" + echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g' + return 0 } # Capitalize first letter (portable) capitalize() { - local str="$1" - local first - local rest - first=$(echo "$str" | cut -c1 | tr '[:lower:]' '[:upper:]') - rest=$(echo "$str" | cut -c2-) - echo "${first}${rest}" - return 0 + local str="$1" + local first + local rest + first=$(echo "$str" | cut -c1 | tr '[:lower:]' '[:upper:]') + rest=$(echo "$str" | cut -c2-) + echo "${first}${rest}" + return 0 } -# Generate SKILL.md content for a folder with parent .md +# Generate SKILL.md content for a folder with parent .md (pure pointer) generate_folder_skill() { - local folder_path="$1" - local parent_md="$2" - local folder_name - folder_name=$(basename "$folder_path") - local skill_name - skill_name=$(to_skill_name "$folder_name") - - # Extract description from parent .md - local description - description=$(extract_description "$parent_md") - - # List subskills in folder - local subskills="" - while IFS= read -r subfile; do - if [[ -n "$subfile" ]]; then - local subname - subname=$(basename "$subfile" .md) - local subdesc - subdesc=$(extract_frontmatter_field "$subfile" "description") - if [[ -n "$subdesc" ]]; then - subskills="${subskills} -- ${subname}: ${subdesc}" - else - subskills="${subskills} -- ${subname}" - fi - fi - done < <(find "$folder_path" -maxdepth 1 -name "*.md" -not -name "SKILL.md" -type f 2>/dev/null | sort) - - # Generate SKILL.md content - local title - title=$(capitalize "$folder_name") - - echo "---" - echo "name: ${skill_name}" - echo "description: ${description}" - echo "---" - echo "" - echo "# ${title}" - echo "" - echo "See [${folder_name}.md](../${folder_name}.md) for full instructions." - if [[ -n "$subskills" ]]; then - echo "" - echo "## Subskills" - echo "$subskills" - fi - return 0 + local folder_path="$1" + local parent_md="$2" + local folder_name + folder_name=$(basename "$folder_path") + local skill_name + skill_name=$(to_skill_name "$folder_name") + + # Extract description from parent .md + local description + description=$(extract_description "$parent_md") + + # Generate pure pointer SKILL.md — no inlined subskill lists + local title + title=$(capitalize "$folder_name") + + echo "---" + echo "name: ${skill_name}" + echo "description: ${description}" + echo "---" + echo "" + echo "# ${title}" + echo "" + echo "See [${folder_name}.md](../${folder_name}.md) for full instructions." + return 0 } # Generate SKILL.md content for a leaf .md file generate_leaf_skill() { - local md_file="$1" - local filename - filename=$(basename "$md_file" .md) - local skill_name - skill_name=$(to_skill_name "$filename") - - # Extract description - local description - description=$(extract_description "$md_file") - - # Get relative path to the .md file from the new folder - local relative_path="../${filename}.md" - - local title - title=$(capitalize "$filename") - - echo "---" - echo "name: ${skill_name}" - echo "description: ${description}" - echo "---" - echo "" - echo "# ${title}" - echo "" - echo "See [${filename}.md](${relative_path}) for full instructions." - return 0 + local md_file="$1" + local filename + filename=$(basename "$md_file" .md) + local skill_name + skill_name=$(to_skill_name "$filename") + + # Extract description + local description + description=$(extract_description "$md_file") + + # Get relative path to the .md file from the new folder + local relative_path="../${filename}.md" + + local title + title=$(capitalize "$filename") + + echo "---" + echo "name: ${skill_name}" + echo "description: ${description}" + echo "---" + echo "" + echo "# ${title}" + echo "" + echo "See [${filename}.md](${relative_path}) for full instructions." + return 0 } # ============================================================================= @@ -228,25 +205,25 @@ generate_leaf_skill() { # ============================================================================= if [[ "$CLEAN" == true ]]; then - log_info "Cleaning generated SKILL.md files..." - - count=0 - while IFS= read -r skill_file; do - if [[ "$DRY_RUN" == true ]]; then - log_warning "Would remove: $skill_file" - else - rm -f "$skill_file" - log_success "Removed: $skill_file" - fi - ((count++)) || true - done < <(find "$AGENTS_DIR" -name "SKILL.md" -type f 2>/dev/null) - - if [[ $count -eq 0 ]]; then - log_info "No SKILL.md files found to clean" - else - log_info "Cleaned $count SKILL.md files" - fi - exit 0 + log_info "Cleaning generated SKILL.md files..." + + count=0 + while IFS= read -r skill_file; do + if [[ "$DRY_RUN" == true ]]; then + log_warning "Would remove: $skill_file" + else + rm -f "$skill_file" + log_success "Removed: $skill_file" + fi + ((count++)) || true + done < <(find "$AGENTS_DIR" -name "SKILL.md" -type f 2>/dev/null) + + if [[ $count -eq 0 ]]; then + log_info "No SKILL.md files found to clean" + else + log_info "Cleaned $count SKILL.md files" + fi + exit 0 fi # ============================================================================= @@ -257,7 +234,7 @@ log_info "Generating Agent Skills SKILL.md files..." log_info "Source: $AGENTS_DIR" if [[ "$DRY_RUN" == true ]]; then - log_warning "DRY RUN - no files will be written" + log_warning "DRY RUN - no files will be written" fi generated=0 @@ -269,25 +246,25 @@ log_info "" log_info "Pattern 1: Folders with parent .md files" while IFS= read -r folder; do - folder_name=$(basename "$folder") - parent_md="$AGENTS_DIR/${folder_name}.md" - skill_file="$folder/SKILL.md" - - # Skip special folders - if [[ "$folder_name" == "scripts" || "$folder_name" == "memory" || "$folder_name" == "templates" ]]; then - continue - fi - - if [[ -f "$parent_md" ]]; then - if [[ "$DRY_RUN" == true ]]; then - log_success "Would generate: $skill_file (from $parent_md)" - else - mkdir -p "$folder" - generate_folder_skill "$folder" "$parent_md" > "$skill_file" - log_success "Generated: $skill_file" - fi - ((generated++)) || true - fi + folder_name=$(basename "$folder") + parent_md="$AGENTS_DIR/${folder_name}.md" + skill_file="$folder/SKILL.md" + + # Skip special folders + if [[ "$folder_name" == "scripts" || "$folder_name" == "memory" || "$folder_name" == "templates" ]]; then + continue + fi + + if [[ -f "$parent_md" ]]; then + if [[ "$DRY_RUN" == true ]]; then + log_success "Would generate: $skill_file (from $parent_md)" + else + mkdir -p "$folder" + generate_folder_skill "$folder" "$parent_md" >"$skill_file" + log_success "Generated: $skill_file" + fi + ((generated++)) || true + fi done < <(find "$AGENTS_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort) # Pattern 2: Nested folders without parent .md but with children @@ -296,55 +273,84 @@ log_info "" log_info "Pattern 2: Nested folders with child .md files" while IFS= read -r folder; do - folder_name=$(basename "$folder") - skill_file="$folder/SKILL.md" - - # Skip if already handled or special - if [[ -f "$skill_file" ]]; then - continue - fi - - # Check if folder has .md files - md_count=$(find "$folder" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l) - if [[ $md_count -gt 0 ]]; then - # Create a minimal SKILL.md for discovery - local_name=$(to_skill_name "$folder_name") - - if [[ "$DRY_RUN" == true ]]; then - log_success "Would generate: $skill_file (folder index)" - else - # Build subskill list - subskills="" - while IFS= read -r subfile; do - subname=$(basename "$subfile" .md) - subdesc=$(extract_frontmatter_field "$subfile" "description") - if [[ -n "$subdesc" ]]; then - subskills="${subskills} -- ${subname}: ${subdesc}" - else - subskills="${subskills} -- ${subname}" - fi - done < <(find "$folder" -maxdepth 1 -name "*.md" -not -name "SKILL.md" -type f 2>/dev/null | sort) - - title=$(capitalize "$folder_name") - { - echo "---" - echo "name: ${local_name}" - echo "description: ${title} tools and utilities" - echo "---" - echo "" - echo "# ${title}" - echo "" - echo "This folder contains ${folder_name} subskills:" - echo "$subskills" - } > "$skill_file" - log_success "Generated: $skill_file" - fi - ((generated++)) || true - fi + folder_name=$(basename "$folder") + skill_file="$folder/SKILL.md" + + # Skip if already handled or special + if [[ -f "$skill_file" ]]; then + continue + fi + + # Check if folder has .md files + md_count=$(find "$folder" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l) + if [[ $md_count -gt 0 ]]; then + local_name=$(to_skill_name "$folder_name") + + if [[ "$DRY_RUN" == true ]]; then + log_success "Would generate: $skill_file (folder index)" + else + # Pure pointer — no inlined subskill lists + title=$(capitalize "$folder_name") + { + echo "---" + echo "name: ${local_name}" + echo "description: ${title} tools and utilities" + echo "---" + echo "" + echo "# ${title}" + echo "" + echo "Browse the .md files in this directory for full instructions." + } >"$skill_file" + log_success "Generated: $skill_file" + fi + ((generated++)) || true + fi done < <(find "$AGENTS_DIR" -mindepth 2 -type d 2>/dev/null | sort) +# Pattern 3: Standalone .md files in nested dirs without matching folders +# e.g., services/hosting/local-hosting.md with no local-hosting/ folder +# These were previously missed, causing discovery gaps. +log_info "" +log_info "Pattern 3: Standalone .md files without matching folders" + +while IFS= read -r md_file; do + filename=$(basename "$md_file" .md) + parent_dir=$(dirname "$md_file") + target_dir="${parent_dir}/${filename}" + skill_file="${target_dir}/SKILL.md" + + # Skip if a matching folder already exists (handled by Pattern 1 or 2) + if [[ -d "$target_dir" ]]; then + continue + fi + + # Skip SKILL.md files, AGENTS.md, README.md, and other non-skill files + if [[ "$filename" == "SKILL" || "$filename" == "AGENTS" || "$filename" == "README" ]]; then + continue + fi + + # Skip files in special top-level directories + rel_path="${md_file#"$AGENTS_DIR"/}" + top_dir="${rel_path%%/*}" + if [[ "$top_dir" == "scripts" || "$top_dir" == "memory" || "$top_dir" == "templates" || "$top_dir" == "configs" || "$top_dir" == "prompts" || "$top_dir" == "plugins" ]]; then + continue + fi + + # Skip top-level .md files (Pattern 1 handles those via folders) + if [[ "$parent_dir" == "$AGENTS_DIR" ]]; then + continue + fi + + if [[ "$DRY_RUN" == true ]]; then + log_success "Would generate: $skill_file (standalone)" + else + mkdir -p "$target_dir" + generate_leaf_skill "$md_file" >"$skill_file" + log_success "Generated: $skill_file" + fi + ((generated++)) || true +done < <(find "$AGENTS_DIR" -mindepth 2 -name "*.md" -not -name "SKILL.md" -not -name "AGENTS.md" -not -name "README.md" -type f 2>/dev/null | sort) + # ============================================================================= # Summary # ============================================================================= @@ -355,8 +361,8 @@ log_info " Generated: $generated SKILL.md files" log_info " Skipped: $skipped (already exist or excluded)" if [[ "$DRY_RUN" == true ]]; then - log_warning "" - log_warning "This was a dry run. Run without --dry-run to generate files." + log_warning "" + log_warning "This was a dry run. Run without --dry-run to generate files." fi exit 0 diff --git a/.agents/scripts/localdev-helper.sh b/.agents/scripts/localdev-helper.sh index 67aaa273f..6ccee7e12 100755 --- a/.agents/scripts/localdev-helper.sh +++ b/.agents/scripts/localdev-helper.sh @@ -6,9 +6,11 @@ set -euo pipefail # Manages dnsmasq, Traefik conf.d, mkcert certs, and port registry # for production-like .local domains with HTTPS on port 443. # -# Coexists with LocalWP: dnsmasq wildcard DNS only resolves domains -# NOT already in /etc/hosts (LocalWP entries take precedence via -# macOS resolver order: /etc/hosts -> /etc/resolver/local -> upstream). +# DNS: /etc/hosts entries are the PRIMARY mechanism for .local domains in +# browsers (macOS mDNS intercepts .local before /etc/resolver/local). +# dnsmasq provides wildcard resolution for CLI tools only. +# Coexists with LocalWP: LocalWP entries (#Local Site) in /etc/hosts +# take precedence; localdev entries use a different marker (# localdev:). SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/shared-constants.sh" 2>/dev/null || true @@ -73,8 +75,8 @@ cmd_init() { echo "" print_success "localdev init complete" - print_info "Verify DNS: dig awardsapp.local @127.0.0.1" - print_info "Verify Traefik: curl -sk https://awardsapp.local" + print_info "Next: localdev-helper.sh add (registers app + /etc/hosts entry)" + print_info "Verify dnsmasq (CLI only): dig testdomain.local @127.0.0.1" return 0 } @@ -172,9 +174,10 @@ configure_resolver() { echo "nameserver 127.0.0.1" | sudo tee "$resolver_file" >/dev/null print_success "Created /etc/resolver/local (nameserver 127.0.0.1)" - # Note about coexistence with /etc/hosts - print_info "DNS resolution order: /etc/hosts → /etc/resolver/local → upstream" - print_info "LocalWP entries in /etc/hosts take precedence over dnsmasq" + # Note about .local mDNS limitation + print_info "Note: /etc/resolver/local enables dnsmasq for CLI tools (dig, curl)" + print_info "Browsers require /etc/hosts entries for .local (mDNS intercepts resolver files)" + print_info "The 'add' command handles /etc/hosts entries automatically" return 0 } @@ -723,10 +726,12 @@ remove_traefik_route() { } # ============================================================================= -# /etc/hosts Fallback +# /etc/hosts Entry (Primary DNS for Browsers) # ============================================================================= -# Add /etc/hosts entry for a domain (fallback when dnsmasq not configured) +# Add /etc/hosts entry for a domain (REQUIRED for .local in browsers) +# macOS reserves .local for mDNS (Bonjour), which intercepts resolution before +# /etc/resolver/local. Only /etc/hosts reliably overrides mDNS for browsers. add_hosts_entry() { local domain="$1" local marker="# localdev: $domain" @@ -737,7 +742,7 @@ add_hosts_entry() { return 0 fi - print_info "Adding /etc/hosts fallback entry for $domain..." + print_info "Adding /etc/hosts entry for $domain (required for browser resolution)..." printf '\n127.0.0.1 %s %s # localdev: %s\n' "$domain" "*.$domain" "$domain" | sudo tee -a /etc/hosts >/dev/null print_success "Added /etc/hosts entry: 127.0.0.1 $domain *.$domain" return 0 @@ -831,13 +836,10 @@ cmd_add() { # Step 4: Create Traefik conf.d route file create_traefik_route "$name" "$port" || exit 1 - # Step 5: Add /etc/hosts fallback if dnsmasq not configured - if ! is_dnsmasq_configured; then - print_info "dnsmasq not configured — adding /etc/hosts fallback entry" - add_hosts_entry "$domain" || true - else - print_info "dnsmasq configured — skipping /etc/hosts fallback" - fi + # Step 5: Add /etc/hosts entry (required for browser resolution of .local) + # macOS mDNS intercepts .local before /etc/resolver/local, so dnsmasq alone + # is insufficient for browsers. /etc/hosts is the only reliable mechanism. + add_hosts_entry "$domain" || true # Step 6: Register in port registry register_app "$name" "$port" "$domain" || exit 1 @@ -2294,17 +2296,19 @@ cmd_help() { echo " 2. Auto-assign port from 3100-3999 (or use specified port)" echo " 3. Generate mkcert wildcard cert (*.name.local + name.local)" echo " 4. Create Traefik conf.d/{name}.yml route file" - echo " 5. Add /etc/hosts fallback entry (if dnsmasq not configured)" + echo " 5. Add /etc/hosts entry (required for browser resolution of .local)" echo " 6. Register in ~/.local-dev-proxy/ports.json" echo "" echo "Remove reverses all add operations." echo "" echo "Init performs:" - echo " 1. Configure dnsmasq with address=/.local/127.0.0.1" - echo " 2. Create /etc/resolver/local (macOS)" + echo " 1. Configure dnsmasq with address=/.local/127.0.0.1 (CLI wildcard resolution)" + echo " 2. Create /etc/resolver/local (routes .local to dnsmasq for CLI tools)" echo " 3. Migrate Traefik from single dynamic.yml to conf.d/ directory" echo " 4. Preserve existing routes (e.g., awardsapp)" echo " 5. Restart Traefik if running" + echo " Note: dnsmasq resolves .local for CLI tools only. Browsers need /etc/hosts" + echo " entries (added automatically by 'add' command) due to macOS mDNS." echo "" echo "Requires: docker, mkcert, dnsmasq (brew install dnsmasq)" echo "Requires: sudo (for /etc/hosts and dnsmasq restart)" diff --git a/.agents/services/hosting/local-hosting.md b/.agents/services/hosting/local-hosting.md index 13e197548..5f0c39cb5 100644 --- a/.agents/services/hosting/local-hosting.md +++ b/.agents/services/hosting/local-hosting.md @@ -57,7 +57,8 @@ localdev-helper.sh add myapp Browser request: https://myapp.local | v - /etc/resolver/local → dnsmasq (127.0.0.1) + /etc/hosts (127.0.0.1 myapp.local) + ← REQUIRED for .local in browsers (mDNS intercepts /etc/resolver) | v Traefik (Docker, ports 80/443/8080) @@ -73,22 +74,35 @@ Browser request: https://myapp.local | Component | Role | Config location | |-----------|------|-----------------| -| **dnsmasq** | Resolves `*.local` → `127.0.0.1` | `$(brew --prefix)/etc/dnsmasq.conf` | -| **macOS resolver** | Routes `.local` DNS queries to dnsmasq | `/etc/resolver/local` | +| **/etc/hosts** | **Primary**: maps `.local` domains to `127.0.0.1` for browsers | `/etc/hosts` | +| **dnsmasq** | Wildcard `*.local` → `127.0.0.1` (CLI tools only) | `$(brew --prefix)/etc/dnsmasq.conf` | +| **macOS resolver** | Routes `.local` to dnsmasq for CLI tools | `/etc/resolver/local` | | **Traefik v3.3** | Reverse proxy, TLS termination, routing | `~/.local-dev-proxy/traefik.yml` | | **mkcert** | Generates browser-trusted wildcard certs | `~/.local-ssl-certs/` | | **Port registry** | Tracks app→port→domain mappings | `~/.local-dev-proxy/ports.json` | | **conf.d/** | Per-app Traefik route files (hot-reloaded) | `~/.local-dev-proxy/conf.d/` | -### DNS Resolution Order (macOS) +### DNS Resolution and the .local mDNS Problem + +macOS reserves `.local` for mDNS (Bonjour/multicast DNS). This creates a resolution conflict: ```text -1. /etc/hosts ← LocalWP entries (#Local Site) win here -2. /etc/resolver/local ← dnsmasq wildcard for .local -3. Upstream DNS ← External domains +Browsers (Chrome, Safari, Firefox): + 1. /etc/hosts ← WORKS — only reliable method for .local + 2. mDNS multicast ← INTERCEPTS .local before resolver files + 3. /etc/resolver/local ← NEVER REACHED for .local in browsers + +CLI tools (dig, curl, etc.): + 1. /etc/hosts ← Checked first + 2. /etc/resolver/local ← Works — routes to dnsmasq + 3. Upstream DNS ← External domains ``` -This order is critical for LocalWP coexistence: domains in `/etc/hosts` always take precedence over dnsmasq. +**Why `localdev add` always writes `/etc/hosts`**: The `/etc/resolver/local` → dnsmasq path only works for CLI tools (`dig`, `curl`). Browsers use the system resolver which sends `.local` queries to mDNS before consulting resolver files. Only `/etc/hosts` entries reliably override mDNS for `.local` domains in browsers. + +**dnsmasq is still useful** for wildcard subdomain resolution in CLI tools (e.g., `dig feature-login.myapp.local @127.0.0.1`), but it cannot serve as the primary DNS mechanism for browser access. + +> **Future consideration**: `.test` (RFC 6761) and `.localhost` (resolves to `127.0.0.1` natively) avoid the mDNS conflict entirely. Switching TLD would be a breaking change for existing projects but would eliminate the `/etc/hosts` requirement. See [RFC 6761](https://www.rfc-editor.org/rfc/rfc6761) and [RFC 6762 Section 3](https://www.rfc-editor.org/rfc/rfc6762#section-3). ### Port Registry Format @@ -126,15 +140,17 @@ localdev-helper.sh init Performs: 1. Check prerequisites (docker, mkcert, dnsmasq) -2. Add `address=/.local/127.0.0.1` to dnsmasq.conf -3. Create `/etc/resolver/local` with `nameserver 127.0.0.1` +2. Add `address=/.local/127.0.0.1` to dnsmasq.conf (for CLI wildcard resolution) +3. Create `/etc/resolver/local` with `nameserver 127.0.0.1` (for CLI tools) 4. Migrate Traefik from single `dynamic.yml` to `conf.d/` directory provider 5. Preserve existing routes (backs up to `~/.local-dev-proxy/backup/`) 6. Restart Traefik if running +Note: `init` sets up dnsmasq for CLI tool resolution (`dig`, `curl`). Browser resolution requires per-app `/etc/hosts` entries, which `add` handles automatically. + ### add -Register a new app with cert, Traefik route, and port assignment. +Register a new app with cert, Traefik route, `/etc/hosts` entry, and port assignment. ```bash localdev-helper.sh add [port] @@ -149,7 +165,7 @@ Performs: 2. Auto-assign port from 3100-3999 (or validate specified port) 3. Generate mkcert wildcard cert (`*.name.local` + `name.local`) 4. Create Traefik route: `conf.d/{name}.yml` -5. Add `/etc/hosts` fallback if dnsmasq not configured +5. Add `/etc/hosts` entry (required for browser resolution of `.local` domains) 6. Register in `ports.json` Result: `https://{name}.local` routes to `http://localhost:{port}` @@ -259,9 +275,11 @@ localhost-helper.sh generate-cert # Generate mkcert cert # App management localhost-helper.sh create-app [ssl] [type] -# LocalWP MCP -localhost-helper.sh start-mcp # Start LocalWP MCP server (port 8085) +# LocalWP MCP (port 8085) +localhost-helper.sh start-mcp # Start LocalWP MCP server localhost-helper.sh stop-mcp # Stop LocalWP MCP server +localhost-helper.sh test-mcp # Test MCP connection +localhost-helper.sh mcp-query "" # Query WordPress database via MCP ``` ## LocalWP Coexistence @@ -594,33 +612,33 @@ networks: ### DNS Resolution -**Symptom**: `https://myapp.local` doesn't resolve. +**Symptom**: `https://myapp.local` doesn't resolve in browser (but `dig myapp.local @127.0.0.1` works). -```bash -# Check dnsmasq is running -pgrep -x dnsmasq +**Most likely cause**: Missing `/etc/hosts` entry. macOS sends `.local` queries to mDNS before consulting `/etc/resolver/local`, so dnsmasq alone is insufficient for browsers. -# Test DNS resolution -dig myapp.local @127.0.0.1 +```bash +# Step 1: Check /etc/hosts entry exists (REQUIRED for browsers) +grep 'myapp.local' /etc/hosts +# Should show: 127.0.0.1 myapp.local *.myapp.local # localdev: myapp -# Verify resolver file -cat /etc/resolver/local -# Should contain: nameserver 127.0.0.1 +# Step 2: If missing, add it +localdev-helper.sh add myapp # Re-running add is safe (idempotent) -# Check dnsmasq config -grep 'address=/.local/' "$(brew --prefix)/etc/dnsmasq.conf" -# Should contain: address=/.local/127.0.0.1 +# Step 3: Flush macOS DNS cache +sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder -# Restart dnsmasq -sudo brew services restart dnsmasq +# Step 4: Verify resolution via system resolver +dscacheutil -q host -a name myapp.local +# Should show: ip_address: 127.0.0.1 -# Flush macOS DNS cache -sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder +# Optional: verify dnsmasq works for CLI tools +dig myapp.local @127.0.0.1 +# Should return: 127.0.0.1 ``` -**Common cause**: macOS mDNSResponder sometimes caches stale results. Flushing the cache usually resolves it. +**Why `dig myapp.local` works but browser doesn't**: `dig` without `@127.0.0.1` uses the system resolver which may hit mDNS for `.local`. `dig @127.0.0.1` queries dnsmasq directly. Browsers use the system resolver, not dnsmasq directly. -**LocalWP conflict**: If a domain resolves to a LocalWP IP instead of 127.0.0.1, check `/etc/hosts` for a conflicting entry. +**LocalWP conflict**: If a domain resolves to a LocalWP IP instead of 127.0.0.1, check `/etc/hosts` for a conflicting `#Local Site` entry. ### Certificate Issues @@ -756,9 +774,9 @@ localdev-helper.sh init ## Legacy Context -The `localhost.md` agent in this directory contains the original localhost development guide. It documents the older `localhost-helper.sh` approach with manual setup steps. For new projects, use `localdev-helper.sh` which automates the full workflow. +The `localhost.md` file in this directory is a redirect stub pointing here. The legacy `localhost-helper.sh` commands are documented in the CLI Reference section above. -Key differences: +Key differences between legacy and current: | Aspect | localhost-helper.sh (legacy) | localdev-helper.sh (current) | |--------|------------------------------|------------------------------| diff --git a/.agents/services/hosting/localhost.md b/.agents/services/hosting/localhost.md index 21d39b912..efc90b7d0 100644 --- a/.agents/services/hosting/localhost.md +++ b/.agents/services/hosting/localhost.md @@ -1,423 +1,17 @@ --- -description: Localhost development environment setup (legacy — see local-hosting.md) +description: "Redirect: see local-hosting.md for all local development hosting" mode: subagent -tools: - read: true - write: true - edit: true - bash: true - glob: true - grep: true - webfetch: false --- -> **Legacy reference**: This file documents the original `localhost-helper.sh` approach. -> For new projects, use [`local-hosting.md`](local-hosting.md) which covers the current -> `localdev-helper.sh` system (dnsmasq + Traefik v3.3 + mkcert + port registry). +# Localhost — Redirected -# Localhost Development Environment Guide +This file has been merged into [`local-hosting.md`](local-hosting.md), which covers both the current `localdev-helper.sh` system and the legacy `localhost-helper.sh` commands. - +Read `local-hosting.md` for: -## Quick Reference - -- **Type**: Local development environment management -- **Config**: `configs/localhost-config.json` -- **Commands**: `localhost-helper.sh [check-port|find-port|list-ports|kill-port|generate-cert|setup-dns|setup-proxy|create-app|start-mcp] [args]` -- **LocalWP**: Sites in `/Users/username/Local Sites`, MCP on port 8085 -- **SSL**: `generate-cert`, `setup-proxy` for local HTTPS via Traefik -- **Ports**: `check-port`, `find-port`, `list-ports`, `kill-port` - -**CRITICAL: Why .local + SSL + Port Checking?** - -| Problem | Solution | Why It Matters | -|---------|----------|----------------| -| Port conflicts | `check-port`, `find-port` | Avoids "address already in use" errors | -| Password managers don't work | SSL via Traefik proxy | 1Password, Bitwarden require HTTPS to autofill | -| Inconsistent URLs | `.local` domains | `myapp.local` instead of `localhost:3847` | -| Browser security warnings | `mkcert` trusted certs | No "proceed anyway" clicks | - -**Standard Setup Pattern:** - -```bash -# 1. Check port availability first -localhost-helper.sh check-port 3000 - -# 2. If conflict, find next available (returns e.g., 3001) -available_port=$(localhost-helper.sh find-port 3000) - -# 3. Create app with .local domain + SSL using available port -localhost-helper.sh create-app myapp myapp.local "$available_port" true docker -``` - - - -Localhost development provides local development capabilities with .local domain support, perfect for development workflows and testing environments. - -## Why .local Domains + SSL + Port Management? - -### The Problem with `localhost:port` - -Traditional local development uses URLs like `http://localhost:3000`. This causes several issues: - -1. **Password managers don't work** - 1Password, Bitwarden, and other password managers require HTTPS to autofill credentials. They won't save or fill passwords on `http://` URLs for security reasons. - -2. **Port conflicts are common** - Multiple projects fighting for port 3000, 8080, etc. leads to "EADDRINUSE" errors and manual port hunting. - -3. **Inconsistent URLs** - Remembering which project is on which port is cognitive overhead. Was it 3000 or 3001? - -4. **No SSL testing** - Can't test SSL-specific features, secure cookies, or HSTS locally. - -### The Solution: .local + SSL + Port Checking - -| Component | Tool | Purpose | -|-----------|------|---------| -| `.local` domains | dnsmasq | Consistent, memorable URLs (`myapp.local`) | -| SSL certificates | mkcert | Browser-trusted HTTPS locally | -| Reverse proxy | Traefik | Routes `.local` domains to correct ports | -| Port management | localhost-helper.sh | Avoids conflicts, finds available ports | - -### Standard Workflow - -**Before starting any local service:** - -```bash -# 1. Check if desired port is available -~/.aidevops/agents/scripts/localhost-helper.sh check-port 3000 - -# 2. If in use, find next available -available_port=$(~/.aidevops/agents/scripts/localhost-helper.sh find-port 3000) -echo "Using port: $available_port" - -# 3. Create app with .local domain + SSL using available port -~/.aidevops/agents/scripts/localhost-helper.sh create-app myapp myapp.local "$available_port" true docker -``` - -**Result:** Access your app at `https://myapp.local` with: -- Password manager autofill working -- No port conflicts -- Browser-trusted SSL -- Consistent URL across sessions - -## Provider Overview - -### **Localhost Characteristics:** - -- **Service Type**: Local development environment management -- **Domain Support**: .local domain resolution and management -- **Development Tools**: Integration with local development stacks -- **SSL Support**: Local SSL certificate management -- **Port Management**: Local port allocation and management -- **Service Discovery**: Local service discovery and routing - -### **Best Use Cases:** - -- **Local WordPress development** with LocalWP integration -- **Microservices development** with local service discovery -- **API development and testing** with local endpoints -- **Frontend development** with local backend services -- **SSL testing** with local certificate management -- **Development environment isolation** and management - -## 🔧 **Configuration** - -### **Setup Configuration:** - -```bash -# Copy template -cp configs/localhost-config.json.txt configs/localhost-config.json - -# Edit with your local development setup -``` - -### **Configuration Structure:** - -```json -{ - "environments": { - "wordpress": { - "type": "localwp", - "sites_path": "/Users/username/Local Sites", - "description": "LocalWP WordPress development", - "mcp_enabled": true, - "mcp_port": 3001 - }, - "nodejs": { - "type": "nodejs", - "projects_path": "/Users/username/Projects", - "description": "Node.js development projects", - "default_port": 3000 - }, - "docker": { - "type": "docker", - "compose_path": "/Users/username/Docker", - "description": "Docker development environments", - "network": "dev-network" - } - } -} -``` - -### **Local Domain Setup:** - -1. **Configure local DNS** resolution for .local domains -2. **Set up SSL certificates** for HTTPS development -3. **Configure port forwarding** for service access -4. **Set up service discovery** for microservices -5. **Test local domain** resolution - -## 🚀 **Usage Examples** - -### **Basic Commands:** - -```bash -# List local environments -./.agents/scripts/localhost-helper.sh environments - -# Start local environment -./.agents/scripts/localhost-helper.sh start wordpress - -# Stop local environment -./.agents/scripts/localhost-helper.sh stop wordpress - -# Get environment status -./.agents/scripts/localhost-helper.sh status wordpress -``` - -### **LocalWP Integration:** - -```bash -# List LocalWP sites -./.agents/scripts/localhost-helper.sh localwp-sites - -# Start LocalWP site -./.agents/scripts/localhost-helper.sh start-site mysite.local - -# Stop LocalWP site -./.agents/scripts/localhost-helper.sh stop-site mysite.local - -# Get site info -./.agents/scripts/localhost-helper.sh site-info mysite.local - -# Start MCP server for LocalWP -./.agents/scripts/localhost-helper.sh start-mcp -``` - -### **SSL Management:** - -```bash -# Generate local SSL certificate -./.agents/scripts/localhost-helper.sh generate-ssl mysite.local - -# Install SSL certificate -./.agents/scripts/localhost-helper.sh install-ssl mysite.local - -# List SSL certificates -./.agents/scripts/localhost-helper.sh list-ssl - -# Renew SSL certificate -./.agents/scripts/localhost-helper.sh renew-ssl mysite.local -``` - -### **Port Management:** - -```bash -# List active ports -./.agents/scripts/localhost-helper.sh list-ports - -# Check port availability -./.agents/scripts/localhost-helper.sh check-port 3000 - -# Kill process on port -./.agents/scripts/localhost-helper.sh kill-port 3000 - -# Forward port -./.agents/scripts/localhost-helper.sh forward-port 3000 8080 -``` - -## 🛡️ **Security Best Practices** - -### **Local Development Security:** - -- **Isolated environments**: Keep development environments isolated -- **SSL certificates**: Use valid SSL certificates for HTTPS testing -- **Access control**: Limit access to development services -- **Data protection**: Protect sensitive development data -- **Network isolation**: Use isolated networks for development - -### **SSL Certificate Management:** - -```bash -# Generate development CA -./.agents/scripts/localhost-helper.sh generate-ca - -# Create site certificate -./.agents/scripts/localhost-helper.sh create-cert mysite.local - -# Trust certificate in system -./.agents/scripts/localhost-helper.sh trust-cert mysite.local - -# Verify certificate -./.agents/scripts/localhost-helper.sh verify-cert mysite.local -``` - -## 🔍 **Troubleshooting** - -### **Common Issues:** - -#### **Domain Resolution Issues:** - -```bash -# Check DNS resolution -nslookup mysite.local -dig mysite.local - -# Verify hosts file -cat /etc/hosts | grep mysite.local - -# Test local connectivity -ping mysite.local -``` - -#### **SSL Certificate Issues:** - -```bash -# Check certificate validity -openssl x509 -in cert.pem -text -noout - -# Verify certificate chain -./.agents/scripts/localhost-helper.sh verify-chain mysite.local - -# Regenerate certificate -./.agents/scripts/localhost-helper.sh regenerate-ssl mysite.local -``` - -#### **Port Conflicts:** - -```bash -# Find process using port -lsof -i :3000 -netstat -tulpn | grep :3000 - -# Kill conflicting process -./.agents/scripts/localhost-helper.sh kill-port 3000 - -# Use alternative port -./.agents/scripts/localhost-helper.sh start-on-port mysite.local 3001 -``` - -## 📊 **Development Workflow** - -### **Environment Management:** - -```bash -# Start development stack -./.agents/scripts/localhost-helper.sh start-stack development - -# Stop development stack -./.agents/scripts/localhost-helper.sh stop-stack development - -# Restart services -./.agents/scripts/localhost-helper.sh restart-services - -# Check service health -./.agents/scripts/localhost-helper.sh health-check -``` - -### **Project Management:** - -```bash -# Create new project -./.agents/scripts/localhost-helper.sh create-project myproject - -# Clone project template -./.agents/scripts/localhost-helper.sh clone-template react-app myproject - -# Set up project environment -./.agents/scripts/localhost-helper.sh setup-env myproject - -# Start project services -./.agents/scripts/localhost-helper.sh start-project myproject -``` - -## 🔄 **Integration & Automation** - -### **LocalWP MCP Integration:** - -```bash -# Start LocalWP MCP server -./.agents/scripts/localhost-helper.sh start-mcp - -# Test MCP connection -./.agents/scripts/localhost-helper.sh test-mcp - -# Query WordPress database via MCP -./.agents/scripts/localhost-helper.sh mcp-query "SELECT * FROM wp_posts LIMIT 5" - -# Stop MCP server -./.agents/scripts/localhost-helper.sh stop-mcp -``` - -### **Docker Integration:** - -```bash -# Start Docker development environment -./.agents/scripts/localhost-helper.sh docker-up myproject - -# Stop Docker environment -./.agents/scripts/localhost-helper.sh docker-down myproject - -# View Docker logs -./.agents/scripts/localhost-helper.sh docker-logs myproject - -# Execute command in container -./.agents/scripts/localhost-helper.sh docker-exec myproject "npm test" -``` - -## 📚 **Best Practices** - -### **Development Environment:** - -1. **Consistent setup**: Use consistent development environments across team -2. **Version control**: Version control development configurations -3. **Documentation**: Document local setup procedures -4. **Automation**: Automate environment setup and teardown -5. **Testing**: Test applications in production-like environments - -### **Local Domain Management:** - -- **Naming conventions**: Use consistent naming for local domains -- **SSL everywhere**: Use SSL for all local development -- **Service discovery**: Implement service discovery for microservices -- **Port management**: Manage port allocation systematically -- **Environment isolation**: Isolate different project environments - -### **Security Practices:** - -- **Certificate management**: Properly manage local SSL certificates -- **Access control**: Limit access to development services -- **Data handling**: Handle sensitive data appropriately in development -- **Network security**: Secure local development networks -- **Regular cleanup**: Regularly clean up unused environments - -## 🎯 **AI Assistant Integration** - -### **Automated Development:** - -- **Environment provisioning**: Automated development environment setup -- **Service orchestration**: Automated service startup and management -- **SSL management**: Automated SSL certificate generation and renewal -- **Port management**: Automated port allocation and conflict resolution -- **Health monitoring**: Automated health checks for development services - -### **Development Assistance:** - -- **Project scaffolding**: Automated project template generation -- **Dependency management**: Automated dependency installation and updates -- **Testing automation**: Automated test execution and reporting -- **Code quality**: Automated code quality checks and formatting -- **Documentation**: Automated documentation generation and updates - ---- - -**Localhost development environment provides comprehensive local development capabilities with excellent integration options for modern development workflows.** 🚀 +- `.local` domain setup (dnsmasq + Traefik + mkcert) +- Port registry and collision detection +- Branch subdomains for worktrees +- Shared Postgres database management +- LocalWP MCP integration +- Legacy `localhost-helper.sh` CLI reference