This is only a suggestion! It applies to TiddlyWiki Node.js edition in which I tested it successfully, running in TUXEDO OS.
Purpose:
Core Objectives
-
Audit TiddlyWiki plugins across multiple wikis It scans each wiki under a given root directory and compares the plugins declared in
tiddlywiki.infowith 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_PATHor your configured shared paths) - Core (bundled with the TiddlyWiki installation)
- Missing (declared but not found anywhere)
-
Local (inside that wikiβs
-
Detect anomalies It flags:
-
Missing plugins (
) -
Localβonly plugins (
declared nowhere else) -
Duplicates (
both local and declared/shared)
-
Missing plugins (
-
Provide flexible reporting modes Users can choose:
-
--fullβ detailed table of all plugins with
/
/
-
--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.
Why itβs useful
- Gives you a dashboardβstyle overview of plugin usage.
- Makes anomalies immediately visible (colorized in terminal, or
/
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