-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMakefile
More file actions
422 lines (351 loc) · 17.3 KB
/
Copy pathMakefile
File metadata and controls
422 lines (351 loc) · 17.3 KB
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
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
PI_TARGET := $(HOME)/.pi
# sources.toml is read/written via tomllib, so python >= 3.11 is required.
# Absolute nix-profile path (python313 in nix/home/packages.nix): hooks and CI
# invoke make without the interactive shell's PATH. Falls back to PATH python3
# where the profile doesn't exist (e.g. Linux CI).
PYTHON ?= $(shell [ -x /etc/profiles/per-user/$$USER/bin/python3 ] && echo /etc/profiles/per-user/$$USER/bin/python3 || echo python3)
# $HOME dotfile packages, now linked per-file by home-manager (nix/home/).
# packages/ stays the source of truth for content; this list drives
# managed-targets.sh so doctor keeps health-checking every target.
HM_PACKAGES := zsh git atuin btop gh bin
# The brewfile is generated by the nix-darwin homebrew module
# (nix/darwin/homebrew.nix); scripts/nix-brewfile.sh prints its path.
# The local host name selects the darwinConfigurations entry; a new machine
# needs its own entry in flake.nix (see mkDarwinHost).
NIX_HOST := $(shell scutil --get LocalHostName)
# Absolute paths: make's shell (and sudo's secure_path) don't carry the
# nix profile dirs.
NIX := /nix/var/nix/profiles/default/bin/nix
DARWIN_REBUILD := /run/current-system/sw/bin/darwin-rebuild
MANIFESTS := $(CURDIR)/manifests
PI_SKILLS_REPO := https://github.com/badlogic/pi-skills
PI_SKILLS_CACHE := /tmp/pi-skills
PI_SKILLS_DIR := $(CURDIR)/skills
CLAUDE_CONFIG_DIRS := $(HOME)/.claude-personal $(HOME)/.claude-work
CLAUDE_DIR := $(CURDIR)/packages/claude
PLUGINS_FILE := $(MANIFESTS)/claude-plugins.txt
SCRIPTS_DIR := $(CLAUDE_DIR)/scripts
# Harness links (claude → each profile, pi → ~/.pi) are home-manager
# out-of-store symlinks — see nix/home/harness.nix; the entry lists below
# still drive doctor's checks.
CLAUDE_PKG_ENTRIES := CLAUDE.md commands rules scripts agents skills
PI_PKG_ENTRIES := agent extensions prompts skills README.md
RESOURCE_MANAGER := $(CURDIR)/scripts/resource-manager.sh
# NVIDIA SkillSpector release the security scan and its baselines were reviewed
# against. Bump deliberately, then re-run `make skills-scan` and re-review
# skills/.security/skillspector/*.json — rule ids and finding text can change upstream.
SKILLSPECTOR_REF ?= v2.9.5
# Cap on the always-loaded context this repo injects per session (see
# `make context-budget`). Raise it deliberately — the diff is the alert.
CONTEXT_BUDGET_TOKENS ?= 12000
export CONTEXT_BUDGET_TOKENS
SKILLS_VENDOR := $(CURDIR)/scripts/skills-vendor.sh
PI_MONO_REPO := https://github.com/badlogic/pi-mono
PI_MONO_CACHE := /tmp/pi-mono
PI_MONO_EXTENSIONS_SRC := $(PI_MONO_CACHE)/packages/coding-agent/examples/extensions
PI_EXTENSIONS_DIR := $(CURDIR)/pi/extensions
PI_EXTENSIONS := \
confirm-destructive.ts \
dirty-repo-guard.ts \
mac-system-theme.ts \
permission-gate.ts \
protected-paths.ts \
status-line.ts \
todo.ts \
notify.ts \
handoff.ts \
subagent
SHELL := /bin/bash
# Modern bash for the repo's scripts: /bin/bash is 3.2 (parser bugs bite
# drift.sh); prefer the nix profile bash, fall back to PATH bash (Linux CI).
BASH := $(shell [ -x /etc/profiles/per-user/$$USER/bin/bash ] && echo /etc/profiles/per-user/$$USER/bin/bash || echo bash)
.PHONY: install uninstall doctor drift python-check \
nix-build nix-switch nix-check nix-check-soft nix-fmt nix-rollback nix-update nix-gc \
brew-install brew-check brew-dump cask-adopt \
go-install npm-install pipx-install tools-install \
macos-apply raycast-export \
harness-link harness-unlink \
skills-sync extensions-sync plugins-check plugins-sync \
skills-find skills-add \
skills-fetch skills-materialize skills-list skills-update skills-update-all skills-category skills-delete \
skills-catalog suites-catalog skills-doctor context-budget skills-scan skillspector-install usage-report \
agents-fetch agents-list agents-update agents-update-all agents-category agents-delete \
agents-doctor preflight lint test
install: nix-switch skills-materialize tools-install
uninstall:
@echo "the declared system is removed by darwin-uninstaller; see nix-darwin docs"
# Nix (nix-darwin + home-manager). nix-build previews without activating;
# nix-switch is the apply verb for the whole declared system.
nix-build:
$(DARWIN_REBUILD) build --flake $(CURDIR)#$(NIX_HOST)
nix-switch:
sudo $(DARWIN_REBUILD) switch --flake $(CURDIR)#$(NIX_HOST)
nix-check:
$(NIX) flake check $(CURDIR)
$(NIX) run nixpkgs#statix -- check $(CURDIR)/nix
nix-fmt:
$(NIX) fmt
nix-rollback:
sudo $(DARWIN_REBUILD) switch --flake $(CURDIR)#$(NIX_HOST) --rollback
# Lockfile bump — review `git diff flake.lock` like a Brewfile re-curation.
# (CI also opens a weekly bump PR: .github/workflows/update-flake-lock.yml.)
nix-update:
$(NIX) flake update
# Prune generations older than 30 days and dedupe the store. Run with the
# monthly nix-update ritual; system generations are root-owned, hence sudo.
nix-gc:
sudo $(NIX)-collect-garbage --delete-older-than 30d
$(NIX) store optimise
brew-install:
@echo "brew packages are declared in nix/darwin/homebrew.nix — run: make nix-switch"
brew-check:
brew bundle check --file=$$($(CURDIR)/scripts/nix-brewfile.sh)
# Regenerate Brewfile.dump (gitignored) to diff against the declared set in
# nix/darwin/homebrew.nix; never writes the nix config itself.
brew-dump:
brew bundle dump --file=$(CURDIR)/Brewfile.dump --describe --force
@echo "wrote Brewfile.dump — diff against nix/darwin/homebrew.nix to re-curate"
# Take over apps that were installed outside brew (pkg-based casks prompt for sudo).
cask-adopt:
brew install --cask --adopt 1password claude devin-desktop google-chrome \
openvpn-connect tailscale-app wispr-flow
tools-install: go-install npm-install pipx-install
# Installs each module from manifests/go-tools.txt; lines without @version get @latest.
go-install:
@command -v go >/dev/null || { echo "go not found — run: make nix-switch"; exit 1; }
@grep -vE '^[[:space:]]*#|^[[:space:]]*$$' $(MANIFESTS)/go-tools.txt | while read -r mod; do \
case "$$mod" in *@*) ;; *) mod="$$mod@latest" ;; esac; \
echo "go install $$mod"; \
go install "$$mod"; \
done
# Warn-and-skip when npm is absent (fresh machine before nvm install --lts),
# so tools-install still reaches pipx-install.
npm-install:
@if command -v npm >/dev/null; then \
grep -vE '^[[:space:]]*#|^[[:space:]]*$$' $(MANIFESTS)/npm-globals.txt | xargs npm install -g; \
else \
echo "warn: npm not found — run 'nvm install --lts' then 'make npm-install'"; \
fi
pipx-install:
@command -v pipx >/dev/null || { echo "pipx not found — run: make brew-install"; exit 1; }
@grep -vE '^[[:space:]]*#|^[[:space:]]*$$' $(MANIFESTS)/pipx-tools.txt | while read -r pkg; do \
pipx install "$$pkg"; \
done
@echo "!! REVIEW 'git diff' BEFORE COMMITTING — adopt replaces repo files with the live ones !!"
macos-apply:
$(BASH) $(CURDIR)/macos/defaults.sh
doctor:
$(BASH) $(CURDIR)/scripts/doctor.sh
drift:
$(BASH) $(CURDIR)/scripts/drift.sh
# Raycast keeps settings in an encrypted local DB (extension configs can hold
# API tokens), so its own encrypted export is the backup mechanism — never this
# repo (*.rayconfig is gitignored). Opens the export dialog; save the file to a
# private location (iCloud Drive / 1Password). Restore on a new Mac via
# Raycast Settings -> Advanced -> Import.
raycast-export:
open "raycast://extensions/raycast/raycast/export-settings-data"
# Harness links are created by home-manager (nix/home/harness.nix).
harness-link:
@echo "harness links are managed by home-manager — run: make nix-switch"
harness-unlink:
@echo "remove nix/home/harness.nix from nix/home/default.nix and run: make nix-switch"
plugins-check:
@test -f $(PLUGINS_FILE) || { echo "missing: $(PLUGINS_FILE)"; exit 1; }
@command -v jq >/dev/null || { echo "jq required"; exit 1; }
@desired=$$(grep -v '^[[:space:]]*#' $(PLUGINS_FILE) | grep -v '^[[:space:]]*$$' | sort -u); \
for target in $(CLAUDE_CONFIG_DIRS); do \
echo "== $$target =="; \
file=$$target/plugins/installed_plugins.json; \
if [ ! -f $$file ]; then \
echo " no installed_plugins.json"; \
continue; \
fi; \
installed=$$(jq -r '.plugins | to_entries[] | select(.value | map(.scope) | index("user")) | .key' $$file | sort -u); \
missing=$$(comm -23 <(echo "$$desired") <(echo "$$installed")); \
extra=$$(comm -13 <(echo "$$desired") <(echo "$$installed")); \
if [ -n "$$missing" ]; then \
echo " missing (run /plugin install <name> in this profile):"; \
echo "$$missing" | sed 's/^/ /'; \
fi; \
if [ -n "$$extra" ]; then \
echo " extra (user-scoped, not in plugins.txt):"; \
echo "$$extra" | sed 's/^/ /'; \
fi; \
if [ -z "$$missing" ] && [ -z "$$extra" ]; then \
echo " in sync"; \
fi; \
done
plugins-sync:
@test -f $(PLUGINS_FILE) || { echo "missing: $(PLUGINS_FILE)"; exit 1; }
@command -v jq >/dev/null || { echo "jq required"; exit 1; }
@desired=$$(grep -v '^[[:space:]]*#' $(PLUGINS_FILE) | grep -v '^[[:space:]]*$$' | sort -u); \
for target in $(CLAUDE_CONFIG_DIRS); do \
case $$target in \
*personal*) wrap=pclaude ;; \
*work*) wrap=wclaude ;; \
*) wrap="claude (with CLAUDE_CONFIG_DIR=$$target)" ;; \
esac; \
echo "== $$target =="; \
file=$$target/plugins/installed_plugins.json; \
if [ ! -f $$file ]; then \
installed=""; \
else \
installed=$$(jq -r '.plugins | to_entries[] | select(.value | map(.scope) | index("user")) | .key' $$file | sort -u); \
fi; \
missing=$$(comm -23 <(echo "$$desired") <(echo "$$installed")); \
if [ -z "$$missing" ]; then \
echo " in sync"; \
continue; \
fi; \
echo " Start a session with \`$$wrap\` and paste:"; \
echo "$$missing" | sed 's|^| /plugin install |'; \
done
skills-sync:
if [ -d $(PI_SKILLS_CACHE)/.git ]; then \
git -C $(PI_SKILLS_CACHE) pull --ff-only; \
else \
git clone --depth=1 $(PI_SKILLS_REPO) $(PI_SKILLS_CACHE); \
fi
mkdir -p $(PI_SKILLS_DIR)
for dir in $$(find $(PI_SKILLS_CACHE) -mindepth 1 -maxdepth 1 -type d ! -name '.git'); do \
cp -r $$dir $(PI_SKILLS_DIR)/; \
done
extensions-sync:
if [ -d $(PI_MONO_CACHE)/.git ]; then \
git -C $(PI_MONO_CACHE) pull --ff-only; \
else \
git clone --depth=1 $(PI_MONO_REPO) $(PI_MONO_CACHE); \
fi
mkdir -p $(PI_EXTENSIONS_DIR)
for ext in $(PI_EXTENSIONS); do \
cp -r $(PI_MONO_EXTENSIONS_SRC)/$$ext $(PI_EXTENSIONS_DIR)/; \
done
# --- skills.sh discovery + fetch (scripts/skills-vendor.sh) ----------------
# Front-end onto the vercel-labs `skills` CLI (npx skills, the skills.sh
# ecosystem). skills-find discovers; skills-add fetches via the CLI and vendors
# the result into skills/ through resource-manager.sh, so each lands with a
# sources.toml entry and stays manageable by skills-list / skills-update / skills-delete.
# The CLI is used only as a resolver/fetcher — it never installs per-agent, so
# the two Claude profiles and packages/claude/agents/ subagents are unaffected.
# Set GITHUB_TOKEN (or have `gh` logged in) to avoid anonymous rate limits.
skills-find:
@command -v npx >/dev/null || { echo "npx (Node.js) required"; exit 1; }
npx -y skills@latest find $(if $(Q),"$(Q)") $(if $(OWNER),--owner "$(OWNER)")
skills-add:
@test -n "$(SOURCE)" || { echo "usage: make skills-add SOURCE=owner/repo [SKILL='a b'] [ALL=1] [REF=main] [FORCE=1]"; exit 1; }
@$(SKILLS_VENDOR) --source "$(SOURCE)" \
$(if $(SKILL),--skill "$(SKILL)") \
$(if $(ALL),--all) \
$(if $(REF),--ref "$(REF)") \
$(if $(CATEGORY),--category "$(CATEGORY)") \
$(if $(FORCE),--force)
# --- Source management (scripts/resource-manager.sh) -----------------------
# Fetch one skill (dir under skills/) or agent (.md under packages/claude/agents/) from
# any repo/subpath, tracking its source in the sources.toml manifest so it can be
# listed, updated, and deleted.
skills-fetch:
@$(RESOURCE_MANAGER) --kind skill fetch \
$(if $(URL),--url "$(URL)") \
$(if $(REPO),--repo "$(REPO)") \
$(if $(SUBPATH),--subpath "$(SUBPATH)") \
$(if $(REF),--ref "$(REF)") \
$(if $(NAME),--name "$(NAME)") \
$(if $(CATEGORY),--category "$(CATEGORY)") \
$(if $(FORCE),--force)
# Reconstruct vendored skills' working files from the pinned commits in
# sources.toml (their files are gitignored, not committed). Idempotent —
# skips any skill already present at the right commit. Run by `make install`;
# NAME=<skill> materializes one, FORCE=1 re-fetches even if present.
skills-materialize:
@$(RESOURCE_MANAGER) --kind skill materialize $(if $(NAME),--name "$(NAME)") $(if $(FORCE),--force)
skills-list:
@$(RESOURCE_MANAGER) --kind skill list
skills-update:
@$(RESOURCE_MANAGER) --kind skill update --name "$(NAME)"
skills-update-all:
@$(RESOURCE_MANAGER) --kind skill update --all
skills-category:
@test -n "$(NAME)" -a -n "$(CATEGORY)" || { echo "usage: make skills-category NAME=<skill> CATEGORY=<category>"; exit 1; }
@$(RESOURCE_MANAGER) --kind skill category --name "$(NAME)" --category "$(CATEGORY)"
skills-delete:
@$(RESOURCE_MANAGER) --kind skill delete --name "$(NAME)" $(if $(YES),--yes)
# Regenerate the category-grouped skills tables in README.md (between the
# skills-catalog markers) from each skill's sidecar category + SKILL.md
# description. CHECK=1 only verifies (exit 1 if stale) without writing.
skills-catalog:
@$(RESOURCE_MANAGER) --kind skill catalog $(if $(CHECK),--check)
# Regenerate each suite README's generated block (skill table + install
# command) and the Suites index in README.md from suites/*/suite.json.
# CHECK=1 only verifies (exit 1 if stale) without writing.
suites-catalog:
@$(RESOURCE_MANAGER) --kind skill suites $(if $(CHECK),--check)
# Estimate the always-loaded context this repo injects into every Claude Code
# session (shared CLAUDE.md + rules, skill/agent/command names+descriptions).
# CHECK=1 exits 1 when the total exceeds CONTEXT_BUDGET_TOKENS (also enforced
# by skills-doctor). TOP=N lists the N heaviest skill descriptions.
context-budget:
@$(RESOURCE_MANAGER) --kind skill budget $(if $(CHECK),--check) $(if $(TOP),--top $(TOP))
# Aggregate the usage telemetry (packages/claude/scripts/usage-log-hook.py) of every
# profile: subagent spend by agent type × model, per-day cache-hit ratio.
# SINCE=N limits to the last N days (default 30).
usage-report:
@$(PYTHON) $(SCRIPTS_DIR)/usage-report.py $(if $(SINCE),--since $(SINCE)) $(foreach d,$(CLAUDE_CONFIG_DIRS),--log "$(d)/usage.jsonl")
# Security-scan skills with NVIDIA SkillSpector (scripts/skills-scan.py):
# every skill by default, NAME=x for one, LLM=1 adds the semantic pass via the
# local `claude` CLI, SHOW=1 lists baseline-suppressed findings, REPORT=file
# writes the combined JSON. Fails on any residual HIGH/CRITICAL finding or a
# score >= FAIL_AT (default 50). Accepted findings live in skills/.security/skillspector/.
# skills-fetch / skills-update run the same scan on the staged skill before
# installing it (SKILLS_SCAN=0 skips).
skills-scan:
@$(PYTHON) $(CURDIR)/scripts/skills-scan.py $(if $(NAME),--name "$(NAME)") $(if $(LLM),--llm) $(if $(SHOW),--show-suppressed) $(if $(REPORT),--report "$(REPORT)") $(if $(FAIL_AT),--fail-at $(FAIL_AT)) $(if $(QUIET),--quiet)
# Install (or move to) the pinned SkillSpector release with uv.
skillspector-install:
@uv tool install --force "git+https://github.com/NVIDIA/skillspector.git@$(SKILLSPECTOR_REF)"
# Validate every skill (SKILL.md present, name/description frontmatter,
# sidecar + category), that the README catalog is current, and that the
# always-loaded context stays under CONTEXT_BUDGET_TOKENS.
skills-doctor:
@$(RESOURCE_MANAGER) --kind skill doctor
agents-fetch:
@$(RESOURCE_MANAGER) --kind agent fetch \
$(if $(URL),--url "$(URL)") \
$(if $(REPO),--repo "$(REPO)") \
$(if $(SUBPATH),--subpath "$(SUBPATH)") \
$(if $(REF),--ref "$(REF)") \
$(if $(NAME),--name "$(NAME)") \
$(if $(CATEGORY),--category "$(CATEGORY)") \
$(if $(FORCE),--force)
agents-list:
@$(RESOURCE_MANAGER) --kind agent list
agents-update:
@$(RESOURCE_MANAGER) --kind agent update --name "$(NAME)"
agents-update-all:
@$(RESOURCE_MANAGER) --kind agent update --all
agents-category:
@test -n "$(NAME)" -a -n "$(CATEGORY)" || { echo "usage: make agents-category NAME=<agent> CATEGORY=<category>"; exit 1; }
@$(RESOURCE_MANAGER) --kind agent category --name "$(NAME)" --category "$(CATEGORY)"
agents-delete:
@$(RESOURCE_MANAGER) --kind agent delete --name "$(NAME)" $(if $(YES),--yes)
agents-doctor:
@$(RESOURCE_MANAGER) --kind agent doctor
# The pre-flight gate, defined once: both doctors (skills-doctor already covers
# catalog, suites, and the context budget) plus a syntax pass over every script.
# The project commit-gate hook (scripts/precommit-gate-hook.sh) and CI run this
# same target; a new check goes here and nowhere else.
preflight: python-check skills-doctor agents-doctor lint nix-check-soft
# nix-check when nix is available, warn-and-skip where it isn't (Linux CI's
# preflight job, machines without nix) — the CI nix job runs it for real.
nix-check-soft:
@if [ -x $(NIX) ]; then $(MAKE) nix-check; else echo "warn: nix not installed — skipping nix-check"; fi
python-check:
@$(PYTHON) -c 'import tomllib' 2>/dev/null \
|| { echo "python3 >= 3.11 with tomllib is required (brew install python)"; exit 1; }
lint:
@for f in scripts/*.sh packages/claude/scripts/*.sh; do $(BASH) -n "$$f" || exit 1; done
@$(PYTHON) -m py_compile scripts/*.py packages/claude/scripts/*.py
# The repo's own regression tests: every scripts/test-*.sh and scripts/test-*.py.
test:
@set -e; for t in scripts/test-*.sh; do echo "== $$t"; $(BASH) "$$t"; done; \
for t in scripts/test-*.py; do echo "== $$t"; $(PYTHON) "$$t"; done