407 lines
13 KiB
Bash
Executable file
407 lines
13 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
#
|
|
# find-consumers.sh - Find all consumers of a package across ~/Code
|
|
#
|
|
# Searches ~/Code/@applications, @packages, @services for projects that depend
|
|
# on a given package. Auto-detects Python (pyproject.toml) vs JavaScript
|
|
# (package.json) packages. Shows dependency source (registry/path/link/git)
|
|
# and optionally where imports occur in each consumer.
|
|
#
|
|
# Usage:
|
|
# find-consumers.sh [options] <package-path>
|
|
#
|
|
# Arguments:
|
|
# <package-path> Path to package, relative paths resolve from @packages
|
|
# Examples: @ts/queue, @py/model-boss, ~/Code/@packages/@ts/ui-react
|
|
#
|
|
# Options:
|
|
# -i, --imports Show import locations within each consumer (recommended)
|
|
# -p, --potential Include projects that could use the package but don't
|
|
# -h, --help Show this help
|
|
#
|
|
# Output:
|
|
# [REGISTRY] Installed from forge.black.lan (pip/npm registry)
|
|
# [PATH] Local file:// or -e editable install
|
|
# [LINK] npm link: dependency
|
|
# [GIT] git+https:// or git+ssh:// dependency
|
|
# [POTENTIAL] Project without the dependency (with -p flag)
|
|
#
|
|
# Examples:
|
|
# find-consumers.sh @ts/queue # basic consumer list
|
|
# find-consumers.sh -i @ts/service-addresses # with import locations
|
|
# find-consumers.sh -i -p @py/model-boss # imports + potential consumers
|
|
#
|
|
|
|
set -uo pipefail
|
|
|
|
# Build search dirs from existing directories
|
|
_build_search_dirs() {
|
|
local dirs=""
|
|
for d in "$HOME/Code/@applications" "$HOME/Code/@packages" "$HOME/Code/@services"; do
|
|
[[ -d "$d" ]] && dirs="$dirs $d"
|
|
done
|
|
echo "$dirs"
|
|
}
|
|
SEARCH_DIRS="${SEARCH_DIRS:-$(_build_search_dirs)}"
|
|
PACKAGES_ROOT="$HOME/Code/@packages"
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
MAGENTA='\033[0;35m'
|
|
DIM='\033[2m'
|
|
NC='\033[0m'
|
|
|
|
# Counters
|
|
REGISTRY_DEPS=0
|
|
PATH_DEPS=0
|
|
LINK_DEPS=0
|
|
GIT_DEPS=0
|
|
IMPORTS_FOUND=0
|
|
POTENTIAL_CONSUMERS=0
|
|
|
|
# State
|
|
declare -A CONSUMER_DIRS_MAP=()
|
|
SHOW_POTENTIAL=false
|
|
SHOW_IMPORTS=false
|
|
PKG_PATH=""
|
|
PKG_TYPE=""
|
|
PKG_NAME=""
|
|
PKG_VERSION=""
|
|
PKG_MODULE=""
|
|
|
|
# ============================================================================
|
|
# Utility Functions
|
|
# ============================================================================
|
|
|
|
die() {
|
|
echo -e "${RED}Error: $1${NC}" >&2
|
|
exit 1
|
|
}
|
|
|
|
resolve_path() {
|
|
local input="$1"
|
|
|
|
[[ "$input" == /* && -d "$input" ]] && { echo "$input"; return 0; }
|
|
[[ -d "$PACKAGES_ROOT/$input" ]] && { echo "$PACKAGES_ROOT/$input"; return 0; }
|
|
[[ -d "$PACKAGES_ROOT/@$input" ]] && { echo "$PACKAGES_ROOT/@$input"; return 0; }
|
|
return 1
|
|
}
|
|
|
|
detect_package_type() {
|
|
local path="$1"
|
|
[[ -f "$path/pyproject.toml" ]] && { echo "python"; return; }
|
|
[[ -f "$path/package.json" ]] && { echo "javascript"; return; }
|
|
echo "unknown"
|
|
}
|
|
|
|
get_project_root() {
|
|
local file="$1"
|
|
local dir=$(dirname "$file")
|
|
|
|
# Walk up until we find pyproject.toml, package.json, or hit ~/Code
|
|
while [[ "$dir" != "$HOME/Code" && "$dir" != "/" ]]; do
|
|
[[ -f "$dir/pyproject.toml" || -f "$dir/package.json" ]] && { echo "$dir"; return; }
|
|
dir=$(dirname "$dir")
|
|
done
|
|
echo "$(dirname "$file")"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Package Info Extraction
|
|
# ============================================================================
|
|
|
|
extract_python_info() {
|
|
local pyproject="$PKG_PATH/pyproject.toml"
|
|
|
|
PKG_NAME=$(grep -A20 '^\[project\]' "$pyproject" 2>/dev/null | grep '^name' | head -1 | sed 's/.*= *"\([^"]*\)".*/\1/' || echo "")
|
|
PKG_VERSION=$(grep -A20 '^\[project\]' "$pyproject" 2>/dev/null | grep '^version' | head -1 | sed 's/.*= *"\([^"]*\)".*/\1/' || echo "")
|
|
PKG_MODULE="${PKG_NAME//-/_}"
|
|
|
|
[[ -n "$PKG_NAME" ]] || die "Could not extract package name from pyproject.toml"
|
|
}
|
|
|
|
extract_js_info() {
|
|
local package_json="$PKG_PATH/package.json"
|
|
|
|
PKG_NAME=$(grep '"name"' "$package_json" 2>/dev/null | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/' || echo "")
|
|
PKG_VERSION=$(grep '"version"' "$package_json" 2>/dev/null | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/' || echo "")
|
|
|
|
[[ -n "$PKG_NAME" ]] || die "Could not extract package name from package.json"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Import Search per Consumer
|
|
# ============================================================================
|
|
|
|
show_python_imports_for_consumer() {
|
|
local consumer_dir="$1"
|
|
|
|
[[ "$SHOW_IMPORTS" == false ]] && return
|
|
|
|
local imports
|
|
imports=$(find "$consumer_dir" \( -name ".venv" -o -name "venv" -o -name "__pycache__" -o -name "dist" \) -prune -o -name "*.py" -type f -print 2>/dev/null | \
|
|
xargs grep -HnE "^\s*import ${PKG_MODULE}|^\s*from ${PKG_MODULE}" 2>/dev/null || true)
|
|
|
|
if [[ -n "$imports" ]]; then
|
|
echo -e " ${CYAN}Imports:${NC}"
|
|
while IFS= read -r line; do
|
|
[[ -z "$line" ]] && continue
|
|
local relpath="${line#$consumer_dir/}"
|
|
echo -e " ${DIM}$relpath${NC}"
|
|
((IMPORTS_FOUND++))
|
|
done <<< "$imports"
|
|
fi
|
|
}
|
|
|
|
show_js_imports_for_consumer() {
|
|
local consumer_dir="$1"
|
|
|
|
[[ "$SHOW_IMPORTS" == false ]] && return
|
|
|
|
local imports
|
|
imports=$(find "$consumer_dir" \( -name "node_modules" -o -name "dist" \) -prune -o \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.mjs" \) -type f -print 2>/dev/null | \
|
|
xargs grep -HnE "from ['\"]${PKG_NAME}[/'\":]|require\(['\"]${PKG_NAME}[/'\":]" 2>/dev/null || true)
|
|
|
|
if [[ -n "$imports" ]]; then
|
|
echo -e " ${CYAN}Imports:${NC}"
|
|
while IFS= read -r line; do
|
|
[[ -z "$line" ]] && continue
|
|
local relpath="${line#$consumer_dir/}"
|
|
echo -e " ${DIM}$relpath${NC}"
|
|
((IMPORTS_FOUND++))
|
|
done <<< "$imports"
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# Consumer Search Functions
|
|
# ============================================================================
|
|
|
|
classify_and_print_dep() {
|
|
local file="$1"
|
|
local dep_line="$2"
|
|
local lang="$3"
|
|
|
|
local consumer_dir
|
|
consumer_dir=$(get_project_root "$file")
|
|
CONSUMER_DIRS_MAP["$consumer_dir"]=1
|
|
|
|
local source_type=""
|
|
local source_color=""
|
|
|
|
if echo "$dep_line" | grep -qE '@ *file://|^-e|file:'; then
|
|
source_type="PATH"
|
|
source_color="$YELLOW"
|
|
((PATH_DEPS++))
|
|
elif echo "$dep_line" | grep -q 'link:'; then
|
|
source_type="LINK"
|
|
source_color="$YELLOW"
|
|
((LINK_DEPS++))
|
|
elif echo "$dep_line" | grep -qE 'git\+https://|git\+ssh://|github:'; then
|
|
source_type="GIT"
|
|
source_color="$MAGENTA"
|
|
((GIT_DEPS++))
|
|
else
|
|
source_type="REGISTRY"
|
|
source_color="$GREEN"
|
|
((REGISTRY_DEPS++))
|
|
fi
|
|
|
|
echo ""
|
|
echo -e "${source_color}[$source_type]${NC} $consumer_dir"
|
|
echo -e " ${DIM}$dep_line${NC}"
|
|
|
|
if [[ "$lang" == "python" ]]; then
|
|
show_python_imports_for_consumer "$consumer_dir"
|
|
else
|
|
show_js_imports_for_consumer "$consumer_dir"
|
|
fi
|
|
}
|
|
|
|
search_python_deps() {
|
|
echo -e "${BLUE}--- Python Consumers ---${NC}"
|
|
|
|
local found=0
|
|
|
|
# Search pyproject.toml
|
|
while IFS= read -r file; do
|
|
[[ -z "$file" ]] && continue
|
|
[[ "$file" == "$PKG_PATH/pyproject.toml" ]] && continue
|
|
|
|
local dep_line
|
|
dep_line=$(grep -E "\"${PKG_NAME}(>=|<=|==|~=|!=|>|<|@|\[)[^\"]*\"" "$file" 2>/dev/null | head -1 || true)
|
|
|
|
if [[ -n "$dep_line" ]]; then
|
|
classify_and_print_dep "$file" "$dep_line" "python"
|
|
found=1
|
|
fi
|
|
done < <(find $SEARCH_DIRS \( -name "node_modules" -o -name ".venv" -o -name "venv" -o -name "dist" -o -name "__pycache__" \) -prune -o -name "pyproject.toml" -type f -print 2>/dev/null | xargs grep -l "$PKG_NAME" 2>/dev/null || true)
|
|
|
|
# Search requirements.txt
|
|
while IFS= read -r file; do
|
|
[[ -z "$file" ]] && continue
|
|
|
|
local dep_line
|
|
dep_line=$(grep -E "^${PKG_NAME}|^-e.*${PKG_NAME}" "$file" 2>/dev/null | head -1 || true)
|
|
|
|
if [[ -n "$dep_line" ]]; then
|
|
classify_and_print_dep "$file" "$dep_line" "python"
|
|
found=1
|
|
fi
|
|
done < <(find $SEARCH_DIRS \( -name "node_modules" -o -name ".venv" -o -name "venv" -o -name "dist" -o -name "__pycache__" \) -prune -o -name "requirements.txt" -type f -print 2>/dev/null | xargs grep -l "$PKG_NAME" 2>/dev/null || true)
|
|
|
|
(( found == 0 )) && echo -e "\n${DIM}(none)${NC}" || true
|
|
}
|
|
|
|
search_js_deps() {
|
|
echo -e "${BLUE}--- JavaScript Consumers ---${NC}"
|
|
|
|
local found=0
|
|
|
|
while IFS= read -r file; do
|
|
[[ -z "$file" ]] && continue
|
|
[[ "$file" == "$PKG_PATH/package.json" ]] && continue
|
|
|
|
local dep_line
|
|
dep_line=$(grep -E "\"${PKG_NAME}\":" "$file" 2>/dev/null | head -1 || true)
|
|
|
|
if [[ -n "$dep_line" ]]; then
|
|
classify_and_print_dep "$file" "$dep_line" "javascript"
|
|
found=1
|
|
fi
|
|
done < <(find $SEARCH_DIRS \( -name "node_modules" -o -name "dist" \) -prune -o -name "package.json" -type f -print 2>/dev/null | xargs grep -l "\"${PKG_NAME}\"" 2>/dev/null || true)
|
|
|
|
(( found == 0 )) && echo -e "\n${DIM}(none)${NC}" || true
|
|
}
|
|
|
|
# ============================================================================
|
|
# Potential Consumers
|
|
# ============================================================================
|
|
|
|
search_potential() {
|
|
[[ "$SHOW_POTENTIAL" == false ]] && return
|
|
|
|
local file_pattern="$1"
|
|
|
|
echo ""
|
|
echo -e "${BLUE}--- Potential Consumers ---${NC}"
|
|
echo ""
|
|
|
|
local found=0
|
|
while IFS= read -r file; do
|
|
[[ -z "$file" ]] && continue
|
|
[[ "$file" == "$PKG_PATH"* ]] && continue
|
|
|
|
local consumer_dir
|
|
consumer_dir=$(get_project_root "$file")
|
|
[[ -n "${CONSUMER_DIRS_MAP[$consumer_dir]:-}" ]] && continue
|
|
|
|
echo -e "${CYAN}[POTENTIAL]${NC} $consumer_dir"
|
|
((POTENTIAL_CONSUMERS++))
|
|
found=1
|
|
CONSUMER_DIRS_MAP["$consumer_dir"]=1 # Don't show duplicates
|
|
done < <(find $SEARCH_DIRS \( -name "node_modules" -o -name ".venv" -o -name "venv" -o -name "dist" -o -name "__pycache__" \) -prune -o \( $file_pattern \) -type f -print 2>/dev/null || true)
|
|
|
|
(( found == 0 )) && echo -e "${DIM}(none)${NC}" || true
|
|
}
|
|
|
|
# ============================================================================
|
|
# Summary
|
|
# ============================================================================
|
|
|
|
print_summary() {
|
|
echo ""
|
|
echo -e "${BLUE}=== Summary ===${NC}"
|
|
|
|
local total_deps=$((REGISTRY_DEPS + PATH_DEPS + LINK_DEPS + GIT_DEPS))
|
|
|
|
[[ $REGISTRY_DEPS -gt 0 ]] && echo -e "Registry (forge): ${GREEN}${REGISTRY_DEPS}${NC}"
|
|
[[ $PATH_DEPS -gt 0 ]] && echo -e "Path (file://): ${YELLOW}${PATH_DEPS}${NC}"
|
|
[[ $LINK_DEPS -gt 0 ]] && echo -e "Link (link:): ${YELLOW}${LINK_DEPS}${NC}"
|
|
[[ $GIT_DEPS -gt 0 ]] && echo -e "Git (git+): ${MAGENTA}${GIT_DEPS}${NC}"
|
|
[[ $IMPORTS_FOUND -gt 0 ]] && echo -e "Import locations: ${CYAN}${IMPORTS_FOUND}${NC}"
|
|
[[ $POTENTIAL_CONSUMERS -gt 0 ]] && echo -e "Potential: ${DIM}${POTENTIAL_CONSUMERS}${NC}"
|
|
|
|
(( total_deps == 0 )) && echo -e "${DIM}No dependencies found${NC}" || true
|
|
|
|
# Warnings
|
|
local non_registry=$((PATH_DEPS + LINK_DEPS))
|
|
if [[ $non_registry -gt 0 ]]; then
|
|
echo ""
|
|
echo -e "${YELLOW}Warning: $non_registry path/link dep(s) should migrate to registry${NC}"
|
|
exit 1
|
|
fi
|
|
|
|
echo ""
|
|
}
|
|
|
|
# ============================================================================
|
|
# Main
|
|
# ============================================================================
|
|
|
|
print_usage() {
|
|
echo "Usage: $0 [options] <package-path>"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " -p, --potential Include potential consumers"
|
|
echo " -i, --imports Show import locations per consumer"
|
|
echo " -h, --help Show this help"
|
|
echo ""
|
|
echo "Examples:"
|
|
echo " $0 @ts/queue"
|
|
echo " $0 -i @ts/service-addresses # detailed with imports"
|
|
echo " $0 -p @py/model-boss # with potential consumers"
|
|
}
|
|
|
|
main() {
|
|
local positional_args=()
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-p|--potential) SHOW_POTENTIAL=true; shift ;;
|
|
-i|--imports) SHOW_IMPORTS=true; shift ;;
|
|
-h|--help) print_usage; exit 0 ;;
|
|
-*) die "Unknown option: $1" ;;
|
|
*) positional_args+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
|
|
[[ ${#positional_args[@]} -lt 1 ]] && { print_usage; exit 1; }
|
|
|
|
PKG_PATH=$(resolve_path "${positional_args[0]}") || die "Package not found: ${positional_args[0]}"
|
|
PKG_TYPE=$(detect_package_type "$PKG_PATH")
|
|
|
|
[[ "$PKG_TYPE" == "unknown" ]] && die "Cannot detect package type"
|
|
|
|
echo -e "${BLUE}=== Package Consumer Search ===${NC}"
|
|
|
|
case "$PKG_TYPE" in
|
|
python)
|
|
extract_python_info
|
|
echo -e "Package: ${YELLOW}${PKG_NAME}${NC} ${DIM}(Python)${NC}"
|
|
echo -e "Version: ${PKG_VERSION}"
|
|
echo -e "Module: ${PKG_MODULE}"
|
|
echo ""
|
|
|
|
search_python_deps
|
|
search_potential "-name pyproject.toml -o -name requirements.txt"
|
|
;;
|
|
javascript)
|
|
extract_js_info
|
|
echo -e "Package: ${YELLOW}${PKG_NAME}${NC} ${DIM}(JavaScript)${NC}"
|
|
echo -e "Version: ${PKG_VERSION}"
|
|
echo ""
|
|
|
|
search_js_deps
|
|
search_potential "-name package.json"
|
|
;;
|
|
esac
|
|
|
|
print_summary
|
|
}
|
|
|
|
main "$@"
|