#!/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 project’s .pnpm are hard links into the store." echo " Real disk use β‰ˆ store size + small symlink/metadata overhead." echo " Often ~30–50% 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"