main
  1makefile := $(abspath $(lastword $(MAKEFILE_LIST)))
  2dotfiles := $(abspath $(dir $(makefile)))
  3
  4force:
  5
  6define rule.template
  7$(1)/% : $(2)/% force
  8	@echo "→ Linking $$< → $$@"
  9	@mkdir -p $$(@D)
 10	@ln -snf $$< $$@
 11endef
 12
 13rule.define = $(eval $(call rule.template,$(1),$(2)))
 14
 15# Map source directories to target directories
 16$(call rule.define,~/.config,$(dotfiles)/config)
 17$(call rule.define,~/.pi,$(dotfiles)/pi)
 18$(call rule.define,~/bin,$(dotfiles)/bin)
 19$(call rule.define,~/.agents,$(dotfiles)/agents)
 20
 21##@ Desktop
 22
 23all += niri
 24niri : ~/.config/niri/config.kdl
 25
 26all += zoom
 27zoom : ~/.config/zoomus.conf
 28
 29all += emacs
 30emacs : ~/.config/emacs
 31
 32all += nvim
 33nvim : ~/.config/nvim
 34
 35##@ Claude Code
 36
 37all += claude-agents claude-settings claude-hooks claude-plugins claude-statusline claude-compat
 38claude-agents : ~/.config/claude/agents
 39claude-settings : ~/.config/claude/settings.json
 40claude-hooks : ~/.config/claude/hooks
 41claude-plugins : ~/.config/claude/plugins/session-manager
 42claude-statusline : ~/.config/claude/statusline.sh
 43
 44# Backward compatibility: symlink ~/.claude to ~/.config/claude
 45claude-compat : ~/.claude
 46~/.claude : force
 47	@echo "→ Creating backward compatibility symlink: ~/.claude -> ~/.config/claude"
 48	@mkdir -p ~/.config
 49	@ln -snf ~/.config/claude ~/.claude
 50
 51##@ Pi Agent
 52# Pi manages: ~/.pi/agent/{auth.json,settings.json}
 53# We provide everything else via the ~/.pi pattern rule.
 54# Sessions are special: redirected to ai-sync for syncthing sharing.
 55
 56all += pi-agent pi-agent-settings pi-agent-auth nono-profiles
 57pi-agent : ~/.pi/agent/extensions ~/.pi/agent/agents ~/.pi/agent/README.md ~/.pi/agent/keybindings.json ~/.pi/agent/modes.json ~/.pi/agent/models.json ~/.pi/agent/sessions
 58nono-profiles : ~/.config/nono/profiles/pi-nono-local.json ~/.config/nono/profiles/pi-nono-github-ro.json
 59pi-agent-settings : pi-agent
 60	@$(dotfiles)/pi/agent/ensure-settings.sh
 61pi-agent-auth : pi-agent
 62	@$(dotfiles)/pi/agent/ensure-auth.sh
 63
 64~/.pi/agent/sessions : force
 65	@echo "→ Linking ~/.local/share/ai-sync/pi-sessions -> ~/.pi/agent/sessions"
 66	@mkdir -p ~/.pi/agent ~/.local/share/ai-sync/pi-sessions
 67	@if [ -d ~/.pi/agent/sessions ] && [ ! -L ~/.pi/agent/sessions ]; then \
 68		echo "  ⚠️  Moving existing sessions directory contents to ai-sync"; \
 69		cp -a ~/.pi/agent/sessions/. ~/.local/share/ai-sync/pi-sessions/ 2>/dev/null || true; \
 70		rm -rf ~/.pi/agent/sessions; \
 71	fi
 72	@ln -snf ~/.local/share/ai-sync/pi-sessions ~/.pi/agent/sessions
 73
 74
 75
 76##@ Herdr
 77
 78all += herdr
 79herdr : ~/.config/herdr/config.toml
 80
 81##@ AI Shared Config
 82
 83all += agent-skills agent-skill-manager-bin ai-config agent-skills-link skills-install
 84agent-skills : ~/.config/agent-skills
 85
 86# Symlink each skill from dots/agents/skills/ into all agent skill directories.
 87# This is the single source of truth for our own skills.
 88AGENT_SKILL_DIRS := ~/.config/claude/skills ~/.agents/skills ~/.pi/agent/skills
 89
 90agent-skills-link:
 91	@# Clean up stale dir-symlink from old layout
 92	@if [ -L ~/.config/claude/skills ]; then \
 93		echo "  ⚠️  Removing stale ~/.config/claude/skills dir-symlink"; \
 94		rm ~/.config/claude/skills; \
 95	fi
 96	@for dir in $(AGENT_SKILL_DIRS); do \
 97		mkdir -p $$dir; \
 98	done
 99	@for skill in $(dotfiles)/agents/skills/*/; do \
100		name=$$(basename $$skill); \
101		for dir in $(AGENT_SKILL_DIRS); do \
102			if [ ! -e "$$dir/$$name" ]; then \
103				ln -snf "$$skill" "$$dir/$$name"; \
104				echo "  → $$name -> $$dir"; \
105			fi; \
106		done; \
107	done
108	@for skill in $(HOME)/.agents/skills/*/; do \
109		name=$$(basename $$skill); \
110		for dir in $(AGENT_SKILL_DIRS); do \
111			case "$$dir" in */.agents/skills) continue;; esac; \
112			if [ ! -e "$$dir/$$name" ]; then \
113				ln -snf "$$skill" "$$dir/$$name"; \
114				echo "  → $$name -> $$dir (from ~/.agents)"; \
115			fi; \
116		done; \
117	done
118	@echo "✅ Agent skills linked!"
119
120# Declarative skill packages — installed via `skills` CLI if not present.
121# Format: sentinel-skill=package (sentinel is any skill name from the package)
122# Installed to ~/.agents/skills/ and auto-linked to Claude, Pi, etc.
123SKILL_PACKAGES := brainstorming=obra/superpowers make-interfaces-feel-better=jakubkrehel/make-interfaces-feel-better emacsclient=xenodium/emacs-skills
124
125# Upstream skills we deliberately replace with our own dots/agents/skills/<name>.
126# The `skills` CLI has no per-skill exclude, so we delete the upstream copy after
127# install/update and let agent-skills-link put ours in its place.
128# using-git-worktrees: this machine has a single worktree root
129# (~/.local/share/worktrees) plus native tools (lwt, herdr); the upstream skill
130# defaults to project-local .worktrees/ and knows about neither.
131SKILL_SUPPRESS := using-git-worktrees
132
133skills-suppress:
134	@for name in $(SKILL_SUPPRESS); do \
135		if [ -d "$(HOME)/.agents/skills/$$name" ] && [ ! -L "$(HOME)/.agents/skills/$$name" ]; then \
136			echo "🚫 Removing upstream $$name (superseded by dots)"; \
137			rm -rf "$(HOME)/.agents/skills/$$name"; \
138		fi; \
139		for dir in $(AGENT_SKILL_DIRS); do \
140			if [ -L "$$dir/$$name" ] && [ ! -e "$$dir/$$name" ]; then \
141				rm -f "$$dir/$$name"; \
142			fi; \
143		done; \
144	done
145
146skills-install: skills-suppress agent-skills-link
147	@for entry in $(SKILL_PACKAGES); do \
148		sentinel=$${entry%%=*}; \
149		pkg=$${entry#*=}; \
150		if [ ! -d "$(HOME)/.agents/skills/$$sentinel" ]; then \
151			echo "📦 Installing skills from $$pkg..."; \
152			skills add -g -y "$$pkg" || \
153				echo "  ⚠️  Failed to install $$pkg"; \
154		fi; \
155	done
156	@$(MAKE) --no-print-directory skills-suppress agent-skills-link
157	@echo "✅ Skill packages installed!"
158
159skills-update:
160	@echo "🔄 Updating all installed skills..."
161	@skills update -g -y
162	@$(MAKE) --no-print-directory skills-suppress agent-skills-link
163	@echo "✅ Skills updated!"
164
165ai-config : ~/.config/ai/skills ~/.config/ai/path-policies.json
166
167# Skills shared with claude via cross-link
168~/.config/ai/skills : force
169	@echo "→ Symlinking ~/.config/ai/skills -> ~/.config/claude/skills (shared)"
170	@mkdir -p ~/.config/ai
171	@ln -snf ~/.config/claude/skills ~/.config/ai/skills
172
173# agent-skill-manager lives under config/agent-skills, not bin/
174agent-skill-manager-bin : ~/bin/agent-skill-manager
175~/bin/agent-skill-manager : $(dotfiles)/config/agent-skills/agent-skill-manager force
176	@echo "→ Linking $< → $@"
177	@mkdir -p ~/bin
178	@ln -snf $< $@
179
180##@ Shell
181
182all += zsh
183zsh : ~/.config/zsh/init.zsh ~/.config/zsh/core ~/.config/zsh/tools ~/.config/zsh/functions
184
185##@ Dev Tools
186
187all += git-template copilot-hooks opencode-plugin lazygit lazyworktree lazypr raffi
188git-template : ~/.config/git/template
189copilot-hooks : ~/.config/copilot-hooks
190opencode-plugin : ~/.config/opencode/plugin
191lazygit : ~/.config/lazygit/config.yml
192lazyworktree : ~/.config/lazyworktree/config.yaml
193lazypr : ~/.config/lazypr/config.toml
194raffi : ~/.config/raffi/raffi.yaml ~/.config/raffi/scripts
195
196##@ Jira
197
198all += jayrah jayrat
199jayrah : ~/.config/jayrah/config.yaml
200jayrat : ~/.config/jayrat/config.yaml
201
202##@ GitHub
203
204all += gh-news github-notif-manager
205gh-news : ~/.config/gh-news/config.toml
206github-notif-manager : ~/.config/github-notif-manager/config.yaml
207
208##@ Notifications
209
210all += ntfy-config ntfy-scripts
211ntfy-scripts : ~/.config/ntfy/handle-notification.sh ~/.config/ntfy/acknowledge-notification.sh ~/.config/ntfy/ntfy-update-config
212
213# Generated from template with passage secrets
214ntfy-config : ~/.config/ntfy/client.yml
215~/.config/ntfy/client.yml : $(dotfiles)/config/ntfy/client.yml.in $(dotfiles)/config/ntfy/ntfy-update-config force
216	@echo "⚙️  Generating $@ from template with passage secrets"
217	@$(dotfiles)/config/ntfy/ntfy-update-config
218
219##@ Chat / LLM
220
221all += aichat-config aichat-genconf aichat-roles
222aichat-genconf : ~/.config/aichat/genconf.py
223aichat-roles : ~/.config/aichat/roles
224
225# Generated from template with dynamic model fetching
226aichat-config : ~/.config/aichat/config.yaml
227~/.config/aichat/config.yaml : $(dotfiles)/config/aichat/config.yaml.in $(dotfiles)/config/aichat/genconf.py force
228	@echo "⚙️  Generating $@ with dynamic models and passage secrets"
229	@mkdir -p ~/.config/aichat
230	@$(dotfiles)/config/aichat/genconf.py > $@ 2>/dev/null
231
232##@ Global Agent Instructions
233# One canonical instructions file symlinked to each agent's expected filename.
234all += agent-instructions
235CANON_AGENTS := $(dotfiles)/agents/AGENTS.md
236AGENT_INSTRUCTION_FILES := ~/.pi/agent/AGENTS.md ~/.claude/CLAUDE.md ~/.codex/AGENTS.md ~/.copilot/copilot-instructions.md ~/.gemini/GEMINI.md ~/.config/opencode/AGENTS.md
237agent-instructions : $(AGENT_INSTRUCTION_FILES)
238$(AGENT_INSTRUCTION_FILES) : $(CANON_AGENTS) force
239	@echo "→ Linking $< → $@"
240	@mkdir -p $(@D)
241	@ln -snf $< $@
242
243##@ Project Envrc
244# Per-project .envrc files that set PI_START_MODE for automatic pi mode selection.
245# These are plain files (not symlinks) since they live outside the home repo.
246
247all += project-envrc
248PROJECT_ENVRC := ~/src/.envrc ~/src/tektoncd/.envrc ~/src/osp/.envrc
249
250project-envrc : $(PROJECT_ENVRC)
251
252~/src/.envrc : force
253	@echo "→ Creating $@"
254	@mkdir -p $(@D)
255	@printf 'export PI_START_MODE=code-oss-gpt\n' > $@
256
257~/src/tektoncd/.envrc : force
258	@echo "→ Creating $@"
259	@mkdir -p $(@D)
260	@printf 'export PI_START_MODE=code-oss\n' > $@
261
262~/src/osp/.envrc : force
263	@echo "→ Creating $@"
264	@mkdir -p $(@D)
265	@printf 'export PI_START_MODE=code-work\n' > $@
266
267##@ Meta
268
269all : $(all) pi-extensions-install
270	@echo "✅ All dotfiles installed!"
271
272# Install npm dependencies for pi agent extensions (runtime only, no dev deps)
273pi-extensions-install:
274	@echo "⚡ Installing npm dependencies for pi agent extensions..."
275	@for ext in $(dotfiles)/pi/agent/extensions/*/package.json; do \
276		if [ -f "$$ext" ]; then \
277			dir=$$(dirname $$ext); \
278			has_deps=$$(jq -r '(.dependencies // {}) | length' $$ext 2>/dev/null); \
279			if [ "$$has_deps" -gt 0 ] 2>/dev/null; then \
280				echo "  • Installing dependencies in $$(basename $$dir)..."; \
281				(cd $$dir && npm install --omit=dev --silent) || exit 1; \
282			fi; \
283		fi \
284	done
285	@echo "✅ Pi agent extensions dependencies installed!"
286
287help:
288	@echo "Available targets:"
289	@echo "  all                    - Install all dotfiles and pi extensions (default)"
290	@echo "  pi-extensions-install  - Install npm dependencies for pi agent extensions"
291	@echo ""
292	@echo "Individual components:"
293	@$(foreach target,$(all),echo "  $(target)";)
294
295.PHONY: all $(all) pi-extensions-install skills-update skills-suppress help
296.DEFAULT_GOAL := all