A suggestion for checking plugins in Node.js

This is only a suggestion! It applies to TiddlyWiki Node.js edition in which I tested it successfully, running in TUXEDO OS.

Purpose:

:dart: Core Objectives

  • Audit TiddlyWiki plugins across multiple wikis It scans each wiki under a given root directory and compares the plugins declared in tiddlywiki.info with what’s actually present on disk.

  • Classify plugin sources For every declared plugin, it determines whether it comes from:

    • Local (inside that wiki’s plugins/ folder)
    • Shared (from directories in TIDDLYWIKI_PLUGIN_PATH or your configured shared paths)
    • Core (bundled with the TiddlyWiki installation)
    • Missing (declared but not found anywhere)
  • Detect anomalies It flags:

    • Missing plugins (:x:)
    • Local‑only plugins (:warning: declared nowhere else)
    • Duplicates (:warning: both local and declared/shared)
  • Provide flexible reporting modes Users can choose:

    • --full β†’ detailed table of all plugins with :white_check_mark:/:x:/:warning:
    • --anomalies β†’ only show problems, plus a summary
    • --summary-only β†’ one line per wiki with counts
    • --csv β†’ export per‑wiki counts as CSV (plus totals)
    • --markdown β†’ export per‑wiki counts as Markdown table (plus totals)
    • --plain β†’ disable ANSI colors for copy/paste or Markdown‑friendly output
  • Aggregate results At the end, it prints grand totals across all wikis, so you can see the overall health of your plugin ecosystem.

:jigsaw: Why it’s useful

  • Gives you a dashboard‑style overview of plugin usage.
  • Makes anomalies immediately visible (colorized in terminal, or :warning:/:x: markers in plain/Markdown).
  • Produces export‑ready outputs (CSV/Markdown) for documentation, reports, or embedding in TiddlyWiki itself.
  • Keeps your environment consistent and maintainable by spotting missing or duplicate plugins before they cause runtime issues.

The script will prompt you for three parameters which it will store in a plugins_audit.cfg directory inside a configs sub-directory. This is done only on first run if no such parameters exist in the first place. For example in my case I have my wikis in ~/wikis and my plugins in ~/Twplugins:

# ~/scripts/configs/plugins_auditor.cfg
WIKI_ROOT="$HOME/wikis"
CORE_PATH="$(npm root -g)/tiddlywiki"
SHARED_PATHS="$HOME/TWplugins:/opt/tw-plugins"

The script:

#!/bin/bash

CONFIG_FILE="$HOME/scripts/configs/plugins_auditor.cfg"

# ─────────────────────────────────────────────
# Load configuration or ask interactively
# ─────────────────────────────────────────────
if [[ -f "$CONFIG_FILE" ]]; then
  # shellcheck source=/dev/null
  source "$CONFIG_FILE"
else
  echo "No config file found at $CONFIG_FILE"
  read -rp "Enter wiki root directory: " WIKI_ROOT
  read -rp "Enter core path (tiddlywiki install): " CORE_PATH
  read -rp "Enter shared plugin paths (colon-separated): " SHARED_PATHS

  # Optionally save for next time
  mkdir -p "$(dirname "$CONFIG_FILE")"
  {
    echo "WIKI_ROOT=\"$WIKI_ROOT\""
    echo "CORE_PATH=\"$CORE_PATH\""
    echo "SHARED_PATHS=\"$SHARED_PATHS\""
  } > "$CONFIG_FILE"
  echo "Saved configuration to $CONFIG_FILE"
fi

# Split SHARED_PATHS into array for later use
IFS=':' read -ra SHARED_PATHS_ARR <<< "$SHARED_PATHS"

# ANSI colors
RED=$'\e[31m'; YELLOW=$'\e[33m'; GREEN=$'\e[32m'; RESET=$'\e[0m'

# ─────────────────────────────────────────────
# Usage/help
# ─────────────────────────────────────────────
show_help() {
  cat <<EOF
Usage: $(basename "$0") [MODE]

Modes:
  --full          Show all plugins with βœ…/❌/⚠️
  --anomalies     Show only anomalies (❌ + ⚠️) plus summary
  --summary-only  Show one summary line per wiki
  --csv           Export per-wiki counts as CSV (plus totals row)
  --markdown      Export per-wiki counts as Markdown table (plus totals row)
  --plain         Force plain/Markdown-friendly output (no ANSI colors)
  -h, --help      Show this help message

If no mode is given, you’ll be prompted interactively.
EOF
}

# ─────────────────────────────────────────────
# Mode detection
# ─────────────────────────────────────────────
MODE=""
PLAIN=0
case "$1" in
  --full) MODE="full" ;;
  --anomalies) MODE="anomalies" ;;
  --summary-only) MODE="summary" ;;
  --csv) MODE="csv" ;;
  --markdown) MODE="markdown" ;;
  --plain) PLAIN=1 ;;
  -h|--help) show_help; exit 0 ;;
  "") ;; # no arg, fall back to interactive
  *) echo "Unknown option: $1"; show_help; exit 1 ;;
esac

[[ -n "$NO_COLOR" ]] && PLAIN=1
[[ ! -t 1 ]] && PLAIN=1   # auto-plain if not a terminal

if [[ -z "$MODE" ]]; then
  echo "Select audit mode:"
  echo "  1) Full (all plugins with βœ…/❌/⚠️)"
  echo "  2) Anomalies only (❌ + ⚠️, plus summary)"
  echo "  3) Summary only (one line per wiki)"
  echo "  4) CSV export"
  echo "  5) Markdown export"
  read -rp "Enter choice [1-5]: " choice
  case "$choice" in
    1) MODE="full" ;;
    2) MODE="anomalies" ;;
    3) MODE="summary" ;;
    4) MODE="csv" ;;
    5) MODE="markdown" ;;
    *) MODE="full" ;;
  esac
fi

# ─────────────────────────────────────────────
# Helpers
# ─────────────────────────────────────────────
print_dashboard_header() {
  if [[ $MODE == "markdown" ]]; then
    printf '\n| %-15s | %6s | %5s | %6s | %4s | %7s | %5s | %10s |\n' \
      "Wiki" "Listed" "Local" "Shared" "Core" "Missing" "Dupes" "Local-only"
    printf '|%s|%s|%s|%s|%s|%s|%s|%s|\n' \
      '-----------------' '--------' '-------' '--------' '------' '--------' '-------' '------------'
  elif [[ $MODE != "csv" ]]; then
    echo -e "\nπŸ“Š Audit Dashboard"
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  fi
}

print_dashboard_row() {
  local wiki="$1" total="$2" localc="$3" shared="$4" core="$5" missing="$6" dupes="$7" localonly="$8"
  case "$MODE" in
    csv)
      printf '%s,%d,%d,%d,%d,%d,%d,%d\n' \
        "$wiki" "$total" "$localc" "$shared" "$core" "$missing" "$dupes" "$localonly"
      ;;
    markdown)
      printf '| %-15s | %6s | %5s | %6s | %4s | %7s | %5s | %10s |\n' \
        "$wiki" "$total" "$localc" "$shared" "$core" "$missing" "$dupes" "$localonly"
      ;;
    *)
      printf '%-15s: %3s listed β€” %2s local, %2s shared, %2s core, %2s missing, %2s dupes, %2s local-only\n' \
        "$wiki" "$total" "$localc" "$shared" "$core" "$missing" "$dupes" "$localonly"
      ;;
  esac
}

print_dashboard_footer() {
  case "$MODE" in
    csv) : ;; # nothing
    markdown) echo "" ;;
    *) echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" ;;
  esac
}

print_table_header() {
  if [[ $PLAIN -eq 1 ]]; then
    printf '| %-30s | %-6s | %-20s |\n' "Plugin" "Status" "Location"
    printf '|%s|%s|%s|\n' \
      '--------------------------------' '--------' '----------------------'
  else
    printf 'β”‚ %-30s β”‚ %-6s β”‚ %-20s β”‚\n' "Plugin" "Status" "Location"
    printf 'β”œ%sβ”Ό%sβ”Ό%s─\n' \
      '────────────────────────────────' '───────' '──────────────────────'
  fi
}

print_table_row() {
  local plugin="$1" status="$2" location="$3" color="$4"
  if [[ $PLAIN -eq 1 ]]; then
    printf '| %-30s | %-6s | %-20s |\n' "$plugin" "$status" "$location"
  else
    printf 'β”‚ %-30s β”‚ %s%-6s%s β”‚ %s%-20s%s β”‚\n' "$plugin" "$color" "$status" "$RESET" "$color" "$location" "$RESET"
  fi
}

# ─────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────
echo "πŸ“¦ TiddlyWiki Super Plugin Audit ($MODE mode)"
echo "Wiki root: $WIKI_ROOT"
echo "Core path: $CORE_PATH"
echo "Shared plugin paths: ${SHARED_PATHS[*]:-(none)}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

print_dashboard_header

# Grand totals
grand_listed=0; grand_local=0; grand_shared=0; grand_core=0
grand_missing=0; grand_dupes=0; grand_localonly=0

for wiki in "$WIKI_ROOT"/*; do
  [ -d "$wiki" ] || continue
  WIKINAME=$(basename "$wiki")
  declare -A LOCAL_PLUGINS=()
  declare -A LISTED_PLUGINS=()

  # Local plugins
  if [ -d "$wiki/plugins" ]; then
    while IFS= read -r plugin_info; do
      relpath="${plugin_info#$wiki/plugins/}"
      publisher=$(echo "$relpath" | cut -d'/' -f1)
      pluginname=$(echo "$relpath" | cut -d'/' -f2)
      LOCAL_PLUGINS["$publisher/$pluginname"]="local"
    done < <(find "$wiki/plugins" -mindepth 2 -maxdepth 2 -type f -name plugin.info 2>/dev/null)
  fi

  # Declared plugins
  if [ -f "$wiki/tiddlywiki.info" ]; then
    for listed in $(jq -r '.plugins[]?' "$wiki/tiddlywiki.info"); do
      LISTED_PLUGINS["$listed"]="listed"
    done
  fi

  # Counters
  count_listed=${#LISTED_PLUGINS[@]}
  count_local=0; count_shared=0; count_core=0
  count_missing=0; count_dupes=0; count_localonly=0
  anomalies_printed=0

    # Process plugins
  for plugin in "${!LISTED_PLUGINS[@]}"; do
    status=""; location=""; color="$RESET"
    if [[ -n "${LOCAL_PLUGINS[$plugin]}" ]]; then
      ((count_local++)); status="βœ…"; location="Local"; color=$GREEN
    else
      found=false
      for spath in "${SHARED_PATHS[@]}"; do
        if [ -f "$spath/$plugin/plugin.info" ]; then
          ((count_shared++)); found=true
          status="βœ…"; location="Shared ($spath)"; color=$GREEN
          break
        fi
      done
      if ! $found; then
        if [ -f "$CORE_PATH/plugins/$plugin/plugin.info" ] \
         || [ -f "$CORE_PATH/languages/$plugin/plugin.info" ] \
         || [ -f "$CORE_PATH/themes/$plugin/plugin.info" ]; then
          ((count_core++)); status="βœ…"; location="Core"; color=$GREEN
        else
          ((count_missing++)); status="❌"; location="Missing"; color=$RED
        fi
      fi
    fi

    if [[ "$MODE" == "full" ]]; then
      [[ $anomalies_printed -eq 0 ]] && print_table_header && anomalies_printed=1
      print_table_row "$plugin" "$status" "$location" "$color"
    elif [[ "$MODE" == "anomalies" && "$status" != "βœ…" ]]; then
      [[ $anomalies_printed -eq 0 ]] && print_table_header && anomalies_printed=1
      print_table_row "$plugin" "$status" "$location" "$color"
    fi
  done

  # Local-only / duplicates
  for plugin in "${!LOCAL_PLUGINS[@]}"; do
    if [[ -z "${LISTED_PLUGINS[$plugin]}" ]]; then
      ((count_localonly++))
      [[ $anomalies_printed -eq 0 ]] && print_table_header && anomalies_printed=1
      print_table_row "$plugin" "⚠️" "Local-only" "$YELLOW"
    else
      ((count_dupes++))
      [[ $anomalies_printed -eq 0 ]] && print_table_header && anomalies_printed=1
      print_table_row "$plugin" "⚠️" "Duplicate" "$YELLOW"
    fi
  done

  # If anomalies mode and nothing was printed
  if [[ "$MODE" == "anomalies" && $anomalies_printed -eq 0 ]]; then
    if [[ $PLAIN -eq 1 ]]; then
      echo "No anomalies in $WIKINAME"
    else
      echo "${GREEN}No anomalies in $WIKINAME${RESET}"
    fi
  fi

    # Always print per‑wiki summary row in summary/csv/markdown modes,
  # and also after full/anomalies tables
  if [[ "$MODE" == "summary" || "$MODE" == "csv" || "$MODE" == "markdown" || "$MODE" == "full" || "$MODE" == "anomalies" ]]; then
    print_dashboard_row "$WIKINAME" "$count_listed" "$count_local" "$count_shared" \
                        "$count_core" "$count_missing" "$count_dupes" "$count_localonly"
  fi

  # Accumulate grand totals
  grand_listed=$((grand_listed + count_listed))
  grand_local=$((grand_local + count_local))
  grand_shared=$((grand_shared + count_shared))
  grand_core=$((grand_core + count_core))
  grand_missing=$((grand_missing + count_missing))
  grand_dupes=$((grand_dupes + count_dupes))
  grand_localonly=$((grand_localonly + count_localonly))

done  # end of wiki loop

# ─────────────────────────────────────────────
# Print dashboard footer and grand totals
# ─────────────────────────────────────────────
print_dashboard_footer

case "$MODE" in
  csv)
    printf 'TOTAL,%d,%d,%d,%d,%d,%d,%d\n' \
      "$grand_listed" "$grand_local" "$grand_shared" "$grand_core" \
      "$grand_missing" "$grand_dupes" "$grand_localonly"
    ;;
  markdown)
    printf '| %-15s | %6s | %5s | %6s | %4s | %7s | %5s | %10s |\n' \
      "TOTAL" "$grand_listed" "$grand_local" "$grand_shared" \
      "$grand_core" "$grand_missing" "$grand_dupes" "$grand_localonly"
    ;;
  *)
    echo -e "\nπŸ“Š Grand Totals"
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    printf 'All wikis      : %3s listed β€” %2s local, %2s shared, %2s core, %2s missing, %2s dupes, %2s local-only\n' \
      "$grand_listed" "$grand_local" "$grand_shared" "$grand_core" \
      "$grand_missing" "$grand_dupes" "$grand_localonly"
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    ;;
esac