.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/*)) # 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= 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 install-elisp install-python list-languages \ diff lint deps ##@ General help: ## Show this help @awk 'BEGIN {FS = ":.*##"; printf "\nrulesets — Claude Code skills, rules, and language bundles\n\nUsage: make \033[36m\033[0m [PROJECT=] [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 \ if [ -L "$(SKILLS_DIR)/$$skill" ]; then \ echo " skip $$skill (already linked)"; \ elif [ -e "$(SKILLS_DIR)/$$skill" ]; then \ echo " WARN $$skill exists and is not a symlink — skipping"; \ else \ ln -s "$(CURDIR)/$$skill" "$(SKILLS_DIR)/$$skill"; \ echo " link $$skill → $(SKILLS_DIR)/$$skill"; \ fi \ done @echo "" @echo "Rules:" @for rule in $(RULES); do \ name=$$(basename $$rule); \ if [ -L "$(RULES_DIR)/$$name" ]; then \ echo " skip $$name (already linked)"; \ elif [ -e "$(RULES_DIR)/$$name" ]; then \ echo " WARN $$name exists and is not a symlink — skipping"; \ else \ ln -s "$(CURDIR)/$$rule" "$(RULES_DIR)/$$name"; \ echo " link $$name → $(RULES_DIR)/$$name"; \ fi \ done @echo "" @echo "done" uninstall: ## Remove global symlinks from ~/.claude/ @echo "Skills:" @for skill in $(SKILLS); do \ if [ -L "$(SKILLS_DIR)/$$skill" ]; then \ rm "$(SKILLS_DIR)/$$skill"; \ echo " rm $$skill"; \ else \ echo " skip $$skill (not a symlink)"; \ fi \ done @echo "" @echo "Rules:" @for rule in $(RULES); do \ name=$$(basename $$rule); \ if [ -L "$(RULES_DIR)/$$name" ]; then \ rm "$(RULES_DIR)/$$name"; \ echo " rm $$name"; \ else \ echo " skip $$name (not a symlink)"; \ fi \ done @echo "" @echo "done" list: ## Show global install status @echo "Skills:" @for skill in $(SKILLS); do \ if [ -L "$(SKILLS_DIR)/$$skill" ]; then \ echo " ✓ $$skill (installed)"; \ else \ echo " - $$skill"; \ fi \ done @echo "" @echo "Rules:" @for rule in $(RULES); do \ name=$$(basename $$rule); \ if [ -L "$(RULES_DIR)/$$name" ]; then \ echo " ✓ $$name (installed)"; \ else \ echo " - $$name"; \ fi \ done ##@ Per-project language bundles list-languages: ## List available language bundles @echo "Available language rulesets (languages/):" @for lang in $(LANGUAGES); do echo " - $$lang"; done install-lang: ## Install language ruleset (LANG= [PROJECT=] [FORCE=1]) @test -n "$(LANG)" || { echo "ERROR: set LANG="; exit 1; } @$(pick_project_shell); \ bash scripts/install-lang.sh "$(LANG)" "$$P" "$(FORCE)" install-elisp: ## Install Elisp bundle ([PROJECT=] [FORCE=1]) @$(MAKE) install-lang LANG=elisp PROJECT="$(PROJECT)" FORCE="$(FORCE)" install-python: ## Install Python bundle ([PROJECT=] [FORCE=1]) @$(MAKE) install-lang LANG=python PROJECT="$(PROJECT)" FORCE="$(FORCE)" ##@ Compare & validate diff: ## Show drift between installed ruleset and repo source (LANG= [PROJECT=]) @test -n "$(LANG)" || { echo "ERROR: set LANG="; 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