Files
qiming/qiming-file-server/scripts/pnpm-check.sh

424 lines
16 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# pnpm disk usage inspection script
echo "======================================"
echo "pnpm disk usage analyzer"
echo "======================================"
echo ""
# Ensure pnpm is installed
if ! command -v pnpm &> /dev/null; then
echo "❌ pnpm not found; install it first"
echo " npm install -g pnpm"
exit 1
fi
echo "✅ pnpm version: $(pnpm --version)"
echo ""
# Read PROJECT_SOURCE_DIR from env file
get_project_dir_from_env() {
local env_name=${1:-"development"}
local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
local project_root="$(dirname "$script_dir")"
local env_file="$project_root/env.$env_name"
if [ -f "$env_file" ]; then
local project_dir=$(grep "^PROJECT_SOURCE_DIR=" "$env_file" | cut -d'=' -f2)
if [ -n "$project_dir" ]; then
echo "$project_dir"
return 0
fi
fi
return 1
}
# Store path
echo "📁 pnpm store path:"
STORE_PATH=$(pnpm store path 2>/dev/null)
STORE_FILESYSTEM=""
if [ -n "$STORE_PATH" ]; then
echo " $STORE_PATH"
if [ -d "$STORE_PATH" ]; then
STORE_SIZE=$(du -sh "$STORE_PATH" 2>/dev/null | awk '{print $1}')
echo " Store size: $STORE_SIZE"
STORE_FILESYSTEM=$(df "$STORE_PATH" 2>/dev/null | tail -n 1 | awk '{print $1}')
if [ -n "$STORE_FILESYSTEM" ]; then
echo " Filesystem: $STORE_FILESYSTEM"
fi
fi
else
echo " ⚠️ Could not resolve store path"
fi
echo ""
# Store status
echo "📊 pnpm store status:"
STORE_STATUS_OUTPUT=$(pnpm store status 2>&1)
STORE_STATUS_EXIT_CODE=$?
if [ $STORE_STATUS_EXIT_CODE -eq 0 ]; then
echo "$STORE_STATUS_OUTPUT"
else
if echo "$STORE_STATUS_OUTPUT" | grep -q "ENOENT"; then
echo " ⚠️ Store index missing or corrupted"
echo " 💡 Try:"
echo " pnpm store prune"
else
echo " ⚠️ Could not read store status"
echo " Error: $(echo "$STORE_STATUS_OUTPUT" | head -n 1)"
fi
fi
echo ""
# Project directory resolution
# Priority: 1) CLI arg 2) PROJECT_SOURCE_DIR 3) env file 4) prompt
PROJECT_DIR=""
AUTO_DETECTED_DIR=""
SOURCE_INFO=""
if [ -n "$1" ]; then
AUTO_DETECTED_DIR="$1"
SOURCE_INFO="command-line argument"
elif [ -n "$PROJECT_SOURCE_DIR" ]; then
AUTO_DETECTED_DIR="$PROJECT_SOURCE_DIR"
SOURCE_INFO="env PROJECT_SOURCE_DIR"
else
ENV_NAME="${NODE_ENV:-development}"
AUTO_DETECTED_DIR=$(get_project_dir_from_env "$ENV_NAME")
if [ -z "$AUTO_DETECTED_DIR" ]; then
for env in development production test; do
AUTO_DETECTED_DIR=$(get_project_dir_from_env "$env")
if [ -n "$AUTO_DETECTED_DIR" ]; then
ENV_NAME="$env"
break
fi
done
fi
if [ -n "$AUTO_DETECTED_DIR" ]; then
SOURCE_INFO="config file env.$ENV_NAME"
fi
fi
if [ -n "$AUTO_DETECTED_DIR" ]; then
echo ""
echo "📂 Detected project directory (source: $SOURCE_INFO)"
echo " Path: $AUTO_DETECTED_DIR"
echo ""
read -p "👉 Press Enter to use this path, or type a different path: " USER_INPUT
if [ -n "$USER_INPUT" ]; then
PROJECT_DIR="$USER_INPUT"
echo "📝 Using entered project directory: $PROJECT_DIR"
else
PROJECT_DIR="$AUTO_DETECTED_DIR"
echo "✅ Using detected project directory"
fi
else
echo ""
echo "⚠️ Could not auto-detect project directory"
echo ""
echo "💡 You can:"
echo " - Pass a path: $0 /path/to/projects"
echo " - Set env: PROJECT_SOURCE_DIR=/path/to/projects $0"
echo " - Set NODE_ENV: NODE_ENV=development $0"
echo ""
read -p "📝 Project directory path (Enter to skip project scan): " USER_INPUT
if [ -n "$USER_INPUT" ]; then
PROJECT_DIR="$USER_INPUT"
echo "📂 Using entered project directory"
else
echo ""
echo "Skipping project scan..."
fi
fi
if [ -n "$PROJECT_DIR" ] && [ ! -d "$PROJECT_DIR" ]; then
echo ""
echo "⚠️ Project directory does not exist: $PROJECT_DIR"
echo ""
echo "Skipping project scan..."
PROJECT_DIR=""
fi
if [ -n "$PROJECT_DIR" ]; then
echo ""
echo "🔍 Scanning project directory: $PROJECT_DIR"
echo ""
if [ -d "$PROJECT_DIR" ]; then
PROJECT_FILESYSTEM=$(df "$PROJECT_DIR" 2>/dev/null | tail -n 1 | awk '{print $1}')
if [ -n "$PROJECT_FILESYSTEM" ]; then
echo "💾 Filesystem check:"
echo " Project filesystem: $PROJECT_FILESYSTEM"
if [ -n "$STORE_FILESYSTEM" ]; then
echo " Store filesystem: $STORE_FILESYSTEM"
echo ""
if [ "$PROJECT_FILESYSTEM" = "$STORE_FILESYSTEM" ]; then
echo " ✅ Project and store are on the same filesystem"
echo " 💡 Hard links work; disk savings apply"
else
echo " ⚠️ Project and store are on different filesystems"
echo " ❌ Hard links cannot cross filesystems; pnpm will copy files"
fi
else
echo " ⚠️ Could not determine store filesystem"
fi
echo ""
fi
fi
echo "📦 Per-project node_modules (apparent size):"
echo " ⚠️ Note: du double-counts hard links; real usage is lower"
TOTAL_SIZE_KB=0
COUNT=0
MAX_DISPLAY=5
while IFS= read -r dir; do
SIZE_HUMAN=$(du -sh "$dir" 2>/dev/null | awk '{print $1}')
SIZE_KB=$(du -sk "$dir" 2>/dev/null | awk '{print $1}')
PROJECT_NAME=$(echo "$dir" | sed "s|$PROJECT_DIR/||" | sed 's|/node_modules||')
if [ $COUNT -lt $MAX_DISPLAY ]; then
echo " [$PROJECT_NAME] $SIZE_HUMAN (includes hard-link double counting)"
fi
TOTAL_SIZE_KB=$((TOTAL_SIZE_KB + SIZE_KB))
((COUNT++))
done < <(find "$PROJECT_DIR" -name "node_modules" -type d -maxdepth 3 2>/dev/null)
if [ $COUNT -gt $MAX_DISPLAY ]; then
echo " ... $((COUNT - MAX_DISPLAY)) more not shown"
fi
if [ $TOTAL_SIZE_KB -gt 0 ]; then
if [ $TOTAL_SIZE_KB -gt 1048576 ]; then
TOTAL_SIZE_HUMAN=$(awk "BEGIN {printf \"%.1fG\", $TOTAL_SIZE_KB/1048576}")
elif [ $TOTAL_SIZE_KB -gt 1024 ]; then
TOTAL_SIZE_HUMAN=$(awk "BEGIN {printf \"%.1fM\", $TOTAL_SIZE_KB/1024}")
else
TOTAL_SIZE_HUMAN="${TOTAL_SIZE_KB}K"
fi
echo " Total apparent size: $TOTAL_SIZE_HUMAN (real usage is lower)"
fi
echo " Found $COUNT node_modules directories"
echo ""
echo "🗂️ Per-project .pnpm folders (apparent size):"
echo " ⚠️ Note: entries under .pnpm are hard links; little extra space per copy"
PNPM_COUNT=0
PNPM_TOTAL_SIZE_KB=0
while IFS= read -r dir; do
SIZE_HUMAN=$(du -sh "$dir" 2>/dev/null | awk '{print $1}')
SIZE_KB=$(du -sk "$dir" 2>/dev/null | awk '{print $1}')
PROJECT_NAME=$(echo "$dir" | sed "s|$PROJECT_DIR/||" | sed 's|/node_modules/.pnpm||')
if [ $PNPM_COUNT -lt $MAX_DISPLAY ]; then
echo " [$PROJECT_NAME] $SIZE_HUMAN (hard links, shared in store)"
fi
PNPM_TOTAL_SIZE_KB=$((PNPM_TOTAL_SIZE_KB + SIZE_KB))
((PNPM_COUNT++))
done < <(find "$PROJECT_DIR" -type d -path "*/node_modules/.pnpm" -maxdepth 4 2>/dev/null)
if [ $PNPM_COUNT -gt $MAX_DISPLAY ]; then
echo " ... $((PNPM_COUNT - MAX_DISPLAY)) more not shown"
fi
if [ $PNPM_TOTAL_SIZE_KB -gt 0 ]; then
if [ $PNPM_TOTAL_SIZE_KB -gt 1048576 ]; then
PNPM_TOTAL_SIZE_HUMAN=$(awk "BEGIN {printf \"%.1fG\", $PNPM_TOTAL_SIZE_KB/1048576}")
elif [ $PNPM_TOTAL_SIZE_KB -gt 1024 ]; then
PNPM_TOTAL_SIZE_HUMAN=$(awk "BEGIN {printf \"%.1fM\", $PNPM_TOTAL_SIZE_KB/1024}")
else
PNPM_TOTAL_SIZE_HUMAN="${PNPM_TOTAL_SIZE_KB}K"
fi
echo " Total apparent size: $PNPM_TOTAL_SIZE_HUMAN (all hard-linked to store)"
fi
echo " Found $PNPM_COUNT .pnpm directories"
echo ""
echo "💾 Filesystem usage (df):"
echo " Whole filesystem is more accurate than summing du:"
df -h "$PROJECT_DIR" | tail -n 1 | awk '{print " Filesystem: "$1, "| Used: "$3, "| Avail: "$4, "| Use: "$5}'
echo ""
fi
echo "======================================"
echo "💡 Important:"
echo "======================================"
echo ""
echo "⚠️ du counts hard-linked files multiple times!"
echo " Files under each projects .pnpm are hard links into the store."
echo " Real disk use ≈ store size + small symlink/metadata overhead."
echo " Often ~3050% of what naive du sums suggest."
echo ""
echo "✅ Ways to see real usage:"
echo " 1. pnpm store size (shown above)"
echo " 2. df -h for the whole filesystem"
echo " 3. Compare df before and after installs"
echo ""
echo "======================================"
echo "💡 Recommendations:"
echo "======================================"
echo ""
echo "1. Prune unused packages from the store:"
echo " pnpm store prune"
echo ""
echo "2. Prefer one store and projects on the same filesystem:"
echo " Keep projects and the store on the same volume when possible"
echo ""
echo "3. Tune per-project .npmrc for your registry and link strategy"
echo ""
if [ -n "$PROJECT_DIR" ] && [ -d "$PROJECT_DIR" ]; then
echo ""
read -p "🔍 Verify hard links between two projects? (y to start, Enter to skip): " VERIFY_HARDLINK
if [[ "$VERIFY_HARDLINK" =~ ^[Yy]$ ]]; then
echo ""
echo "======================================"
echo "🔗 Hard link verification"
echo "======================================"
echo ""
echo "💡 Project layout: $PROJECT_DIR/{projectId}"
echo ""
echo "📋 Project IDs under this directory (up to 5):"
PROJECT_IDS=$(find "$PROJECT_DIR" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; 2>/dev/null | head -5)
if [ -n "$PROJECT_IDS" ]; then
echo "$PROJECT_IDS" | while read -r pid; do
echo " - $pid"
done
else
echo " (none)"
fi
echo ""
read -p "👉 First project ID: " PROJECT_1_ID
if [ -z "$PROJECT_1_ID" ]; then
echo "❌ Project ID cannot be empty"
else
read -p "👉 Second project ID: " PROJECT_2_ID
if [ -z "$PROJECT_2_ID" ]; then
echo "❌ Project ID cannot be empty"
elif [ "$PROJECT_1_ID" = "$PROJECT_2_ID" ]; then
echo "❌ Enter two different project IDs"
else
echo ""
PROJECT_1="$PROJECT_DIR/$PROJECT_1_ID"
PROJECT_2="$PROJECT_DIR/$PROJECT_2_ID"
PROJECT_1_NAME="$PROJECT_1_ID"
PROJECT_2_NAME="$PROJECT_2_ID"
if [ ! -d "$PROJECT_1" ]; then
echo "❌ Project directory not found: $PROJECT_1"
elif [ ! -d "$PROJECT_2" ]; then
echo "❌ Project directory not found: $PROJECT_2"
else
echo "🔍 Comparing projects:"
echo "$PROJECT_1_NAME"
echo "$PROJECT_2_NAME"
echo ""
PNPM_DIR_1="$PROJECT_1/node_modules/.pnpm"
PNPM_DIR_2="$PROJECT_2/node_modules/.pnpm"
if [ ! -d "$PNPM_DIR_1" ]; then
echo "⚠️ $PROJECT_1_NAME: no node_modules/.pnpm"
echo " Path: $PNPM_DIR_1"
elif [ ! -d "$PNPM_DIR_2" ]; then
echo "⚠️ $PROJECT_2_NAME: no node_modules/.pnpm"
echo " Path: $PNPM_DIR_2"
else
echo "🔎 Looking for overlapping dependencies..."
PACKAGES_1=($(find "$PNPM_DIR_1" -maxdepth 1 -type d -name "*@*" 2>/dev/null | xargs -I {} basename {}))
PACKAGES_2=($(find "$PNPM_DIR_2" -maxdepth 1 -type d -name "*@*" 2>/dev/null | xargs -I {} basename {}))
COMMON_PACKAGES=()
for pkg1 in "${PACKAGES_1[@]}"; do
for pkg2 in "${PACKAGES_2[@]}"; do
if [ "$pkg1" = "$pkg2" ]; then
COMMON_PACKAGES+=("$pkg1")
break
fi
done
done
if [ ${#COMMON_PACKAGES[@]} -eq 0 ]; then
echo "⚠️ No shared packages between the two projects"
else
echo "📦 Found ${#COMMON_PACKAGES[@]} shared package(s); verifying..."
echo ""
VERIFIED_COUNT=0
SAME_INODE_COUNT=0
for pkg in "${COMMON_PACKAGES[@]:0:5}"; do
PKG_FILE_1=$(find "$PNPM_DIR_1/$pkg" -name "package.json" -path "*/node_modules/*/package.json" 2>/dev/null | head -n 1)
PKG_FILE_2=$(find "$PNPM_DIR_2/$pkg" -name "package.json" -path "*/node_modules/*/package.json" 2>/dev/null | head -n 1)
if [ -n "$PKG_FILE_1" ] && [ -f "$PKG_FILE_1" ] && [ -n "$PKG_FILE_2" ] && [ -f "$PKG_FILE_2" ]; then
INODE_1=$(ls -i "$PKG_FILE_1" | awk '{print $1}')
INODE_2=$(ls -i "$PKG_FILE_2" | awk '{print $1}')
PKG_DISPLAY=$(echo "$pkg" | sed 's/@[^@]*$//')
if [ "$INODE_1" = "$INODE_2" ]; then
echo "$PKG_DISPLAY: inode=$INODE_1 (same)"
((SAME_INODE_COUNT++))
else
echo "$PKG_DISPLAY: $INODE_1 vs $INODE_2 (different)"
fi
((VERIFIED_COUNT++))
fi
done
echo ""
echo "======================================"
if [ $VERIFIED_COUNT -eq 0 ]; then
echo "⚠️ Could not verify package files"
elif [ $SAME_INODE_COUNT -eq $VERIFIED_COUNT ]; then
echo "✅ Hard links look good"
echo " Checked $VERIFIED_COUNT package(s); all inodes match"
echo " Same physical files → disk savings from deduplication"
elif [ $SAME_INODE_COUNT -eq 0 ]; then
echo "❌ Hard links not shared"
echo " Checked $VERIFIED_COUNT package(s); inodes all differ"
echo ""
echo " Possible causes:"
echo " - Projects on different filesystems"
echo " - Filesystem without working hard links (e.g. some network mounts)"
echo " - pnpm / store configuration"
else
echo "⚠️ Mixed results"
echo " Checked $VERIFIED_COUNT package(s); $SAME_INODE_COUNT inode match(es)"
echo " May indicate inconsistent config or partial reinstalls"
fi
fi
fi
fi
fi
fi
echo ""
fi
fi
echo "✅ Done"