# =============================================================================
# CUSTOM FUNCTIONS
# =============================================================================

# -----------------------------------------------------------------------------
# Function: funcs (List available .func functions)
# -----------------------------------------------------------------------------
funcs() {
  local func_file="${1:-$HOME/.func}"

  if [ ! -f "$func_file" ]; then
    printf "Error: Function file '%s' not found.\n" "$func_file" >&2
    return 1
  fi

  printf "Available functions in %s:\n" "$func_file"
  awk '
    /^# Function: / {
      line = $0
      sub(/^# Function: /, "", line)

      name = line
      sub(/ .*/, "", name)

      desc = ""
      if (line ~ / \(.+\)$/) {
        desc = line
        sub(/^[^ ]+ \(/, "", desc)
        sub(/\)$/, "", desc)
      }

      if (desc != "") {
        printf "  %-22s %s\n", name, desc
      } else {
        printf "  %s\n", name
      }
    }
  ' "$func_file"
}

# -----------------------------------------------------------------------------
# Function: clip (Cross-platform clipboard)
# -----------------------------------------------------------------------------
clip() {
  local cmd
  local args=()

  if command -v pbcopy >/dev/null 2>&1; then
    cmd="pbcopy" # macOS
  elif grep -qi "microsoft" /proc/version 2>/dev/null && command -v clip.exe >/dev/null 2>&1; then
    cmd="clip.exe" # WSL
  elif [ "$XDG_SESSION_TYPE" = "wayland" ] && command -v wl-copy >/dev/null 2>&1; then
    cmd="wl-copy" # Linux Wayland
  elif command -v xclip >/dev/null 2>&1; then
    cmd="xclip" # Linux X11 (Fallback 1)
    args=("-selection" "clipboard")
  elif command -v xsel >/dev/null 2>&1; then
    cmd="xsel" # Linux X11 (Fallback 2)
    args=("--clipboard" "--input")
  else
    printf "Error: No supported clipboard utility found.\n" >&2
    return 1
  fi

  if [ $# -gt 0 ]; then
    if [ -f "$1" ]; then
      "$cmd" "${args[@]}" < "$1"
      echo "Copied contents of '$1' to clipboard."
    else
      printf "Error: File '%s' not found.\n" "$1" >&2
      return 1
    fi
  else
    "$cmd" "${args[@]}"
  fi
}

# -----------------------------------------------------------------------------
# Function: open_file (Cross-platform file/directory opener)
# -----------------------------------------------------------------------------
open_file() {
    local target="${1:-.}"
    
    if [[ "$OSTYPE" == "darwin"* ]]; then
        command open "$target"
    elif grep -qi "microsoft" /proc/version 2>/dev/null; then
        if command -v wslpath >/dev/null 2>&1 && command -v explorer.exe >/dev/null 2>&1; then
            explorer.exe "$(wslpath -w "$target")"
        else
            printf "Error: 'wslpath' or 'explorer.exe' not found.\n" >&2
            return 1
        fi
    elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
        if command -v xdg-open >/dev/null 2>&1; then
            xdg-open "$target"
        else
            printf "Error: 'xdg-open' not found.\n" >&2
            return 1
        fi
    fi
}
alias open='open_file'

# -----------------------------------------------------------------------------
# Function: ch (Supports shell, Claude, Codex, and OpenCode)
# -----------------------------------------------------------------------------
ch() {
  if [ "$#" -gt 1 ]; then
    printf "Usage: ch [--all | shell | claude | codex | opencode]\n" >&2
    return 2
  fi
  case "${1:-}" in
    --all)
      local result=0 target
      for target in claude codex opencode shell; do
        ch "$target" || result=1
      done

      if [ -t 1 ]; then
        command clear
      fi
      return "$result"
      ;;

    claude)
      local claude_home="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
      if [ -d "$claude_home/projects" ]; then
        command rm -rf -- "$claude_home/projects"/*(DN) || return 1
      fi
      if [ -f "$claude_home/history.jsonl" ]; then
        : > "$claude_home/history.jsonl" || return 1
      fi
      echo "Claude project history/cache cleared."
      ;;

    codex)
      local codex_home="${CODEX_HOME:-$HOME/.codex}"
      command mkdir -p -- "$codex_home" || return 1
      : > "$codex_home/history.jsonl" || return 1

      command rm -rf -- "$codex_home/sessions" "$codex_home/archived_sessions" "$codex_home/shell_snapshots" || return 1
      command mkdir -p -- "$codex_home/sessions" "$codex_home/archived_sessions" "$codex_home/shell_snapshots" || return 1

      echo "Codex history cleared."
      ;;

    opencode)
      local opencode_state_home="${XDG_STATE_HOME:-$HOME/.local/state}/opencode"
      local opencode_data_home="${XDG_DATA_HOME:-$HOME/.local/share}/opencode"

      command mkdir -p -- "$opencode_state_home" "$opencode_data_home" || return 1
      : > "$opencode_state_home/prompt-history.jsonl" || return 1
      command rm -f -- \
        "$opencode_data_home/opencode.db" \
        "$opencode_data_home/opencode.db-shm" \
        "$opencode_data_home/opencode.db-wal" || return 1
      command rm -rf -- "$opencode_data_home/repos" "$opencode_data_home/log" "$opencode_data_home/snapshot" || return 1
      command mkdir -p -- "$opencode_data_home/repos" "$opencode_data_home/log" "$opencode_data_home/snapshot" || return 1

      echo "OpenCode history cleared."
      ;;

    ""|shell)
      # 1. Truncate the file
      if [ -n "${HISTFILE:-}" ]; then
        : > "$HISTFILE" || return 1
      fi

      # 2. Start a fresh Zsh history context. HISTSIZE=0 retains one entry.
      builtin fc -p "${HISTFILE:-}" "$HISTSIZE" "$SAVEHIST" || return 1
      
      echo "Shell history cleared."
      ;;
    *)
      printf "Usage: ch [--all | shell | claude | codex | opencode]\n" >&2
      return 2
      ;;
  esac

  if [ -t 1 ]; then
    command clear
  fi
}

# -----------------------------------------------------------------------------
# Function: require_cli (Check external CLI availability)
# -----------------------------------------------------------------------------
require_cli() {
  local binary="$1"
  local label="$2"
  local resolved

  if command -v whence >/dev/null 2>&1; then
    if whence -p "$binary" >/dev/null 2>&1; then
      return 0
    fi
  else
    resolved="$(command -v "$binary" 2>/dev/null)" || resolved=""
    if [ -n "$resolved" ] && [ -x "$resolved" ]; then
      return 0
    fi
  fi

  printf "Error: %s ('%s') is not installed or not in your PATH.\n" "$label" "$binary" >&2
  return 1
}

# -----------------------------------------------------------------------------
# Function: claude (Includes --yolo and clear shortcuts)
# -----------------------------------------------------------------------------
claude() {
  # Shortcut for clearing cache
  if [[ "$1" == "clear" ]]; then
    ch claude
    return $?
  fi

  # Check if binary exists
  require_cli claude "Claude Code CLI" || return 1

  # Handle --yolo mode
  if [[ "$1" == "--yolo" ]]; then
    shift
    command claude --dangerously-skip-permissions "$@"
    return $?
  fi

  command claude "$@"
}

# -----------------------------------------------------------------------------
# Internal: _prompt_confirm_indexing (Confirm project indexing via the terminal)
# -----------------------------------------------------------------------------
_prompt_confirm_indexing() {
  local project_root="$1"
  local reply

  if ! (: </dev/tty) 2>/dev/null; then
    printf "Notice: No interactive terminal available; enhancing without project indexing.\n" >&2
    return 1
  fi

  printf "Allow Auggie to index and use project context from '%s'? (y/N) " "$project_root" >/dev/tty
  if ! IFS= read -r reply </dev/tty; then
    printf "\nNotice: Unable to read confirmation; enhancing without project indexing.\n" >&2
    return 1
  fi

  case "$reply" in
    y|Y|yes|YES|Yes)
      return 0
      ;;
    *)
      return 1
      ;;
  esac
}

# -----------------------------------------------------------------------------
# Internal: _enhance_prompt (Run Auggie and write only the enhanced prompt)
# -----------------------------------------------------------------------------
_enhance_prompt() {
  local prompt="$1"
  local output_file="$2"
  local run_dir
  local prompt_file
  local workspace
  local cache_dir
  local auth_file
  local log_file
  local parsed_file
  local project_root
  local use_project_context
  local script_pid
  local waited
  local timeout_seconds
  local monitor_status

  if [ -z "$prompt" ]; then
    printf "Error: Prompt was empty.\n" >&2
    return 1
  fi

  if [ ! -s "$HOME/.augment/session.json" ]; then
    printf "Error: Auggie session file not found. Run 'auggie login' first.\n" >&2
    return 1
  fi

  run_dir="$(mktemp -d "${TMPDIR:-/tmp}/auggie-enhance.XXXXXX")" || return 1
  prompt_file="$run_dir/prompt.txt"
  workspace="$run_dir/workspace"
  cache_dir="$run_dir/cache"
  auth_file="$HOME/.augment/session.json"
  log_file="$run_dir/auggie.log"
  parsed_file="$run_dir/enhanced.txt"
  mkdir -p "$workspace" "$cache_dir"
  printf "%s\n" "$prompt" > "$prompt_file"

  project_root="$(git rev-parse --show-toplevel 2>/dev/null)" || project_root="$PWD"
  use_project_context=0
  timeout_seconds=90

  if _prompt_confirm_indexing "$project_root"; then
    use_project_context=1
    timeout_seconds=300
    workspace="$project_root"
    cache_dir="$HOME/.augment"
    printf "Indexing approved; enhancing with project context from '%s'.\n" "$project_root" >&2
  else
    printf "Enhancing without project indexing.\n" >&2
  fi

  (
    if [ "$use_project_context" -eq 1 ]; then
      AUGGIE_PROMPT_FILE="$prompt_file" \
      AUGGIE_WORKSPACE="$workspace" \
      AUGGIE_CACHE_DIR="$cache_dir" \
      AUGGIE_AUTH_FILE="$auth_file" \
        script -q -f -O "$log_file" -c 'auggie --print --enhance-prompt --workspace-root "$AUGGIE_WORKSPACE" --augment-cache-dir "$AUGGIE_CACHE_DIR" --augment-session-json "$AUGGIE_AUTH_FILE" --allow-indexing --wait-for-indexing --dont-save-session --instruction-file "$AUGGIE_PROMPT_FILE"' </dev/null >/dev/null 2>&1 &
    else
      AUGGIE_PROMPT_FILE="$prompt_file" \
      AUGGIE_WORKSPACE="$workspace" \
      AUGGIE_CACHE_DIR="$cache_dir" \
      AUGGIE_AUTH_FILE="$auth_file" \
        script -q -f -O "$log_file" -c 'auggie --print --enhance-prompt --no-discover-workspaces --workspace-root "$AUGGIE_WORKSPACE" --augment-cache-dir "$AUGGIE_CACHE_DIR" --augment-session-json "$AUGGIE_AUTH_FILE" --dont-save-session --instruction-file "$AUGGIE_PROMPT_FILE"' </dev/null >/dev/null 2>&1 &
    fi

    script_pid=$!
    waited=0

    while kill -0 "$script_pid" >/dev/null 2>&1; do
      if grep -aq "🤖" "$log_file" 2>/dev/null || grep -aq "Tool call:" "$log_file" 2>/dev/null; then
        kill "$script_pid" >/dev/null 2>&1 || true
        wait "$script_pid" >/dev/null 2>&1 || true
        exit 0
      fi

      if [ "$waited" -ge "$timeout_seconds" ]; then
        kill "$script_pid" >/dev/null 2>&1 || true
        wait "$script_pid" >/dev/null 2>&1 || true
        exit 124
      fi

      sleep 1
      waited=$((waited + 1))
    done

    wait "$script_pid" >/dev/null 2>&1 || true
    exit 0
  )
  monitor_status=$?

  if [ "$monitor_status" -eq 124 ]; then
    printf "Error: Timed out waiting for Auggie to enhance the prompt.\n" >&2
    rm -rf "$run_dir"
    return 124
  fi

  perl -ne '
    # Auggie redraws streamed terminal lines with carriage returns. Keep only
    # the final rendered segment so stale text cannot corrupt the prompt.
    s/\r$//;
    s/.*\r//;
    1 while s/[^\x08]\x08//g;
    s/\x08//g;
    s/\e\][^\a]*(?:\a|\e\\)//g;
    s/\e\[[0-?]*[ -\/]*[@-~]//g;
    next if /Script started on/ || /Script done on/;
    if (/^(?:✨\s*)?Enhanced prompt:\s*(.*)$/) {
      $capturing = 1;
      $output .= "$1\n" if length $1;
      next;
    }
    next unless $capturing;
    exit if /^🤖/ || /Tool call:/ || /Session terminated/;
    $output .= $_;
    END {
      $output =~ s/^\s*\n//;
      $output =~ s/\s+\z//;
      print "$output\n" if length $output;
    }
  ' "$log_file" > "$parsed_file"

  if [ ! -s "$parsed_file" ]; then
    printf "Error: Auggie did not return an enhanced prompt.\n" >&2
    rm -rf "$run_dir"
    return 1
  fi

  if ! command cp "$parsed_file" "$output_file"; then
    printf "Error: Unable to save the enhanced prompt.\n" >&2
    rm -rf "$run_dir"
    return 1
  fi
  rm -rf "$run_dir"
}

# -----------------------------------------------------------------------------
# Function: prompt (Enhance a prompt with Auggie and optional project context)
# -----------------------------------------------------------------------------
prompt() {
  local prompt
  local line
  local output_file
  local input_keymap
  local input_status

  require_cli auggie "Auggie CLI" || return 1
  require_cli script "script utility" || return 1

  if [ $# -gt 0 ]; then
    prompt="$*"
  elif [ ! -t 0 ]; then
    prompt="$(cat)"
  elif [ -n "${ZSH_VERSION:-}" ]; then
    # Keep all lines in one ZLE buffer so previous lines remain editable.
    input_keymap="auggie-prompt-$$-$RANDOM"
    bindkey -N "$input_keymap" emacs || return 1
    bindkey -M "$input_keymap" '^J' self-insert
    bindkey -M "$input_keymap" '^M' self-insert-unmeta
    bindkey -M "$input_keymap" '^[[13;2u' accept-line
    bindkey -M "$input_keymap" '^[[27;2;13~' accept-line
    bindkey -M "$input_keymap" '^D' accept-line
    printf 'Enter your prompt: Enter adds a line; Shift+Enter (configured terminal) or Ctrl+D submits; Ctrl+C cancels.\n'
    prompt=""
    vared -M "$input_keymap" -p '> ' prompt
    input_status=$?
    bindkey -D "$input_keymap"
    [ "$input_status" -eq 0 ] || return "$input_status"
  else
    echo "Reading prompt... Type 'EOF' on a new line and press Enter when finished."
    prompt=""

    while IFS= read -r line; do
      [ "$line" = "EOF" ] && break
      if [ -z "$prompt" ]; then
        prompt="$line"
      else
        prompt="${prompt}"$'\n'"${line}"
      fi
    done
  fi

  if [ -z "$prompt" ]; then
    printf "Error: Prompt was empty.\n" >&2
    return 1
  fi

  output_file="$(mktemp "${TMPDIR:-/tmp}/auggie-enhanced-prompt.XXXXXX")" || return 1
  if ! _enhance_prompt "$prompt" "$output_file"; then
    rm -f "$output_file"
    return 1
  fi

  if [ -t 1 ]; then
    clear
  fi

  printf "Enhanced prompt:\n"
  command cat "$output_file"
  if clip < "$output_file"; then
    printf '\nEnhanced prompt copied to clipboard.\n' >&2
  else
    printf '\nUnable to copy to clipboard; the enhanced prompt is shown above.\n' >&2
  fi
  rm -f "$output_file"

  command auggie account status
}

# -----------------------------------------------------------------------------
# Function: plan_build (Cached canonical plan-build launcher)
# -----------------------------------------------------------------------------
plan_build() {
  emulate -L zsh
  setopt localtraps
  local base_url="${PLAN_BUILD_BASE_URL:-https://opengist.resetrix.work/weehong/plan-build/raw/HEAD}"
  local cache_dir="${PLAN_BUILD_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/plan-build}"
  local curl_command="${PLAN_BUILD_CURL_COMMAND:-curl}"
  local skill_file="$HOME/.claude/skills/plan-build/SKILL.md"
  local architect_file="$HOME/.claude/skills/plan-build-architect/SKILL.md"
  local stage_dir="" release_dir="" current_tmp=""
  local skill_tmp="" architect_tmp="" skill_backup="" architect_backup=""
  local activation_lock="$cache_dir/.activate.lock"
  local activation_lock_fd=""
  local selected_release="" active_pid="" refresh_rc=1 call_rc=0 interrupted_rc=0

  if ! zmodload zsh/system 2>/dev/null; then
    printf "Error: Unable to load the Zsh system module required by plan-build.\n" >&2
    return 1
  fi

  require_cli zsh "Zsh" || return 1

  if ! command mkdir -p "$cache_dir" "${skill_file:h}" "${architect_file:h}"; then
    printf "Error: Unable to create plan-build cache or skill directories.\n" >&2
    return 1
  fi

  _plan_build_cleanup() {
    if [[ -n "${active_pid:-}" ]] && command kill -0 "$active_pid" 2>/dev/null; then
      command kill "$active_pid" 2>/dev/null || true
      wait "$active_pid" 2>/dev/null || true
    fi
    active_pid=""
    [[ -n "$stage_dir" && -d "$stage_dir" ]] && command rm -rf -- "$stage_dir"
    command rm -f -- "$current_tmp" "$skill_tmp" "$architect_tmp"
    command rm -f -- "$skill_backup" "$architect_backup"
    if [[ -n "${activation_lock_fd:-}" ]]; then
      zsystem flock -u "$activation_lock_fd" 2>/dev/null || true
      activation_lock_fd=""
    fi
  }
  _plan_build_cancel() {
    interrupted_rc="$1"
    [[ -n "${active_pid:-}" ]] && command kill "$active_pid" 2>/dev/null || true
  }
  trap '_plan_build_cleanup' EXIT
  trap '_plan_build_cancel 129' HUP
  trap '_plan_build_cancel 130' INT
  trap '_plan_build_cancel 143' TERM

  _plan_build_validate_release() {
    local candidate="$1"
    [[ -d "$candidate" &&
       -s "$candidate/plan_build.zsh" &&
       -s "$candidate/SKILL.md" &&
       -s "$candidate/ARCHITECT.md" ]] &&
      command zsh -n "$candidate/plan_build.zsh"
  }

  _plan_build_replace_path() {
    local replacement="$1" destination="$2"
    if command mv -fT "$replacement" "$destination" 2>/dev/null; then
      return 0
    fi
    command mv -fh "$replacement" "$destination"
  }

  _plan_build_select_current() {
    local resolved
    [[ -L "$cache_dir/current" ]] || return 1
    resolved="$cache_dir/current"
    resolved="${resolved:A}"
    _plan_build_validate_release "$resolved" || return 1
    selected_release="$resolved"
  }

  _plan_build_acquire_activation_lock() {
    (( interrupted_rc )) && return 1
    command touch "$activation_lock" || return 1
    zsystem flock -t 10 -f activation_lock_fd "$activation_lock"
  }

  _plan_build_release_activation_lock() {
    [[ -n "$activation_lock_fd" ]] || return 0
    zsystem flock -u "$activation_lock_fd" || return 1
    activation_lock_fd=""
  }

  _plan_build_download() {
    local remote="$1" destination="$2" download_rc
    command "$curl_command" -fsSL "$remote" -o "$destination" &
    active_pid=$!
    wait "$active_pid"
    download_rc=$?
    active_pid=""
    (( interrupted_rc )) && return "$interrupted_rc"
    return "$download_rc"
  }

  _plan_build_backup_path() {
    local source="$1" backup="$2"
    [[ -e "$source" || -L "$source" ]] || return 0
    command rm -f -- "$backup" && command cp -RP "$source" "$backup"
  }

  _plan_build_restore_path() {
    local destination="$1" backup="$2" had_old="$3"
    if (( had_old )); then
      _plan_build_replace_path "$backup" "$destination" || return 1
    else
      command rm -f -- "$destination"
    fi
  }

  _plan_build_install_skill_links() {
    local activation_ok=1
    local skill_had_old=0 architect_had_old=0

    skill_tmp="$(command mktemp "${skill_file}.link.XXXXXX")" &&
      command rm -f "$skill_tmp" &&
      command ln -s "$cache_dir/current/SKILL.md" "$skill_tmp" || skill_tmp=""
    architect_tmp="$(command mktemp "${architect_file}.link.XXXXXX")" &&
      command rm -f "$architect_tmp" &&
      command ln -s "$cache_dir/current/ARCHITECT.md" "$architect_tmp" || architect_tmp=""
    [[ -n "$skill_tmp" && -L "$skill_tmp" &&
       -n "$architect_tmp" && -L "$architect_tmp" ]] || return 1

    skill_backup="${skill_file}.backup.$$.$RANDOM"
    architect_backup="${architect_file}.backup.$$.$RANDOM"
    [[ -e "$skill_file" || -L "$skill_file" ]] && skill_had_old=1
    [[ -e "$architect_file" || -L "$architect_file" ]] && architect_had_old=1
    _plan_build_backup_path "$skill_file" "$skill_backup" || activation_ok=0
    (( activation_ok )) && _plan_build_backup_path "$architect_file" "$architect_backup" || activation_ok=0
    (( activation_ok && ! interrupted_rc )) &&
      _plan_build_replace_path "$skill_tmp" "$skill_file" || activation_ok=0
    (( activation_ok )) && skill_tmp=""
    (( activation_ok && ! interrupted_rc )) &&
      _plan_build_replace_path "$architect_tmp" "$architect_file" || activation_ok=0
    (( activation_ok )) && architect_tmp=""

    if (( activation_ok )); then
      command rm -f -- "$skill_backup" "$architect_backup"
      skill_backup="" architect_backup=""
      return 0
    fi

    if ! _plan_build_restore_path "$skill_file" "$skill_backup" "$skill_had_old"; then
      printf "Error: Unable to restore plan-build skill; backup preserved at %s.\n" "$skill_backup" >&2
      skill_backup=""
    fi
    if ! _plan_build_restore_path "$architect_file" "$architect_backup" "$architect_had_old"; then
      printf "Error: Unable to restore architect skill; backup preserved at %s.\n" "$architect_backup" >&2
      architect_backup=""
    fi
    return 1
  }

  if (( $+commands[$curl_command] )) || [[ "$curl_command" == */* && -x "$curl_command" ]]; then
    stage_dir="$(command mktemp -d "$cache_dir/.stage.XXXXXX")" || stage_dir=""
    if [[ -n "$stage_dir" ]]; then
      _plan_build_download "$base_url/plan_build.zsh" "$stage_dir/plan_build.zsh" &&
        _plan_build_download "$base_url/SKILL.md" "$stage_dir/SKILL.md" &&
        _plan_build_download "$base_url/ARCHITECT.md" "$stage_dir/ARCHITECT.md"
      refresh_rc=$?
      if (( interrupted_rc )); then
        _plan_build_cleanup
        trap - EXIT HUP INT TERM
        return "$interrupted_rc"
      fi
    else
      refresh_rc=1
    fi
    if (( refresh_rc == 0 )) &&
       _plan_build_validate_release "$stage_dir"; then
      release_dir="$(command mktemp -d "$cache_dir/release.XXXXXX")" || release_dir=""
      if [[ -n "$release_dir" ]] &&
         command rmdir "$release_dir" &&
         command mv "$stage_dir" "$release_dir" &&
         _plan_build_acquire_activation_lock; then
        stage_dir=""
        release_dir="${release_dir:A}"
        current_tmp="$(command mktemp "$cache_dir/.current.XXXXXX")" &&
          command rm -f "$current_tmp" &&
          command ln -s "$release_dir" "$current_tmp" || current_tmp=""

        if [[ -n "$current_tmp" && -L "$current_tmp" ]] &&
           _plan_build_install_skill_links &&
           _plan_build_replace_path "$current_tmp" "$cache_dir/current"; then
          current_tmp=""
          selected_release="$release_dir"
        fi
        _plan_build_release_activation_lock || true
      fi
    fi
  fi

  if (( interrupted_rc )); then
    _plan_build_cleanup
    trap - EXIT HUP INT TERM
    return "$interrupted_rc"
  fi

  if [[ -z "$selected_release" ]]; then
    if ! _plan_build_select_current; then
      if (( ! $+commands[$curl_command] )) && [[ ! ( "$curl_command" == */* && -x "$curl_command" ) ]]; then
        printf "Error: curl is required because no valid cached plan-build bundle is available.\n" >&2
      else
        printf "Error: Unable to refresh plan-build and no valid cached bundle is available.\n" >&2
      fi
      return 1
    fi
    if (( ! $+commands[$curl_command] )) && [[ ! ( "$curl_command" == */* && -x "$curl_command" ) ]]; then
      printf "Warning: curl is unavailable; using the validated cached plan-build bundle.\n" >&2
    else
      printf "Warning: Unable to refresh plan-build; using the validated cached bundle.\n" >&2
    fi
  fi

  if [[ "$(command readlink "$skill_file" 2>/dev/null)" != "$cache_dir/current/SKILL.md" ||
        "$(command readlink "$architect_file" 2>/dev/null)" != "$cache_dir/current/ARCHITECT.md" ]]; then
    if ! _plan_build_acquire_activation_lock || ! _plan_build_install_skill_links; then
      _plan_build_release_activation_lock 2>/dev/null || true
      if (( interrupted_rc )); then
        _plan_build_cleanup
        trap - EXIT HUP INT TERM
        return "$interrupted_rc"
      fi
      printf "Error: Unable to install the plan-build skill links.\n" >&2
      return 1
    fi
    _plan_build_release_activation_lock || true
  fi

  if (( interrupted_rc )); then
    _plan_build_cleanup
    trap - EXIT HUP INT TERM
    return "$interrupted_rc"
  fi

  PLAN_BUILD_SKILL_PATH="$selected_release/SKILL.md" \
  PLAN_BUILD_ARCHITECT_SKILL_PATH="$selected_release/ARCHITECT.md" \
    command zsh "$selected_release/plan_build.zsh" "$@" &
  active_pid=$!
  wait "$active_pid"
  call_rc=$?
  active_pid=""
  (( interrupted_rc )) && call_rc="$interrupted_rc"
  _plan_build_cleanup
  trap - EXIT HUP INT TERM
  return "$call_rc"
}

# -----------------------------------------------------------------------------
# Internal: _aicommit_usage (Print aicommit usage)
# -----------------------------------------------------------------------------
_aicommit_usage() {
  printf "Usage: aicommit [--modules] [--yes] [claude | copilot | opencode]\n"
  printf "  --modules  Force per-module commits (automatic above 30 staged paths)\n"
  printf "  --yes      Skip the plan confirmation for per-module commits\n"
}

# -----------------------------------------------------------------------------
# Internal: _aicommit_staged_diff (Staged diff without lock/minified files)
# -----------------------------------------------------------------------------
_aicommit_staged_diff() {
  # Exclude lock files/minified files and cap at ~100k characters to prevent CLI crashes
  git diff --cached -- . ":(exclude)*.lock" ":(exclude)*-lock.json" ":(exclude)*.min.js" | head -c 100000
}

# -----------------------------------------------------------------------------
# Internal: _aicommit_opencode_model (Read OpenCode's most recent model selection)
# -----------------------------------------------------------------------------
_aicommit_opencode_model() {
  python3 - <<'PY'
import json
import os
from pathlib import Path

state = Path(os.environ.get("XDG_STATE_HOME") or Path.home() / ".local/state")
try:
    data = json.loads((state / "opencode/model.json").read_text())
    recent = data.get("recent", [])
    if recent:
        selected = recent[0]
        model = selected["providerID"] + "/" + selected["modelID"]
        variant = data.get("variant", {}).get(model)
        if variant and variant != "default":
            model += "#" + variant
        print(model)
except FileNotFoundError:
    pass  # Let OpenCode use its configured default on a fresh installation.
PY
}

# -----------------------------------------------------------------------------
# Internal: _aicommit_engine (Ask an AI CLI to commit or to plan a split)
# -----------------------------------------------------------------------------
_aicommit_engine() {
  local engine="$1"
  local mode="$2"
  local input="$3"
  local name
  local selected_model
  local -a model_args
  model_args=()

  if [ "$engine" = "opencode" ]; then
    selected_model="$(_aicommit_opencode_model)" || return 1
    if [ -n "$selected_model" ]; then
      model_args=(--model "$selected_model")
    fi
  fi

  case "$engine" in
    claude) name="Claude" ;;
    copilot) name="Copilot" ;;
    opencode) name="OpenCode" ;;
  esac

  # Status goes to stderr so plan output on stdout stays clean JSON.
  if [ "$mode" = "plan" ]; then
    printf "🧩 %s is planning per-module commits...\n" "$name" >&2
  else
    printf "🤖 %s is analyzing staged changes...\n" "$name" >&2
  fi

  case "$engine:$mode" in
    claude:commit)
      printf "%s\n" "$input" \
        | command claude --allowedTools 'Bash(git commit *)' --permission-mode dontAsk -p \
          "Read the provided instructions and diff, then generate and execute the commit."
      ;;
    claude:plan)
      printf "%s\n" "$input" \
        | command claude --permission-mode dontAsk -p \
          "Read the provided instructions and staged changes, then reply with only the JSON plan."
      ;;
    copilot:commit)
      copilot -p "$input" -s --no-ask-user --allow-tool='shell(git:*)'
      ;;
    copilot:plan)
      copilot -p "$input" -s --no-ask-user
      ;;
    opencode:commit)
      printf "%s\n" "$input" | opencode run --agent ai-commit "${model_args[@]}"
      ;;
    opencode:plan)
      printf "%s\n" "$input" | opencode run --agent ai-commit-plan "${model_args[@]}"
      ;;
  esac
}

# -----------------------------------------------------------------------------
# Internal: _aicommit_restage (Stage HEAD plus plan groups first..last from a tree)
# -----------------------------------------------------------------------------
_aicommit_restage() {
  local tree="$1"
  local run_dir="$2"
  local first="$3"
  local last="$4"
  local number

  if git rev-parse -q --verify HEAD >/dev/null; then
    git read-tree HEAD || return 1
  else
    git read-tree --empty || return 1
  fi

  [ "$first" -le "$last" ] || return 0

  number="$first"
  while [ "$number" -le "$last" ]; do
    cat "$run_dir/$(printf "%03d" "$number").files"
    number=$((number + 1))
  done | git --literal-pathspecs restore --staged --source="$tree" --pathspec-from-file=- --pathspec-file-nul
}

# -----------------------------------------------------------------------------
# Internal: _aicommit_plan (Validate the AI split plan into per-group files)
# -----------------------------------------------------------------------------
_aicommit_plan() {
  local run_dir="$1"

  python3 -c '
import json, os, re, sys

sys.stdout.reconfigure(errors="backslashreplace")
sys.stderr.reconfigure(errors="backslashreplace")
run_dir = sys.argv[1]
types = {"feat", "fix", "docs", "style", "refactor", "perf", "test", "build", "ci", "chore"}

with open(os.path.join(run_dir, "staged"), "rb") as fh:
    staged = [p for p in fh.read().decode("utf-8", "surrogateescape").split("\0") if p]
with open(os.path.join(run_dir, "plan.out"), encoding="utf-8", errors="replace") as fh:
    raw = re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", fh.read())

# Take the last {"commits": [...]} object in the reply, ignoring fences and prose.
plan, decoder, pos = None, json.JSONDecoder(), raw.find("{")
while pos != -1:
    try:
        obj, end = decoder.raw_decode(raw, pos)
    except ValueError:
        pos = raw.find("{", pos + 1)
        continue
    if isinstance(obj, dict) and isinstance(obj.get("commits"), list):
        plan = obj
        pos = raw.find("{", end)
    else:
        pos = raw.find("{", pos + 1)

if plan is None:
    sys.exit("❌ Error: The AI reply did not contain a JSON commit plan.")

# Every staged path must land in exactly one group; anything unassigned goes last.
staged_set, seen, groups, warnings = set(staged), set(), [], []
for commit in plan["commits"]:
    if not isinstance(commit, dict) or not isinstance(commit.get("files"), list):
        continue
    files = []
    for path in commit["files"]:
        if not isinstance(path, str):
            continue
        if path not in staged_set:
            warnings.append("ignoring path that is not staged: " + path)
        elif path in seen:
            warnings.append("ignoring duplicate path: " + path)
        else:
            seen.add(path)
            files.append(path)
    if not files:
        continue
    kind = str(commit.get("type", "")).strip().lower()
    scope = re.sub(r"[^a-z0-9._/-]", "", str(commit.get("scope", "")).lower())
    label = ""
    if kind in types:
        label = kind + ("(" + scope + ")" if scope else "")
    groups.append((label, files))

missing = [p for p in staged if p not in seen]
if missing:
    warnings.append("%d staged path(s) were not planned; adding them as a final commit" % len(missing))
    groups.append(("", missing))

if len(staged) > 30 and len(groups) < 2:
    sys.exit("❌ Error: Changes affecting more than 30 staged paths require multiple module commits.")

for number, (label, files) in enumerate(groups, 1):
    base = os.path.join(run_dir, "%03d" % number)
    with open(base + ".files", "wb") as fh:
        fh.write("".join(p + "\0" for p in files).encode("utf-8", "surrogateescape"))
    with open(base + ".label", "w") as fh:
        fh.write(label)
with open(os.path.join(run_dir, "count"), "w") as fh:
    fh.write(str(len(groups)))

for warning in warnings:
    print("⚠️  " + warning, file=sys.stderr)
print("📋 Planned commits:")
for number, (label, files) in enumerate(groups, 1):
    print("  %d. %s (%d file%s)" % (number, label or "unplanned", len(files), "" if len(files) == 1 else "s"))
    for path in files:
        print("       " + path)
' "$run_dir"
}

# -----------------------------------------------------------------------------
# Internal: _aicommit_modules_run (Plan, confirm, and commit each module group)
# -----------------------------------------------------------------------------
_aicommit_modules_run() {
  local engine="$1"
  local assume_yes="$2"
  local run_dir="$3"
  local prompt_file="$HOME/.config/ai-commit-prompt.txt"
  local split_prompt_file="$HOME/.config/ai-commit-split-prompt.txt"
  local tree
  local total
  local current
  local group
  local label
  local hint
  local diff
  local expected
  local head_before
  local head_after
  local unexpected
  local reply

  if ! tree="$(git write-tree 2>/dev/null)"; then
    printf "❌ Error: Unable to snapshot the index (unresolved conflicts?).\n" >&2
    return 1
  fi

  git diff --cached --name-only --no-renames -z > "$run_dir/staged" || return 1
  if [ ! -s "$run_dir/staged" ]; then
    printf "❌ Error: Nothing staged to commit.\n" >&2
    return 1
  fi

  _aicommit_engine "$engine" plan "$(cat "$split_prompt_file")

Here are the staged files:
$(git -c core.quotePath=false diff --cached --name-status -M)

Here is the diffstat:
$(git -c core.quotePath=false diff --cached --numstat -M)

Here is the git diff:
$(_aicommit_staged_diff)" > "$run_dir/plan.out"

  if ! _aicommit_plan "$run_dir"; then
    printf "Last lines of the AI reply:\n" >&2
    tail -n 20 "$run_dir/plan.out" >&2
    return 1
  fi
  total="$(cat "$run_dir/count")"

  if [ "$assume_yes" -ne 1 ]; then
    if ! (: </dev/tty) 2>/dev/null; then
      printf "❌ Error: No interactive terminal to confirm the plan; rerun with --yes.\n" >&2
      return 1
    fi
    printf "Create these %s commit(s)? (Y/n) " "$total" >/dev/tty
    IFS= read -r reply </dev/tty || reply="n"
    case "$reply" in
      ""|y|Y|yes|YES|Yes) ;;
      *)
        printf "Aborted. Staged changes are untouched.\n"
        return 1
        ;;
    esac
  fi

  printf "💾 Staged snapshot: %s (recover with: git read-tree %s)\n" "$tree" "$tree"
  current=1
  trap '_aicommit_restage "$tree" "$run_dir" "$current" "$total"; printf "\n⚠️  Interrupted. Uncommitted groups are staged again.\n" >&2; rm -rf "$run_dir"; exit 130' INT TERM

  while [ "$current" -le "$total" ]; do
    group="$run_dir/$(printf "%03d" "$current").files"
    label="$(cat "$run_dir/$(printf "%03d" "$current").label")"

    if ! _aicommit_restage "$tree" "$run_dir" "$current" "$current"; then
      printf "❌ Error: Unable to stage commit %s/%s.\n" "$current" "$total" >&2
      _aicommit_restage "$tree" "$run_dir" "$current" "$total"
      return 1
    fi

    expected="$(git write-tree)"
    head_before="$(git rev-parse -q --verify HEAD)"
    diff="$(_aicommit_staged_diff)"
    [ -n "$diff" ] || diff="$(git diff --cached --stat)"

    hint="This is commit $current of $total, split by module."
    if [ -n "$label" ]; then
      hint="$hint The planned type and scope is $label; use it unless the diff clearly calls for another."
    fi

    printf "\n[%s/%s] %s\n" "$current" "$total" "${label:-unplanned}"
    _aicommit_engine "$engine" commit "$(cat "$prompt_file")

$hint

Here is the git diff:
$diff"

    head_after="$(git rev-parse -q --verify HEAD)"
    if [ -z "$head_after" ] || [ "$head_after" = "$head_before" ]; then
      printf "❌ Error: No commit was created for group %s/%s.\n" "$current" "$total" >&2
      _aicommit_restage "$tree" "$run_dir" "$current" "$total"
      printf "Uncommitted groups are staged again.\n" >&2
      return 1
    fi

    # Hooks may reformat this group's files, but nothing outside it may sneak in.
    unexpected="$(LC_ALL=C comm -13 \
      <(tr '\0' '\n' < "$group" | LC_ALL=C sort) \
      <(git diff --name-only --no-renames -z "$expected" HEAD | tr '\0' '\n' | LC_ALL=C sort))"
    if [ -n "$unexpected" ]; then
      printf "❌ Error: Commit %s/%s included files outside its group:\n%s\n" "$current" "$total" "$unexpected" >&2
      _aicommit_restage "$tree" "$run_dir" "$((current + 1))" "$total"
      printf "Inspect it with 'git show'; undo it with 'git reset --soft HEAD~1'.\n" >&2
      return 1
    fi

    current=$((current + 1))
  done

  trap - INT TERM
  printf "\n✅ Created %s commit(s):\n" "$total"
  git --no-pager log --oneline -n "$total"
}

# -----------------------------------------------------------------------------
# Internal: _aicommit_modules (Split staged changes into per-module commits)
# -----------------------------------------------------------------------------
_aicommit_modules() {
  local engine="$1"
  local assume_yes="$2"
  local top
  local run_dir
  local rc

  if [ ! -f "$HOME/.config/ai-commit-split-prompt.txt" ]; then
    printf "❌ Error: Configuration file not found at ~/.config/ai-commit-split-prompt.txt\n" >&2
    return 1
  fi

  require_cli python3 "Python 3" || return 1

  if ! top="$(git rev-parse --show-toplevel 2>/dev/null)"; then
    printf "❌ Error: Not inside a git work tree.\n" >&2
    return 1
  fi

  run_dir="$(mktemp -d "${TMPDIR:-/tmp}/aicommit.XXXXXX")" || return 1

  # Subshell: run from the repo root and keep the traps away from the caller.
  (
    trap 'rm -rf "$run_dir"; exit 130' INT TERM
    cd "$top" || exit 1
    _aicommit_modules_run "$engine" "$assume_yes" "$run_dir"
  )
  rc=$?
  rm -rf "$run_dir"
  return "$rc"
}

# -----------------------------------------------------------------------------
# Function: aicommit (Automated Conventional Commit Engine Wrapper)
# -----------------------------------------------------------------------------
aicommit() {
  local engine="claude"
  local modules=0
  local assume_yes=0
  local arg
  local diff
  local staged_count
  local prompt_file="$HOME/.config/ai-commit-prompt.txt"

  for arg in "$@"; do
    case "$arg" in
      claude|copilot|opencode) engine="$arg" ;;
      --modules) modules=1 ;;
      -y|--yes) assume_yes=1 ;;
      -h|--help)
        _aicommit_usage
        return 0
        ;;
      *)
        printf "❌ Error: Invalid argument '%s'\n" "$arg" >&2
        _aicommit_usage >&2
        return 1
        ;;
    esac
  done

  if [ ! -f "$prompt_file" ]; then
    printf "❌ Error: Configuration file not found at ~/.config/ai-commit-prompt.txt\n" >&2
    printf "Please create it and paste your system prompt inside.\n" >&2
    return 1
  fi

  # Count the full index, including excluded previews and both sides of renames.
  # NUL delimiters keep whitespace/newlines in filenames from skewing the count.
  staged_count="$(set -o pipefail; git diff --cached --name-only --no-renames -z | tr -cd '\0' | wc -c)" || return 1
  if [ "$staged_count" -gt 30 ] && [ "$modules" -ne 1 ]; then
    printf "🧩 %s staged paths exceed 30; splitting commits by module.\n" "$staged_count" >&2
  fi
  if [ "$modules" -eq 1 ] || [ "$staged_count" -gt 30 ]; then
    _aicommit_modules "$engine" "$assume_yes"
    return
  fi

  diff="$(_aicommit_staged_diff)"
  if [ -z "$diff" ]; then
    printf "❌ Error: Nothing staged to commit.\n" >&2
    return 1
  fi

  _aicommit_engine "$engine" commit "$(cat "$prompt_file")

Staged path count: $staged_count

Here is the git diff:
$diff"
}

# -----------------------------------------------------------------------------
# Function: create (Efficiently create file + parent dirs)
# -----------------------------------------------------------------------------
create() {
  if ! command -v install >/dev/null 2>&1; then
    printf "Error: 'install' coreutil is not available.\n" >&2
    return 1
  fi

  if [ $# -eq 0 ]; then
    printf "Usage: create <file> [file ...]\n" >&2
    return 2
  fi

  for p in "$@"; do
    # Expand tilde manually if shell doesn't
    [[ "$p" == "~/"* ]] && p="${HOME}/${p#\~/}"

    if [ -z "$p" ] || [ "$p" = "/" ]; then
      printf "create: refusing to operate on '%s'\n" "$p" >&2
      continue
    fi

    if [ -e "$p" ]; then
      printf "create: '%s' already exists. Skipping.\n" "$p" >&2
      continue
    fi

    if install -D /dev/null "$p"; then
      echo "Created '$p'"
    fi
  done
}

# -----------------------------------------------------------------------------
# Function: ckp (Kill or check the process using a port)
# -----------------------------------------------------------------------------
ckp() {
  local action="kill"
  local signal="TERM"
  local port
  local pids
  local pid

  case "${1:-}" in
    -c|--check)
      action="check"
      shift
      ;;
    -k|--kill)
      action="kill"
      shift
      ;;
    -f|--force)
      action="kill"
      signal="KILL"
      shift
      ;;
    -h|--help|"")
      printf "Usage: ckp [--check|-c|--kill|-k|--force|-f] <port_number>\n" >&2
      return 1
      ;;
  esac

  port="${1:-}"
  if [[ ! "$port" =~ ^[0-9]+$ ]] || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then
    printf "Error: port must be a number between 1 and 65535.\n" >&2
    printf "Usage: ckp [--check|-c|--kill|-k|--force|-f] <port_number>\n" >&2
    return 1
  fi

  if [[ "$OSTYPE" == "darwin"* ]]; then
    pids="$(
      {
        lsof -nP -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null
        lsof -nP -tiUDP:"$port" 2>/dev/null
      } | sort -u
    )"

    if [ -z "$pids" ]; then
      printf "Port %s is FREE\n" "$port"
      return 0
    fi

    printf "Port %s is in use:\n" "$port"
    lsof -nP -iTCP:"$port" -sTCP:LISTEN -iUDP:"$port" 2>/dev/null
  else
    local port_hex
    local inodes

    port_hex="$(printf "%04X" "$port")"
    inodes="$(
      awk -v port="$port_hex" '
        NR > 1 {
          split($2, local_addr, ":")
          if (toupper(local_addr[length(local_addr)]) == port && ($4 == "0A" || FILENAME ~ /udp/)) {
            print $10
          }
        }
      ' /proc/net/tcp /proc/net/tcp6 /proc/net/udp /proc/net/udp6 2>/dev/null | sort -u
    )"

    if [ -z "$inodes" ]; then
      printf "Port %s is FREE\n" "$port"
      return 0
    fi

    pids=""
    pids="$(
      find /proc/[0-9]*/fd -maxdepth 1 -type l -printf "%p %l\n" 2>/dev/null |
        awk -v inodes="$inodes" '
          BEGIN {
            split(inodes, inode_list, " ")
            for (i in inode_list) {
              wanted["socket:[" inode_list[i] "]"] = 1
            }
          }
          $2 in wanted {
            split($1, path, "/")
            print path[3]
          }
        ' | sort -u
    )"

    if [ -z "$pids" ]; then
      printf "Port %s is in use, but no readable process owner was found.\n" "$port" >&2
      return 1
    fi

    printf "Port %s is in use:\n" "$port"
    for pid in $pids; do
      if [ -r "/proc/$pid/cmdline" ]; then
        printf "  PID %-8s %s\n" "$pid" "$(tr '\0' ' ' < "/proc/$pid/cmdline")"
      else
        printf "  PID %s\n" "$pid"
      fi
    done
  fi

  if [ "$action" = "kill" ]; then
    for pid in $pids; do
      if kill "-$signal" "$pid" 2>/dev/null; then
        printf "Killed PID %s with SIG%s.\n" "$pid" "$signal"
      else
        printf "Error: failed to kill PID %s. Try with sudo or --force.\n" "$pid" >&2
        return 1
      fi
    done
  fi
}

# -----------------------------------------------------------------------------
# Function: append_path (Adds to session PATH if exists)
# -----------------------------------------------------------------------------
append_path() {
    local dir="$1"
    if [ -d "$dir" ]; then
        case ":$PATH:" in
            *":$dir:"*) ;; 
            *) PATH="$PATH:$dir" ;;
        esac
    elif [ "${SUPPRESS_WARNINGS:-0}" -ne 1 ]; then
        echo "Warning: $dir does not exist." >&2
    fi
}

# -----------------------------------------------------------------------------
# Function: add_path_to_config (Fixed awk/sed injection)
# -----------------------------------------------------------------------------
add_path_to_config() {
  local new_path="$1"
  local config_file="$HOME/.bashrc"
  [[ "$SHELL" == *"zsh"* ]] && config_file="$HOME/.zshrc"

  if [ -z "$new_path" ]; then
    printf "Usage: add_path_to_config <directory>\n" >&2
    return 1
  fi

  new_path="${new_path/#\~/$HOME}"

  if [ ! -d "$new_path" ]; then
    printf "Warning: '%s' does not exist. Add anyway? (y/N) " "$new_path"
    read -r REPLY
    [[ ! $REPLY =~ ^[Yy]$ ]] && return 1
  fi

  # Check if already exists in file
  if grep -Fq "append_path \"$new_path\"" "$config_file"; then
    printf "Notice: Already in %s.\n" "$config_file"
    return 0
  fi

  # Robust Injection: Use sed to insert before export PATH or just append
  if grep -q "^export PATH" "$config_file"; then
    sed -i.bak "/^export PATH/i append_path \"$new_path\"" "$config_file"
  else
    echo "append_path \"$new_path\"" >> "$config_file"
    echo "export PATH" >> "$config_file"
  fi

  printf "✅ Success: Added to %s\n" "$config_file"
  append_path "$new_path" && export PATH
}