aboutsummaryrefslogtreecommitdiff
path: root/Makefile
blob: 0c26ea8f9f43f8c931dd6e758c3846a5f46ccecd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
.DEFAULT_GOAL := help
SHELL := /bin/bash

SKILLS_DIR := $(HOME)/.claude/skills
RULES_DIR  := $(HOME)/.claude/rules
HOOKS_DIR  := $(HOME)/.claude/hooks
SKILLS     := c4-analyze c4-diagram debug add-tests respond-to-review review-code fix-issue security-check \
              arch-design arch-decide arch-document arch-evaluate \
              brainstorm codify root-cause-trace five-whys prompt-engineering \
              playwright-js playwright-py frontend-design pairwise-tests \
              finish-branch create-v2mom
RULES      := $(wildcard claude-rules/*.md)
HOOKS      := $(wildcard hooks/*.sh hooks/*.py)
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-hooks uninstall-hooks \
        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<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, node, jq, fzf, ripgrep, emacs, playwright)
	@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 node >/dev/null 2>&1 && echo "  node:       installed ($$(node --version))" || \
		{ echo "  node:       installing..."; $(call install_pkg,nodejs); }
	@command -v npm >/dev/null 2>&1 && echo "  npm:        installed" || \
		{ echo "  npm:        installing..."; $(call install_pkg,npm); }
	@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); }
	@command -v uv >/dev/null 2>&1 && echo "  uv:         installed ($$(uv --version | awk '{print $$NF}'))" || \
		{ echo "  uv:         installing..."; $(call install_pkg,uv); }
	@if [ -d "$(CURDIR)/playwright-js" ]; then \
		if [ -d "$(CURDIR)/playwright-js/node_modules/playwright" ]; then \
			echo "  playwright (js):  installed (skill node_modules present)"; \
		else \
			echo "  playwright (js):  running skill setup (npm install + chromium download ~300 MB)..."; \
			(cd "$(CURDIR)/playwright-js" && npm run setup); \
		fi \
	else \
		echo "  playwright (js):  skipped (playwright-js/ not present)"; \
	fi
	@if [ -d "$(CURDIR)/playwright-py" ]; then \
		if command -v playwright >/dev/null 2>&1; then \
			echo "  playwright (py):  CLI installed ($$(playwright --version 2>&1 | head -1))"; \
		elif command -v uv >/dev/null 2>&1; then \
			echo "  playwright (py):  installing via uv tool (isolated venv)..."; \
			uv tool install playwright; \
			echo "                    (Chromium already cached by playwright-js step; no re-download.)"; \
		else \
			echo "  playwright (py):  skipped — install uv, then re-run 'make deps'."; \
		fi; \
		echo "                    Per-project library import: add 'playwright' to your project's venv"; \
		echo "                    (e.g. 'uv add playwright' or 'pip install playwright' inside .venv)."; \
	else \
		echo "  playwright (py):  skipped (playwright-py/ not present)"; \
	fi
	@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
	@echo ""
	@echo "Hooks:"
	@for hook in $(HOOKS); do \
		name=$$(basename $$hook); \
		if [ -L "$(HOOKS_DIR)/$$name" ]; then \
			echo "  ✓ $$name (installed)"; \
		else \
			echo "  - $$name"; \
		fi \
	done

install-hooks: ## Symlink global hooks into ~/.claude/hooks/ + print settings.json snippet
	@mkdir -p $(HOOKS_DIR)
	@echo "Hooks:"
	@for hook in $(HOOKS); do \
		name=$$(basename $$hook); \
		if [ -L "$(HOOKS_DIR)/$$name" ]; then \
			echo "  skip  $$name (already linked)"; \
		elif [ -e "$(HOOKS_DIR)/$$name" ]; then \
			echo "  WARN  $$name exists and is not a symlink — skipping"; \
		else \
			ln -s "$(CURDIR)/$$hook" "$(HOOKS_DIR)/$$name"; \
			echo "  link  $$name → $(HOOKS_DIR)/$$name"; \
		fi \
	done
	@echo ""
	@echo "Merge this into ~/.claude/settings.json (preserve any existing hooks arrays):"
	@echo ""
	@cat hooks/settings-snippet.json
	@echo ""
	@echo "After merging, reload Claude Code (open /hooks menu once, or restart the session)."

uninstall-hooks: ## Remove global hook symlinks from ~/.claude/hooks/
	@for hook in $(HOOKS); do \
		name=$$(basename $$hook); \
		if [ -L "$(HOOKS_DIR)/$$name" ]; then \
			rm "$(HOOKS_DIR)/$$name"; \
			echo "  rm    $$name"; \
		else \
			echo "  skip  $$name (not a symlink)"; \
		fi \
	done
	@echo ""
	@echo "Note: this does NOT edit ~/.claude/settings.json — remove the hook entries manually."

##@ 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=<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 bundle ([PROJECT=<path>] [FORCE=1])
	@$(MAKE) install-lang LANG=elisp PROJECT="$(PROJECT)" FORCE="$(FORCE)"

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