aboutsummaryrefslogtreecommitdiff
path: root/scripts/lint.sh
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-04-19 12:58:16 -0500
committerCraig Jennings <c@cjennings.net>2026-04-19 12:58:16 -0500
commitacc3e5a532e433ce6a93afe54a040d0270f42b39 (patch)
tree499fcbefa2dacc3210f0d549453da887b570134d /scripts/lint.sh
parent8d4495d4bc62fc4695617e6d5cc62b47a6d43820 (diff)
downloadrulesets-acc3e5a532e433ce6a93afe54a040d0270f42b39.tar.gz
rulesets-acc3e5a532e433ce6a93afe54a040d0270f42b39.zip
feat(makefile): add deps, diff, lint targets and fzf-picker fallback
Ports useful quality-of-life targets from DeepSat's coding-rulesets Makefile, adapted to this repo's two-scope (global + per-project) structure. New targets: make deps Install claude, jq, fzf, ripgrep, emacs via brew/apt/pacman. Idempotent (skips already-present tools). For new machines and VMs. make diff LANG=<lang> [PROJECT=<path>] Show unified diff between repo source and installed copies in a target project. CLAUDE.md excluded (seed- only, diverges by design). make lint Validate ruleset structure: top-level headings, 'Applies to:' headers on rule files, shebangs and exec bits on hook scripts. Infrastructure: - Help migrated to awk-parsed ##@/## pattern; new targets document themselves via a single trailing `## ...` comment. - fzf-picker fallback: if PROJECT= is unset, install-lang and diff launch fzf over local .git dirs under $HOME. Keeps PROJECT=<path> for scripts/automation; only interactive users hit fzf. scripts/diff-lang.sh Walks the file list the installer would copy, diffs each against the target. scripts/lint.sh Standalone ruleset structure validator.
Diffstat (limited to 'scripts/lint.sh')
-rwxr-xr-xscripts/lint.sh91
1 files changed, 91 insertions, 0 deletions
diff --git a/scripts/lint.sh b/scripts/lint.sh
new file mode 100755
index 0000000..2956aff
--- /dev/null
+++ b/scripts/lint.sh
@@ -0,0 +1,91 @@
+#!/usr/bin/env bash
+# Validate ruleset structure. Runs from the rulesets repo root.
+# Checks:
+# - Every .md rule file starts with a top-level heading
+# - Every rule file has an 'Applies to:' header
+# - Every language CLAUDE.md has a top-level heading
+# - Every hook script has a shebang and is executable
+
+set -u
+
+REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+cd "$REPO_ROOT"
+
+errors=0
+
+warn() {
+ printf ' WARN: %s\n' "$1"
+ errors=$((errors + 1))
+}
+
+check_md_heading() {
+ local f="$1"
+ [ -f "$f" ] || return 0
+ if ! head -1 "$f" | grep -q '^# '; then
+ warn "$f — missing top-level heading"
+ fi
+}
+
+check_md_applies_to() {
+ local f="$1"
+ [ -f "$f" ] || return 0
+ if ! grep -q '^Applies to:' "$f"; then
+ warn "$f — missing 'Applies to:' header"
+ fi
+}
+
+check_hook() {
+ local f="$1"
+ [ -f "$f" ] || return 0
+ if ! head -1 "$f" | grep -q '^#!'; then
+ warn "$f — missing shebang"
+ fi
+ if [ ! -x "$f" ]; then
+ warn "$f — not executable (chmod +x)"
+ fi
+}
+
+echo "Linting rulesets in $REPO_ROOT"
+
+# Generic rules
+for f in claude-rules/*.md; do
+ [ -f "$f" ] || continue
+ check_md_heading "$f"
+ check_md_applies_to "$f"
+done
+
+# Per-language rule files
+for rules_dir in languages/*/claude/rules; do
+ [ -d "$rules_dir" ] || continue
+ for f in "$rules_dir"/*.md; do
+ [ -f "$f" ] || continue
+ check_md_heading "$f"
+ check_md_applies_to "$f"
+ done
+done
+
+# Per-language CLAUDE.md templates
+for claude_md in languages/*/CLAUDE.md; do
+ [ -f "$claude_md" ] || continue
+ check_md_heading "$claude_md"
+done
+
+# Hook scripts
+for h in languages/*/claude/hooks/*.sh languages/*/githooks/*; do
+ [ -f "$h" ] || continue
+ check_hook "$h"
+done
+
+# Shared install/diff/lint scripts (sanity check)
+for s in scripts/*.sh; do
+ [ -f "$s" ] || continue
+ check_hook "$s"
+done
+
+echo "---"
+if [ "$errors" -eq 0 ]; then
+ echo "All checks passed."
+else
+ echo "$errors warning(s)."
+ exit 1
+fi