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 += emacs
27emacs : ~/.config/emacs
28
29all += nvim
30nvim : ~/.config/nvim
31
32##@ Claude Code
33
34all += claude-agents claude-settings claude-hooks claude-plugins claude-statusline claude-compat
35claude-agents : ~/.config/claude/agents
36claude-settings : ~/.config/claude/settings.json
37claude-hooks : ~/.config/claude/hooks
38claude-plugins : ~/.config/claude/plugins/session-manager
39claude-statusline : ~/.config/claude/statusline.sh
40
41# Backward compatibility: symlink ~/.claude to ~/.config/claude
42claude-compat : ~/.claude
43~/.claude : force
44 @echo "→ Creating backward compatibility symlink: ~/.claude -> ~/.config/claude"
45 @mkdir -p ~/.config
46 @ln -snf ~/.config/claude ~/.claude
47
48##@ Pi Agent
49# Pi manages: ~/.pi/agent/{auth.json,settings.json}
50# We provide everything else via the ~/.pi pattern rule.
51# Sessions are special: redirected to ai-sync for syncthing sharing.
52
53all += pi-agent pi-agent-settings pi-agent-auth
54pi-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
55pi-agent-settings : pi-agent
56 @$(dotfiles)/pi/agent/ensure-settings.sh
57pi-agent-auth : pi-agent
58 @$(dotfiles)/pi/agent/ensure-auth.sh
59
60~/.pi/agent/sessions : force
61 @echo "→ Linking ~/.local/share/ai-sync/pi-sessions -> ~/.pi/agent/sessions"
62 @mkdir -p ~/.pi/agent ~/.local/share/ai-sync/pi-sessions
63 @if [ -d ~/.pi/agent/sessions ] && [ ! -L ~/.pi/agent/sessions ]; then \
64 echo " ⚠️ Moving existing sessions directory contents to ai-sync"; \
65 cp -a ~/.pi/agent/sessions/. ~/.local/share/ai-sync/pi-sessions/ 2>/dev/null || true; \
66 rm -rf ~/.pi/agent/sessions; \
67 fi
68 @ln -snf ~/.local/share/ai-sync/pi-sessions ~/.pi/agent/sessions
69
70
71
72##@ Herdr
73
74all += herdr
75herdr : ~/.config/herdr/config.toml
76
77##@ AI Shared Config
78
79all += agent-skills agent-skill-manager-bin ai-config agent-skills-link skills-install
80agent-skills : ~/.config/agent-skills
81
82# Symlink each skill from dots/agents/skills/ into all agent skill directories.
83# This is the single source of truth for our own skills.
84AGENT_SKILL_DIRS := ~/.config/claude/skills ~/.agents/skills ~/.pi/agent/skills
85
86agent-skills-link:
87 @# Clean up stale dir-symlink from old layout
88 @if [ -L ~/.config/claude/skills ]; then \
89 echo " ⚠️ Removing stale ~/.config/claude/skills dir-symlink"; \
90 rm ~/.config/claude/skills; \
91 fi
92 @for dir in $(AGENT_SKILL_DIRS); do \
93 mkdir -p $$dir; \
94 done
95 @for skill in $(dotfiles)/agents/skills/*/; do \
96 name=$$(basename $$skill); \
97 for dir in $(AGENT_SKILL_DIRS); do \
98 if [ ! -e "$$dir/$$name" ]; then \
99 ln -snf "$$skill" "$$dir/$$name"; \
100 echo " → $$name -> $$dir"; \
101 fi; \
102 done; \
103 done
104 @for skill in $(HOME)/.agents/skills/*/; do \
105 name=$$(basename $$skill); \
106 for dir in $(AGENT_SKILL_DIRS); do \
107 case "$$dir" in */.agents/skills) continue;; esac; \
108 if [ ! -e "$$dir/$$name" ]; then \
109 ln -snf "$$skill" "$$dir/$$name"; \
110 echo " → $$name -> $$dir (from ~/.agents)"; \
111 fi; \
112 done; \
113 done
114 @echo "✅ Agent skills linked!"
115
116# Declarative skill packages — installed via `skills` CLI if not present.
117# Format: sentinel-skill=package (sentinel is any skill name from the package)
118# Installed to ~/.agents/skills/ and auto-linked to Claude, Pi, etc.
119SKILL_PACKAGES := brainstorming=obra/superpowers make-interfaces-feel-better=jakubkrehel/make-interfaces-feel-better emacsclient=xenodium/emacs-skills
120
121skills-install: agent-skills-link
122 @for entry in $(SKILL_PACKAGES); do \
123 sentinel=$${entry%%=*}; \
124 pkg=$${entry#*=}; \
125 if [ ! -d "$(HOME)/.agents/skills/$$sentinel" ]; then \
126 echo "📦 Installing skills from $$pkg..."; \
127 skills add -g -y "$$pkg" || \
128 echo " ⚠️ Failed to install $$pkg"; \
129 fi; \
130 done
131 @echo "✅ Skill packages installed!"
132
133skills-update:
134 @echo "🔄 Updating all installed skills..."
135 @skills update -g -y
136 @echo "✅ Skills updated!"
137
138ai-config : ~/.config/ai/skills ~/.config/ai/path-policies.json
139
140# Skills shared with claude via cross-link
141~/.config/ai/skills : force
142 @echo "→ Symlinking ~/.config/ai/skills -> ~/.config/claude/skills (shared)"
143 @mkdir -p ~/.config/ai
144 @ln -snf ~/.config/claude/skills ~/.config/ai/skills
145
146# agent-skill-manager lives under config/agent-skills, not bin/
147agent-skill-manager-bin : ~/bin/agent-skill-manager
148~/bin/agent-skill-manager : $(dotfiles)/config/agent-skills/agent-skill-manager force
149 @echo "→ Linking $< → $@"
150 @mkdir -p ~/bin
151 @ln -snf $< $@
152
153##@ Shell
154
155all += zsh
156zsh : ~/.config/zsh/init.zsh ~/.config/zsh/core ~/.config/zsh/tools ~/.config/zsh/functions
157
158##@ Dev Tools
159
160all += git-template copilot-hooks opencode-plugin lazygit lazyworktree lazypr raffi
161git-template : ~/.config/git/template
162copilot-hooks : ~/.config/copilot-hooks
163opencode-plugin : ~/.config/opencode/plugin
164lazygit : ~/.config/lazygit/config.yml
165lazyworktree : ~/.config/lazyworktree/config.yaml
166lazypr : ~/.config/lazypr/config.toml
167raffi : ~/.config/raffi/raffi.yaml ~/.config/raffi/scripts
168
169##@ Jira
170
171all += jayrah jayrat
172jayrah : ~/.config/jayrah/config.yaml
173jayrat : ~/.config/jayrat/config.yaml
174
175##@ GitHub
176
177all += gh-news github-notif-manager
178gh-news : ~/.config/gh-news/config.toml
179github-notif-manager : ~/.config/github-notif-manager/config.yaml
180
181##@ Notifications
182
183all += ntfy-config ntfy-scripts
184ntfy-scripts : ~/.config/ntfy/handle-notification.sh ~/.config/ntfy/acknowledge-notification.sh ~/.config/ntfy/ntfy-update-config
185
186# Generated from template with passage secrets
187ntfy-config : ~/.config/ntfy/client.yml
188~/.config/ntfy/client.yml : $(dotfiles)/config/ntfy/client.yml.in $(dotfiles)/config/ntfy/ntfy-update-config force
189 @echo "⚙️ Generating $@ from template with passage secrets"
190 @$(dotfiles)/config/ntfy/ntfy-update-config
191
192##@ Chat / LLM
193
194all += aichat-config aichat-genconf aichat-roles
195aichat-genconf : ~/.config/aichat/genconf.py
196aichat-roles : ~/.config/aichat/roles
197
198# Generated from template with dynamic model fetching
199aichat-config : ~/.config/aichat/config.yaml
200~/.config/aichat/config.yaml : $(dotfiles)/config/aichat/config.yaml.in $(dotfiles)/config/aichat/genconf.py force
201 @echo "⚙️ Generating $@ with dynamic models and passage secrets"
202 @mkdir -p ~/.config/aichat
203 @$(dotfiles)/config/aichat/genconf.py > $@ 2>/dev/null
204
205##@ Global Agent Instructions
206# One canonical instructions file symlinked to each agent's expected filename.
207all += agent-instructions
208CANON_AGENTS := $(dotfiles)/agents/AGENTS.md
209AGENT_INSTRUCTION_FILES := ~/.pi/agent/AGENTS.md ~/.claude/CLAUDE.md ~/.codex/AGENTS.md ~/.copilot/copilot-instructions.md ~/.gemini/GEMINI.md ~/.config/opencode/AGENTS.md
210agent-instructions : $(AGENT_INSTRUCTION_FILES)
211$(AGENT_INSTRUCTION_FILES) : $(CANON_AGENTS) force
212 @echo "→ Linking $< → $@"
213 @mkdir -p $(@D)
214 @ln -snf $< $@
215
216##@ Meta
217
218all : $(all) pi-extensions-install
219 @echo "✅ All dotfiles installed!"
220
221# Install npm dependencies for pi agent extensions (runtime only, no dev deps)
222pi-extensions-install:
223 @echo "⚡ Installing npm dependencies for pi agent extensions..."
224 @for ext in $(dotfiles)/pi/agent/extensions/*/package.json; do \
225 if [ -f "$$ext" ]; then \
226 dir=$$(dirname $$ext); \
227 has_deps=$$(jq -r '(.dependencies // {}) | length' $$ext 2>/dev/null); \
228 if [ "$$has_deps" -gt 0 ] 2>/dev/null; then \
229 echo " • Installing dependencies in $$(basename $$dir)..."; \
230 (cd $$dir && npm install --omit=dev --silent) || exit 1; \
231 fi; \
232 fi \
233 done
234 @echo "✅ Pi agent extensions dependencies installed!"
235
236help:
237 @echo "Available targets:"
238 @echo " all - Install all dotfiles and pi extensions (default)"
239 @echo " pi-extensions-install - Install npm dependencies for pi agent extensions"
240 @echo ""
241 @echo "Individual components:"
242 @$(foreach target,$(all),echo " $(target)";)
243
244.PHONY: all $(all) pi-extensions-install skills-update help
245.DEFAULT_GOAL := all