aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile112
-rwxr-xr-xscripts/diff-lang.sh80
-rwxr-xr-xscripts/lint.sh91
3 files changed, 250 insertions, 33 deletions
diff --git a/Makefile b/Makefile
index cf1fd5d..98fca36 100644
--- a/Makefile
+++ b/Makefile
@@ -1,31 +1,67 @@
+.DEFAULT_GOAL := help
+SHELL := /bin/bash
+
SKILLS_DIR := $(HOME)/.claude/skills
-RULES_DIR := $(HOME)/.claude/rules
-SKILLS := c4-analyze c4-diagram debug add-tests respond-to-review review-pr fix-issue security-check
-RULES := $(wildcard claude-rules/*.md)
-LANGUAGES := $(notdir $(wildcard languages/*))
+RULES_DIR := $(HOME)/.claude/rules
+SKILLS := c4-analyze c4-diagram debug add-tests respond-to-review review-pr fix-issue security-check
+RULES := $(wildcard claude-rules/*.md)
+LANGUAGES := $(notdir $(wildcard languages/*))
+
+# Pick target project — use PROJECT= or interactive fzf over local .git dirs.
+# Defined inline in each recipe (not via $(shell)) so fzf only runs when needed.
+define pick_project_shell
+ P="$(PROJECT)"; \
+ if [ -z "$$P" ]; then \
+ if ! command -v fzf >/dev/null 2>&1; then \
+ echo "ERROR: PROJECT=<path> not set and fzf is not installed" >&2; \
+ exit 1; \
+ fi; \
+ P=$$(find $$HOME -maxdepth 4 -name .git -type d 2>/dev/null \
+ | sed 's|/\.git$$||' | sort \
+ | fzf --prompt="Target project> " 2>/dev/null); \
+ test -n "$$P" || { echo "No target selected." >&2; exit 1; }; \
+ fi
+endef
+
+# Cross-platform package install helper (brew/apt/pacman)
+define install_pkg
+$(if $(shell command -v brew 2>/dev/null),brew install $(1),\
+$(if $(shell command -v apt-get 2>/dev/null),sudo apt-get install -y $(1),\
+$(if $(shell command -v pacman 2>/dev/null),sudo pacman -S --noconfirm $(1),\
+$(error No supported package manager found (brew/apt-get/pacman)))))
+endef
.PHONY: help install uninstall list \
- install-lang list-languages install-elisp install-python
+ install-lang install-elisp install-python list-languages \
+ diff lint deps
-help:
- @echo "rulesets — Claude Code skills, rules, and language bundles"
- @echo ""
- @echo " Global install (symlinks into ~/.claude/):"
- @echo " make install - Install skills and rules globally"
- @echo " make uninstall - Remove the symlinks"
- @echo " make list - Show install status"
- @echo ""
- @echo " Per-project language rulesets:"
- @echo " make install-lang LANG=<lang> PROJECT=<path> [FORCE=1]"
- @echo " make install-elisp PROJECT=<path> [FORCE=1] (shortcut)"
- @echo " make install-python PROJECT=<path> [FORCE=1] (shortcut)"
- @echo " make list-languages - Show available language bundles"
- @echo ""
- @echo " FORCE=1 overwrites an existing CLAUDE.md (other files always overwrite)."
- @echo ""
- @echo "Available languages: $(LANGUAGES)"
+##@ General
-install:
+help: ## Show this help
+ @awk 'BEGIN {FS = ":.*##"; printf "\nrulesets — Claude Code skills, rules, and language bundles\n\nUsage: make \033[36m<target>\033[0m [PROJECT=<path>] [LANG=<lang>] [FORCE=1]\n"} \
+ /^[a-zA-Z_-]+:.*##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } \
+ /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST)
+ @printf "\nAvailable languages: %s\n" "$(LANGUAGES)"
+
+##@ Dependencies
+
+deps: ## Install required tools (claude, jq, fzf, ripgrep, emacs)
+ @echo "Checking dependencies..."
+ @command -v claude >/dev/null 2>&1 && echo " claude: installed" || \
+ { echo " claude: installing via npm..."; npm install -g @anthropic-ai/claude-code; }
+ @command -v jq >/dev/null 2>&1 && echo " jq: installed" || \
+ { echo " jq: installing..."; $(call install_pkg,jq); }
+ @command -v fzf >/dev/null 2>&1 && echo " fzf: installed" || \
+ { echo " fzf: installing..."; $(call install_pkg,fzf); }
+ @command -v rg >/dev/null 2>&1 && echo " ripgrep: installed" || \
+ { echo " ripgrep: installing..."; $(call install_pkg,ripgrep); }
+ @command -v emacs >/dev/null 2>&1 && echo " emacs: installed" || \
+ { echo " emacs: installing..."; $(call install_pkg,emacs); }
+ @echo "Done."
+
+##@ Global install (symlinks into ~/.claude/)
+
+install: ## Symlink skills and rules into ~/.claude/
@mkdir -p $(SKILLS_DIR) $(RULES_DIR)
@echo "Skills:"
@for skill in $(SKILLS); do \
@@ -54,7 +90,7 @@ install:
@echo ""
@echo "done"
-uninstall:
+uninstall: ## Remove global symlinks from ~/.claude/
@echo "Skills:"
@for skill in $(SKILLS); do \
if [ -L "$(SKILLS_DIR)/$$skill" ]; then \
@@ -78,7 +114,7 @@ uninstall:
@echo ""
@echo "done"
-list:
+list: ## Show global install status
@echo "Skills:"
@for skill in $(SKILLS); do \
if [ -L "$(SKILLS_DIR)/$$skill" ]; then \
@@ -98,19 +134,29 @@ list:
fi \
done
-# --- Per-project language rulesets ---
+##@ Per-project language bundles
-list-languages:
+list-languages: ## List available language bundles
@echo "Available language rulesets (languages/):"
@for lang in $(LANGUAGES); do echo " - $$lang"; done
-install-lang:
- @test -n "$(LANG)" || { echo "ERROR: set LANG=<language> (try: make list-languages)"; exit 1; }
- @test -n "$(PROJECT)" || { echo "ERROR: set PROJECT=<path>"; exit 1; }
- @bash scripts/install-lang.sh "$(LANG)" "$(PROJECT)" "$(FORCE)"
+install-lang: ## Install language ruleset (LANG=<lang> [PROJECT=<path>] [FORCE=1])
+ @test -n "$(LANG)" || { echo "ERROR: set LANG=<language>"; exit 1; }
+ @$(pick_project_shell); \
+ bash scripts/install-lang.sh "$(LANG)" "$$P" "$(FORCE)"
-install-elisp:
+install-elisp: ## Install Elisp bundle ([PROJECT=<path>] [FORCE=1])
@$(MAKE) install-lang LANG=elisp PROJECT="$(PROJECT)" FORCE="$(FORCE)"
-install-python:
+install-python: ## Install Python bundle ([PROJECT=<path>] [FORCE=1])
@$(MAKE) install-lang LANG=python PROJECT="$(PROJECT)" FORCE="$(FORCE)"
+
+##@ Compare & validate
+
+diff: ## Show drift between installed ruleset and repo source (LANG=<lang> [PROJECT=<path>])
+ @test -n "$(LANG)" || { echo "ERROR: set LANG=<language>"; exit 1; }
+ @$(pick_project_shell); \
+ bash scripts/diff-lang.sh "$(LANG)" "$$P"
+
+lint: ## Validate ruleset structure (headings, Applies-to, shebangs, exec bits)
+ @bash scripts/lint.sh
diff --git a/scripts/diff-lang.sh b/scripts/diff-lang.sh
new file mode 100755
index 0000000..a72d2b9
--- /dev/null
+++ b/scripts/diff-lang.sh
@@ -0,0 +1,80 @@
+#!/usr/bin/env bash
+# Diff installed rulesets in a target project vs the repo source.
+# Usage: diff-lang.sh <language> <project-path>
+#
+# Walks every file the installer would copy and shows a unified diff for
+# any that differ. Files missing in the target are flagged separately.
+
+set -u
+
+LANG="${1:-}"
+PROJECT="${2:-}"
+
+if [ -z "$LANG" ] || [ -z "$PROJECT" ]; then
+ echo "Usage: $0 <language> <project-path>" >&2
+ exit 1
+fi
+
+REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+SRC="$REPO_ROOT/languages/$LANG"
+
+[ -d "$SRC" ] || { echo "ERROR: no ruleset for '$LANG'" >&2; exit 1; }
+[ -d "$PROJECT" ] || { echo "ERROR: project path does not exist: $PROJECT" >&2; exit 1; }
+PROJECT="$(cd "$PROJECT" && pwd)"
+
+changed=0
+missing=0
+
+compare_file() {
+ local src="$1" dst="$2"
+ if [ ! -f "$dst" ]; then
+ echo "MISSING: $dst"
+ missing=$((missing + 1))
+ return
+ fi
+ if ! diff -q "$src" "$dst" >/dev/null 2>&1; then
+ echo "--- $src"
+ echo "+++ $dst"
+ diff -u "$src" "$dst" | tail -n +3
+ echo
+ changed=$((changed + 1))
+ fi
+}
+
+echo "Comparing '$LANG' ruleset against $PROJECT"
+echo
+
+# Generic rules (claude-rules/*.md → .claude/rules/)
+for f in "$REPO_ROOT/claude-rules"/*.md; do
+ [ -f "$f" ] || continue
+ name="$(basename "$f")"
+ compare_file "$f" "$PROJECT/.claude/rules/$name"
+done
+
+# Language .claude/ tree
+if [ -d "$SRC/claude" ]; then
+ while IFS= read -r f; do
+ rel="${f#$SRC/claude/}"
+ compare_file "$f" "$PROJECT/.claude/$rel"
+ done < <(find "$SRC/claude" -type f)
+fi
+
+# CLAUDE.md is seed-only (install won't overwrite without FORCE=1), so skip it
+# in normal diff output. Users can diff it manually if curious.
+
+# githooks/
+if [ -d "$SRC/githooks" ]; then
+ while IFS= read -r f; do
+ rel="${f#$SRC/githooks/}"
+ compare_file "$f" "$PROJECT/githooks/$rel"
+ done < <(find "$SRC/githooks" -type f)
+fi
+
+echo "---"
+if [ "$changed" -eq 0 ] && [ "$missing" -eq 0 ]; then
+ echo "No differences."
+else
+ echo "Summary: $changed differ, $missing missing."
+ [ "$changed" -gt 0 ] && exit 1
+ [ "$missing" -gt 0 ] && exit 2
+fi
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