#!/usr/bin/env sh
# frontend-inspect — scan a project directory and report what it is:
# framework (Astro/Next/SvelteKit/Vite-React), styling (Tailwind/CSS-Modules/
# styled-components), UI-component count, and package-manager lockfile.
#
# USAGE
# frontend-inspect [
] # default: current directory
# frontend-inspect --json # machine-readable JSON output
set -eu
DIR="${1:-.}"
JSON=0
[ "${2:-}" = "--json" ] && JSON=1
usage() {
cat <<'EOF'
Usage: frontend-inspect [] [--json]
Reports:
- Framework (astro / next / sveltekit / vite-react / static / unknown)
- Styling (tailwind4 / tailwind3 / css-modules / plain)
- Package manager (npm / pnpm / yarn / bun)
- Component file count (.tsx / .vue / .svelte / .astro)
- Contains tests? (yes/no)
EOF
}
[ "$DIR" = "-h" ] || [ "$DIR" = "--help" ] && { usage; exit 0; }
[ -d "$DIR" ] || { echo "frontend-inspect: $DIR not a directory" >&2; exit 1; }
PKG="$DIR/package.json"
has_dep() {
# $1 = dep name
[ -f "$PKG" ] || return 1
if command -v jq >/dev/null 2>&1; then
jq -e --arg d "$1" '(.dependencies[$d] // .devDependencies[$d] // null) != null' "$PKG" >/dev/null 2>&1
else
grep -q "\"$1\"" "$PKG" 2>/dev/null
fi
}
detect_framework() {
if has_dep astro; then echo astro; return; fi
if has_dep next; then echo next; return; fi
if has_dep "@sveltejs/kit"; then echo sveltekit; return; fi
if has_dep vite && has_dep react; then echo vite-react; return; fi
if has_dep vite && has_dep vue; then echo vite-vue; return; fi
if has_dep vite; then echo vite; return; fi
[ -f "$DIR/index.html" ] && echo static && return
echo unknown
}
detect_styling() {
if has_dep tailwindcss; then
# Tailwind 4 has `@theme` in CSS and no tailwind.config.js, usually; rough heuristic:
if [ -f "$DIR/tailwind.config.ts" ] || [ -f "$DIR/tailwind.config.js" ] || [ -f "$DIR/tailwind.config.mjs" ]; then
echo tailwind3
else
echo tailwind4
fi
return
fi
if has_dep "styled-components"; then echo styled-components; return; fi
if find "$DIR/src" -maxdepth 3 -name '*.module.css' -print -quit 2>/dev/null | grep -q .; then
echo css-modules
return
fi
echo plain
}
detect_pm() {
[ -f "$DIR/pnpm-lock.yaml" ] && echo pnpm && return
[ -f "$DIR/yarn.lock" ] && echo yarn && return
[ -f "$DIR/bun.lockb" ] && echo bun && return
[ -f "$DIR/package-lock.json" ] && echo npm && return
echo none
}
count_components() {
find "$DIR/src" -type f \( -name '*.tsx' -o -name '*.vue' -o -name '*.svelte' -o -name '*.astro' \) 2>/dev/null | wc -l | tr -d ' '
}
has_tests() {
if [ -f "$PKG" ] && (has_dep vitest || has_dep jest || has_dep "@playwright/test"); then
echo yes
else
echo no
fi
}
FW="$(detect_framework)"
ST="$(detect_styling)"
PM="$(detect_pm)"
CC="$(count_components)"
TS="$(has_tests)"
if [ "$JSON" = "1" ]; then
printf '{"dir":"%s","framework":"%s","styling":"%s","pm":"%s","components":%s,"tests":"%s"}\n' \
"$DIR" "$FW" "$ST" "$PM" "$CC" "$TS"
else
printf "dir: %s\n" "$DIR"
printf "framework: %s\n" "$FW"
printf "styling: %s\n" "$ST"
printf "pm: %s\n" "$PM"
printf "components: %s\n" "$CC"
printf "tests: %s\n" "$TS"
fi