# ============================================================================= # 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: clear_history (Supports shell, Claude, Codex, and OpenCode) # ----------------------------------------------------------------------------- clear_history() { case "${1:-}" in claude) if [ -d "$HOME/.claude/projects" ]; then # Use 'yes' to skip prompts and -f to ignore non-existent files yes | rm -rf "$HOME/.claude/projects"/* echo "Claude project history/cache cleared." fi ;; codex) local codex_home="${CODEX_HOME:-$HOME/.codex}" local codex_session_file local codex_session_id mkdir -p "$codex_home" : > "$codex_home/history.jsonl" if command -v codex >/dev/null 2>&1; then find "$codex_home/sessions" "$codex_home/archived_sessions" -type f -name '*.jsonl' -print 2>/dev/null | while IFS= read -r codex_session_file; do codex_session_id="$(sed -n 's/.*"session_id":"\([^"]*\)".*/\1/p; q' "$codex_session_file")" if [ -n "$codex_session_id" ]; then codex delete --force "$codex_session_id" >/dev/null 2>&1 || true fi done fi rm -rf "$codex_home/sessions" "$codex_home/archived_sessions" "$codex_home/shell_snapshots" mkdir -p "$codex_home/sessions" "$codex_home/archived_sessions" "$codex_home/shell_snapshots" 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" mkdir -p "$opencode_state_home" "$opencode_data_home" : > "$opencode_state_home/prompt-history.jsonl" rm -f \ "$opencode_data_home/opencode.db" \ "$opencode_data_home/opencode.db-shm" \ "$opencode_data_home/opencode.db-wal" rm -rf "$opencode_data_home/repos" "$opencode_data_home/log" mkdir -p "$opencode_data_home/repos" "$opencode_data_home/log" echo "OpenCode history cleared." ;; *) # 1. Truncate the file : > "$HISTFILE" # 2. Clear RAM by briefly setting history size to 0 local old_histsize=$HISTSIZE HISTSIZE=0 HISTSIZE=$old_histsize echo "Shell history cleared." ;; esac } # ----------------------------------------------------------------------------- # 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 clear_history claude return 0 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/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 &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 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 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 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)" 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" 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" } # ----------------------------------------------------------------------------- # Function: aicommit (Automated Conventional Commit Engine Wrapper) # ----------------------------------------------------------------------------- aicommit() { local diff engine prompt # 1. Get diff safely: exclude lock files/minified files and cap at ~100k characters to prevent CLI crashes diff=$(git diff --cached -- . ":(exclude)*.lock" ":(exclude)*-lock.json" ":(exclude)*.min.js" | head -c 100000) if [ -z "$diff" ]; then printf "❌ Error: Nothing staged to commit.\n" >&2 return 1 fi if [ ! -f "$HOME/.config/ai-commit-prompt.txt" ]; 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 engine=${1:-claude} prompt=$(cat "$HOME/.config/ai-commit-prompt.txt") case "$engine" in claude) echo "🤖 Claude is analyzing staged changes..." cat "$HOME/.config/ai-commit-prompt.txt" <(echo -e "\n\nHere is the git diff:\n$diff") \ | command claude --allowedTools 'Bash(git commit *)' --permission-mode dontAsk -p \ "Read the provided instructions and diff, then generate and execute the commit." ;; codex) echo "🤖 Codex is analyzing staged changes..." cat "$HOME/.config/ai-commit-prompt.txt" <(echo -e "\n\nHere is the git diff:\n$diff") \ | command codex --ask-for-approval never exec --sandbox workspace-write --ephemeral \ "Read the provided instructions and diff, then generate and execute the commit." ;; copilot) echo "🤖 Copilot is analyzing staged changes..." copilot -p "$prompt Here is the git diff: $diff" -s --no-ask-user --allow-tool='shell(git:*)' ;; opencode) echo "🤖 OpenCode is analyzing staged changes..." cat "$HOME/.config/ai-commit-prompt.txt" <(echo -e "\n\nHere is the git diff:\n$diff") | opencode run ;; *) printf "❌ Error: Invalid engine '%s'\n" "$engine" >&2 printf "Usage: aicommit [claude | codex | copilot | opencode]\n" >&2 return 1 ;; esac } # ----------------------------------------------------------------------------- # 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 ...]\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: kill_port (Kill or check the process using a port) # ----------------------------------------------------------------------------- kill_port() { 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: kill_port [--check|-c|--kill|-k|--force|-f] \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: kill_port [--check|-c|--kill|-k|--force|-f] \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 \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 }