Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
411 lines (345 loc) · 15.5 KB
/
Copy pathMakefile
File metadata and controls
411 lines (345 loc) · 15.5 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
# ============================================================================
# MuscleBuilder — local dev Makefile
#
# Requires: direnv, uv, pnpm, docker (with `docker compose` subcommand).
# Run `make doctor` to verify your toolchain, then `make dev` to launch.
#
# Many targets reference apps/api and apps/web which are scaffolded as part
# of the build plan. `make help` and `make doctor` always work; other targets
# require the relevant apps + infra/docker-compose.yml to exist.
# ============================================================================
SHELL := /usr/bin/env bash
.SHELLFLAGS := -eu -o pipefail -c
.DEFAULT_GOAL := help
# --- paths -------------------------------------------------------------------
API_DIR := apps/api
WEB_DIR := apps/web
SHARED_DIR := packages/shared
COMPOSE_FILE := infra/docker-compose.yml
# --- tools (uv 0.11+: run from API_DIR; no --project flag) -------------------
COMPOSE := docker compose -f $(COMPOSE_FILE)
PNPM := pnpm
UV := uv
# All uv commands: cd into apps/api first in each recipe (do NOT embed cd in variables).
UV_RUN := $(UV) run
PYTHON := $(UV_RUN) python
UVICORN := $(UV_RUN) uvicorn
ALEMBIC := $(UV_RUN) alembic
ARQ := $(UV_RUN) arq
PYTEST := $(UV_RUN) pytest
RUFF := $(UV_RUN) ruff
MYPY := $(UV_RUN) mypy
# --- service URLs (printed in `make dev`) ------------------------------------
DEV_HOST := 127.0.0.1
API_URL := http://$(DEV_HOST):8000
WEB_URL := http://$(DEV_HOST):3000
MINIO_URL := http://localhost:9001
# --- anatomy assets ----------------------------------------------------------
ANATOMY_DIR := apps/web/public/anatomy
ANATOMY_RAW := $(ANATOMY_DIR)/anatomy.glb
ANATOMY_OUT := $(ANATOMY_DIR)/anatomy.draco.glb
# Default: Z-Anatomy derivative (CC-BY-SA 4.0) from hpfrei/body-anatomy-3d-viewer
ANATOMY_URL ?= https://raw.githubusercontent.com/hpfrei/body-anatomy-3d-viewer/main/public/body.glb
# ============================================================================
# Meta
# ============================================================================
.PHONY: help
help: ## Show this help
@awk 'BEGIN {FS = ":.*?## "; printf "\nMuscleBuilder — make targets\n\nUsage: make <target>\n\n"} \
/^[a-zA-Z0-9_.-]+:.*?## / {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2} \
/^# ==/ {printf "\n"}' $(MAKEFILE_LIST)
@printf "\n"
.PHONY: doctor
doctor: ## Verify required tools and config are in place
@ok=1; \
check() { \
if command -v $$1 >/dev/null 2>&1; then \
printf " [ok] %s\n" "$$1"; \
else \
printf " [missing] %s -- %s\n" "$$1" "$$2"; ok=0; \
fi; \
}; \
echo "Checking toolchain:"; \
check direnv "brew install direnv (and hook into your shell)"; \
check uv "curl -LsSf https://astral.sh/uv/install.sh | sh"; \
check pnpm "corepack enable && corepack prepare pnpm@latest --activate"; \
check docker "install Docker Desktop or colima"; \
if docker compose version >/dev/null 2>&1; then \
printf " [ok] docker compose\n"; \
else \
printf " [missing] docker compose -- update Docker Desktop / install compose v2\n"; ok=0; \
fi; \
echo ""; \
echo "Checking project files:"; \
[ -f .envrc.local ] && printf " [ok] .envrc.local\n" || printf " [missing] .envrc.local -- cp .envrc.local.example .envrc.local\n"; \
[ -f $(COMPOSE_FILE) ] && printf " [ok] %s\n" "$(COMPOSE_FILE)" || printf " [missing] %s -- run the build plan to scaffold infra\n" "$(COMPOSE_FILE)"; \
[ -d $(API_DIR) ] && printf " [ok] %s/\n" "$(API_DIR)" || printf " [missing] %s/ -- run the build plan to scaffold the API\n" "$(API_DIR)"; \
[ -d $(WEB_DIR) ] && printf " [ok] %s/\n" "$(WEB_DIR)" || printf " [missing] %s/ -- run the build plan to scaffold the web app\n" "$(WEB_DIR)"; \
[ -f $(ANATOMY_OUT) ] && printf " [ok] %s\n" "$(ANATOMY_OUT)" || printf " [missing] %s -- run 'make anatomy.ensure'\n" "$(ANATOMY_OUT)"; \
echo ""; \
if [ "$$ok" = "1" ]; then echo "Toolchain looks good."; else echo "Fix the items above, then re-run 'make doctor'."; exit 1; fi
# ============================================================================
# Bootstrap / install
# ============================================================================
.PHONY: install
install: install.api install.web ## Install all dependencies (api + web + shared)
.PHONY: install.api
install.api: ## uv sync the API project
@cd $(API_DIR) && $(UV) sync
.PHONY: install.web
install.web: ## pnpm install the workspace (web + shared)
@$(PNPM) install
# ============================================================================
# Infra services (postgres / redis / minio)
# ============================================================================
.PHONY: up
up: ## Start postgres + redis + minio in the background
@$(COMPOSE) up -d postgres redis minio
.PHONY: down
down: ## Stop all compose services
@$(COMPOSE) down
.PHONY: logs
logs: ## Tail logs from all compose services
@$(COMPOSE) logs -f --tail=100
.PHONY: ps
ps: ## Show compose service status
@$(COMPOSE) ps
# ============================================================================
# Database
# ============================================================================
.PHONY: db.up
db.up: ## Start postgres only
@$(COMPOSE) up -d postgres
.PHONY: db.migrate
db.migrate: ## Apply database migrations (alembic upgrade head)
@cd $(API_DIR) && $(ALEMBIC) upgrade head
.PHONY: db.revision
db.revision: ## Create a new alembic revision (usage: make db.revision M="add foo")
@if [ -z "$(M)" ]; then echo "usage: make db.revision M=\"message\""; exit 1; fi
@cd $(API_DIR) && $(ALEMBIC) revision --autogenerate -m "$(M)" || { echo "Alembic not initialized — run 'cd apps/api && uv run alembic init migrations' first"; exit 1; }
.PHONY: db.seed
db.seed: ## Idempotently load seed data (Technogym, exercises, programs, ...)
@cd $(API_DIR) && $(PYTHON) -m musclebuilder.seeds.load
.PHONY: db.reset
db.reset: ## Drop + recreate the database, migrate, seed (destructive)
@printf "About to DROP the dev database. Continue? [y/N] "; read ans; [ "$$ans" = "y" ] || exit 1
@$(COMPOSE) exec -T postgres psql -U musclebuilder -d postgres -c "DROP DATABASE IF EXISTS musclebuilder;"
@$(COMPOSE) exec -T postgres psql -U musclebuilder -d postgres -c "CREATE DATABASE musclebuilder;"
@$(MAKE) db.migrate
@$(MAKE) db.seed
# ============================================================================
# Run individual services on the host
# ============================================================================
.PHONY: api
api: ## Run the FastAPI dev server (auto-reload, port 8000)
@cd $(API_DIR) && $(UV) run uvicorn musclebuilder.main:app --host $(DEV_HOST) --port 8000 --reload
.PHONY: worker
worker: ## Run the arq background worker
@cd $(API_DIR) && $(UV) run arq musclebuilder.workers.WorkerSettings
.PHONY: web
web: ## Run the Next.js dev server (port 3000)
@$(PNPM) --filter @musclebuilder/web dev
# ============================================================================
# The big one: everything at once
# ============================================================================
.PHONY: stop
stop: ## Kill stale processes on dev ports 8000 and 3000
@echo "Clearing dev stack…"
@$(COMPOSE) stop web api 2>/dev/null || true
@pkill -f 'uvicorn musclebuilder.main:app.*--port 8000' 2>/dev/null || true
@pkill -f 'arq musclebuilder.workers.WorkerSettings' 2>/dev/null || true
@pkill -f '@musclebuilder/web exec next dev' 2>/dev/null || true
@pkill -f 'next dev -p 3000' 2>/dev/null || true
@pkill -f 'MuscleBuilder.*Stopping dev stack' 2>/dev/null || true
@for port in 8000 3000; do \
for pid in $$(lsof -nP -iTCP:$(DEV_HOST):$$port -sTCP:LISTEN -t 2>/dev/null); do \
kill "$$pid" 2>/dev/null || true; \
done; \
done
@sleep 0.5
@for port in 8000 3000; do \
pids=$$(lsof -nP -iTCP:$(DEV_HOST):$$port -sTCP:LISTEN -t 2>/dev/null); \
if [ -n "$$pids" ]; then \
kill -9 $$pids 2>/dev/null || true; \
echo " force-stopped $(DEV_HOST):$$port ($$pids)"; \
else \
echo " $(DEV_HOST):$$port free"; \
fi; \
done
.PHONY: wait-ports
wait-ports: ## Block until dev ports 8000 and 3000 are free on localhost
@for port in 8000 3000; do \
for _ in $$(seq 1 30); do \
lsof -nP -iTCP:$(DEV_HOST):$$port -sTCP:LISTEN -t >/dev/null 2>&1 || break; \
sleep 0.1; \
done; \
if lsof -nP -iTCP:$(DEV_HOST):$$port -sTCP:LISTEN -t >/dev/null 2>&1; then \
echo "ERROR: $(DEV_HOST):$$port still in use. Run 'make stop' and check:"; \
lsof -nP -iTCP:$(DEV_HOST):$$port -sTCP:LISTEN; \
exit 1; \
fi; \
done
.PHONY: dev
dev: up anatomy.ensure db.migrate stop wait-ports ## Bring up infra + run api + worker + web concurrently (Ctrl+C stops all)
@echo ""
@echo "==> MuscleBuilder dev stack starting"
@echo " api $(API_URL)"
@echo " web $(WEB_URL)"
@echo " minio $(MINIO_URL)"
@echo ""
@echo " Ctrl+C to stop everything."
@echo ""
@trap 'echo; echo "==> Stopping dev stack..."; $(MAKE) -s stop; exit 0' INT TERM EXIT; \
( cd $(API_DIR) && CORS_ORIGINS="$(WEB_URL),http://localhost:3000" $(UV) run uvicorn musclebuilder.main:app --host $(DEV_HOST) --port 8000 --reload 2>&1 | sed -e "s/^/[api] /" ) & \
( cd $(API_DIR) && $(UV) run arq musclebuilder.workers.WorkerSettings 2>&1 | sed -e "s/^/[worker] /" ) & \
( NEXT_PUBLIC_API_URL="$(API_URL)" $(PNPM) --filter @musclebuilder/web exec next dev -H $(DEV_HOST) -p 3000 2>&1 | sed -e "s/^/[web] /" ) & \
wait
# ============================================================================
# Full containerised stack (alternative to `make dev`)
# ============================================================================
.PHONY: stack.up
stack.up: ## Build + run the entire stack in containers (api + worker + web + infra)
@$(COMPOSE) up -d --build
.PHONY: stack.down
stack.down: ## Tear down the full containerised stack
@$(COMPOSE) down
.PHONY: stack.logs
stack.logs: ## Tail logs from the containerised stack
@$(COMPOSE) logs -f --tail=100 api worker web
# ============================================================================
# Code quality
# ============================================================================
.PHONY: lint
lint: lint.api lint.web ## Lint api + web
.PHONY: lint.api
lint.api:
@cd $(API_DIR) && $(RUFF) check .
.PHONY: lint.web
lint.web:
@$(PNPM) -r lint
.PHONY: fmt
fmt: fmt.api fmt.web ## Format api + web
.PHONY: fmt.api
fmt.api:
@cd $(API_DIR) && $(RUFF) format .
.PHONY: fmt.web
fmt.web:
@$(PNPM) -r format
.PHONY: typecheck
typecheck: typecheck.api typecheck.web ## Typecheck api + web
.PHONY: typecheck.api
typecheck.api:
@cd $(API_DIR) && $(MYPY) src
.PHONY: typecheck.web
typecheck.web:
@$(PNPM) -r typecheck
# ============================================================================
# Tests
# ============================================================================
.PHONY: test
test: test.api test.web ## Run unit tests (api + web)
.PHONY: test.api
test.api:
@cd $(API_DIR) && $(PYTEST)
.PHONY: test.web
test.web:
@$(PNPM) -r test
.PHONY: e2e
e2e: ## Run Playwright end-to-end tests
@$(PNPM) --filter @musclebuilder/web exec playwright test
# ============================================================================
# Build
# ============================================================================
.PHONY: build
build: build.api build.web ## Build api + web for production
.PHONY: build.api
build.api:
@cd $(API_DIR) && $(UV) build
.PHONY: build.web
build.web:
@$(PNPM) -r build
# ============================================================================
# Shared types / OpenAPI
# ============================================================================
.PHONY: openapi
openapi: ## Export openapi.json from FastAPI and regenerate TS types in packages/shared
@mkdir -p $(SHARED_DIR)
@cd $(API_DIR) && $(PYTHON) -m musclebuilder.scripts.export_openapi > $(CURDIR)/$(SHARED_DIR)/openapi.json
@$(PNPM) --filter @musclebuilder/shared run codegen
.PHONY: shared.verify
shared.verify: ## CI drift-check: hand-authored zod schemas vs generated OpenAPI TS types
@$(PNPM) --filter @musclebuilder/shared run verify
# ============================================================================
# Anatomy assets (BioDigital-style GLB)
# ============================================================================
.PHONY: anatomy.fetch
anatomy.fetch: ## Download anatomy.glb (default: Z-Anatomy body.glb; override with ANATOMY_URL=)
@mkdir -p $(ANATOMY_DIR)
@if [ -f "$(ANATOMY_RAW)" ]; then \
echo "Already exists: $(ANATOMY_RAW) (delete to re-fetch)"; \
elif [ -n "$(ANATOMY_URL)" ]; then \
echo "Fetching $(ANATOMY_URL) -> $(ANATOMY_RAW)"; \
curl -fsSL "$(ANATOMY_URL)" -o "$(ANATOMY_RAW)"; \
echo "Saved $(ANATOMY_RAW)"; \
else \
echo "No ANATOMY_URL set and $(ANATOMY_RAW) missing."; \
echo "Export from Z-Anatomy (see docs/anatomy-assets.md) or:"; \
echo " make anatomy.fetch ANATOMY_URL=https://example.com/anatomy.glb"; \
exit 1; \
fi
.PHONY: anatomy.optimize
anatomy.optimize: ## Optimize anatomy.glb -> anatomy.draco.glb (preserves per-muscle meshes)
@if [ ! -f "$(ANATOMY_RAW)" ]; then echo "Run make anatomy.fetch first"; exit 1; fi
@$(PNPM) --filter @musclebuilder/web exec gltf-transform optimize \
"$(CURDIR)/$(ANATOMY_RAW)" "$(CURDIR)/$(ANATOMY_OUT)" \
--join false --flatten false --instance false --compress draco
@echo "Optimized -> $(ANATOMY_OUT)"
.PHONY: anatomy.ensure
anatomy.ensure: ## Fetch + optimize anatomy GLB if missing (idempotent)
@bash infra/scripts/anatomy-ensure.sh
# ============================================================================
# Voice coach avatar (rigged GLB with viseme / ARKit blendshapes)
# ============================================================================
COACH_DIR := apps/web/public/coach
COACH_OUT := $(COACH_DIR)/coach.glb
# Provide a GLB that exposes Oculus viseme (`viseme_*`) or ARKit-52 blendshape
# morph targets so lip-sync can drive the mouth. Sources: a Ready Player Me
# avatar (https://readyplayer.me) exported with visemes, or a MetaHuman GLB
# export. The app falls back to a procedural reactive head when this is absent.
COACH_AVATAR_URL ?=
.PHONY: coach.fetch
coach.fetch: ## Download the voice-coach avatar GLB (set COACH_AVATAR_URL=)
@mkdir -p $(COACH_DIR)
@if [ -f "$(COACH_OUT)" ]; then \
echo "Already exists: $(COACH_OUT) (delete to re-fetch)"; \
elif [ -n "$(COACH_AVATAR_URL)" ]; then \
echo "Fetching $(COACH_AVATAR_URL) -> $(COACH_OUT)"; \
curl -fsSL "$(COACH_AVATAR_URL)" -o "$(COACH_OUT)"; \
echo "Saved $(COACH_OUT)"; \
else \
echo "No COACH_AVATAR_URL set and $(COACH_OUT) missing."; \
echo "The voice coach will use the procedural fallback head."; \
echo " make coach.fetch COACH_AVATAR_URL=https://models.readyplayer.me/<id>.glb?morphTargets=ARKit,Oculus%20Visemes"; \
fi
# ============================================================================
# Housekeeping
# ============================================================================
.PHONY: clean
clean: ## Remove caches, build artifacts, virtualenvs, node_modules
@rm -rf \
$(API_DIR)/.venv \
$(API_DIR)/.pytest_cache \
$(API_DIR)/.ruff_cache \
$(API_DIR)/.mypy_cache \
$(API_DIR)/dist \
$(WEB_DIR)/.next \
$(WEB_DIR)/node_modules \
node_modules \
packages/*/node_modules \
packages/*/dist
@find . -type d -name __pycache__ -prune -exec rm -rf {} + 2>/dev/null || true
@echo "Cleaned."
.PHONY: nuke
nuke: clean ## clean + remove compose volumes (destructive)
@$(COMPOSE) down -v 2>/dev/null || true
@echo "Nuked."