Imported from Olshansk/agent-skills (
skills/cmd-makefile/SKILL.md). Install upstream withnpx skills add Olshansk/agent-skills --skill cmd-makefile. Copyright stays with the author.
Makefile Helper
Create Makefiles that are simple, discoverable, and maintainable.
Core Principles
- Default to rich help - Use categorized help with emoji headers unless user requests minimal
- Default Chrome extensions to modular - Use the modular
makefiles/*.mklayout with shared colors/help for Chrome extension projects unless the repo is truly tiny - Ask about structure upfront - For new Makefiles, ask: "Flat or modular? Rich help or minimal?"
- Follow existing conventions - Match the project's style if Makefile already exists
- Don't over-engineer - Solve the immediate need, not hypothetical futures
- Use
uv run- Always run Python commands viauv runfor venv context - Explain decisions - If choosing flat/minimal, explain why before generating
When to Use This Skill
- Creating a new Makefile for a project
- Adding specific targets to an existing Makefile
- Improving/refactoring an existing Makefile
- Setting up CI/CD make targets
- Distributing pre-built binaries via GitHub Releases
Quick Start
For new projects, use the appropriate template:
| Project Type | Template | Complexity | Asks upfront |
|---|---|---|---|
| Any project | templates/base.mk |
Minimal | — |
| Python with uv | templates/python-uv.mk |
Standard | — |
| Python FastAPI | templates/python-fastapi.mk |
Full-featured | test split? prod target? HEALTH_PATH? |
| PostgreSQL + Alembic | templates/postgres.mk |
Standard | PG_PORT (5433 default)? soft vs HARD reset? |
| Node.js | templates/nodejs.mk |
Standard | — |
| Go | templates/go.mk |
Standard | — |
| Chrome Extension | templates/chrome-extension.mk |
Modular | — |
| Flutter App | templates/flutter.mk |
Modular | — |
| Electron App | templates/electron.mk |
Modular | — |
| Static Site (HTML/CSS/JS) | templates/static-site.mk |
Standard | DEPLOY_MODE (rsync/gh-pages/netlify/vercel/none)? |
For templates in the "Asks upfront" column, run the Phase 2 interactive questions in §"Interaction Pattern" before scaffolding. Companion files:
templates/python-fastapi-env/.template.env→ project's.template.envtemplates/python-fastapi-scripts/export_openapi_spec.py→scripts/export_openapi_spec.pytemplates/postgres-env/.template.env→ merge into project's.template.env(don't ship two)
Chrome Extension Structure
The chrome extension template uses a modular structure:
Makefile # Main file with help + includes
makefiles/
colors.mk # ANSI colors & print helpers
common.mk # Shell flags, VERBOSE mode, guards
build.mk # Build zip, version bump, releases
dev.mk # Lint, clean, install
test.mk # Unit tests, E2E tests, coverage
env.mk # Environment setup, dependency checks
Copy from templates/chrome-extension-modules/ to your project's makefiles/ directory.
Key features:
- Use
makefiles/colors.mkfor ANSI color output and header helpers. - Use
makefiles/common.mkfor shell flags, guard rails, and shared variables. - Use
makefiles/env.mkfor environment checks and dependency sanity. - Use
makefiles/build.mkfor build/package/release targets. - Use
makefiles/dev.mkfor install, watch, clean, and other local workflows. - Use
makefiles/test.mkfor typecheck, unit, and E2E targets when present. build-release- Version bump menu (major/minor/patch) + zip for Chrome Web Storebuild-beta- (Optional) GitHub releases withghCLItest-unit/test-e2e- Vitest + Playwright testingtest-unit-<module>/test-e2e-<module>- Per-module test targetsVERBOSE=1 make <target>- Show commands for debugging
Flutter App Structure
Makefile # Main file with help + includes
makefiles/
colors.mk # ANSI colors & print helpers
common.mk # Shell flags, VERBOSE mode, guards
dev.mk # Setup, run simulator/device, devices, clean
build.mk # iOS/Android builds (IPA, APK, AAB)
deploy.mk # TestFlight upload
lint.mk # Dart analyze & format
Copy from templates/flutter-modules/ to your project's makefiles/ directory.
Key features:
flutter-run-iosauto-boots simulator and waits for itflutter-run-androidauto-launches emulator and waits for itflutter-run-deviceauto-detects or usesFLUTTER_IOS_DEVICE/FLUTTER_ANDROID_DEVICEflutter-build-ipa+flutter-export-ipa+flutter-deploy-testflightfull iOS release workflowflutter-export-ipare-exports IPA from existing archive without rebuilding_check-asc-apppre-flight App Store Connect validation (with ASC_API_KEY/ASC_API_ISSUER)flutter-lint FIX=trueDart formatting with FIX patternVERBOSE=1 make <target>show commands for debugging
Electron App Structure
Makefile # Main file with help + includes
makefiles/
colors.mk # ANSI colors & print helpers
common.mk # Shell flags, VERBOSE mode, guards
dev.mk # Setup, dev server, debug, clean
build.mk # Pack-check, dist (mac/win/linux), publish
lint.mk # ESLint, Prettier, TypeScript, tests
Copy from templates/electron-modules/ to your project's makefiles/ directory.
Key features:
electron-devstarts dev mode with hot-reloadelectron-debuglaunches with DevTools openelectron-cleansingle target that removes artifacts, node_modules, and lock fileelectron-pack-checksmoke-tests that the app loads without errorselectron-dist-mac/electron-dist-win/electron-dist-linuxcross-platform buildselectron-dist-allbuilds for all platforms in one shotelectron-publishpublishes to GitHub Releases (requiresGH_TOKEN)electron-lint FIX=trueESLint + Prettier with auto-fix patternelectron-typecheckTypeScript type checkingVERBOSE=1 make <target>show commands for debugging
Static Site (HTML/CSS/JS)
Plain static sites — landing pages, marketing pages, docs — with no bundler or SSR. Uses npx --yes for tooling so contributors don't need a local package.json or node_modules.
Copy templates/static-site.mk to your project root as Makefile.
Targets use site-* and dev-* prefixes (per §"Naming Conventions"). The template is deliberately slim — lint/link-check/image-optimization targets were cut because they're rarely run locally on a marketing page and collapse under the "too many granular dev-* quality targets" pitfall. Add them back only if a specific project needs them.
Key features:
site-serve- local HTTP server viapython3 -m http.server(falls back tonpx serve). Override withmake site-serve PORT=9000 HOST=0.0.0.0.site-open- open$(ENTRY)(defaultindex.html) in the default browser (macOSopen/ Linuxxdg-open).site-status- print site dir, entry, detected HTML pages, and tooling availability.dev-format- prettier--writeacross HTML/CSS/JS vianpx --yes. No global install required, noFIX=truegate — always writes (formatting check-only is CI's job, not a local ergonomic).dev-asset-report- top 20 largest files (finds accidentally-committed hero images, uncompressed GIFs).dev-build- copies site into$(BUILD_DIR)(defaultdist/) via rsync with sensible excludes, then optionally minifies HTML/CSS/JS viahtml-minifier-terser(silently skipped if unavailable).dev-deploy- depends ondev-build; dispatches onDEPLOY_MODE(rsync|gh-pages|netlify|vercel|none). Fails fast with install hint if the selected tool is missing.dev-clean- removes$(BUILD_DIR)/.
Config knobs (?= — override on command line): SITE_DIR, PORT, HOST, ENTRY, BUILD_DIR, DEPLOY_MODE, RSYNC_DEST.
PostgreSQL + Alembic
Standalone template for database operations. Use alongside python-fastapi.mk for a full stack, or independently for any Python project with PostgreSQL.
Copy templates/postgres.mk to your project root (or include it from your main Makefile).
Key features:
db-start/db-stop/db-cleanvia plaindocker run(default) with health-check wait loop. Docker Compose variant is commented at the bottom of the template for multi-service setups.db-initcomposite target (start + migrate).db-resethas two flavors viaHARDflag:HARD=false(default): kill connections → DROP DATABASE → CREATE → migrate. Fast, preserves container+volume.HARD=true:docker rm -fcontainer +docker volume rm -f+ re-init. Use when container/volume itself is in a broken state.
db-migrate/db-revisionAlembic migrations viauv run alembic; all Alembic recipes inline-source.env(via_check-envguard) so a stale shellDATABASE_URLcan't override the configured value.db-migration-current/db-migration-history/db-migration-checkintrospection.db-shell(psql) /db-pgcli/db-pgwebshell access.db-pgclistrips the SQLAlchemy+psycopgdialect marker before handing the URL to pgcli (pgcli doesn't understand dialect markers).env-templatebootstrap target that copies.template.env→.envwithout overwriting.db-logs/db-seedutilities.- All config via
?=variables (PG_CONTAINER,PG_DB,PG_USER,PG_PASSWORD,PG_PORT=5433,PG_IMAGE). - Port 5433 by default to dodge host Homebrew Postgres on 5432. Override with
make db-start PG_PORT=5432if your machine is clean. - Driver: template targets
psycopg[binary]>=3(psycopg3). SQLAlchemy needs thepostgresql+psycopg://dialect marker; add a pydantic-settings validator that normalizespostgres:///postgresql://→postgresql+psycopg://so Render's managed DB URL works verbatim.
Interaction Pattern (Phased)
Run these phases top-to-bottom on any Makefile scaffolding / refactor request. Do Phase 2 (Interactive questions) BEFORE writing any file — the answers drive which template variants to emit.
Phase 1 — Discovery
- Is there already a Makefile? Read it first — match its conventions.
- What stack / language? (Python+uv, FastAPI+Postgres, Node, Go, …)
- What's the deployment target? (Render, Fly, Vercel, self-hosted, …) — affects
run-api-prod. - How big is the project today, and how big will it reasonably grow? (≥5 targets expected → modular.)
Phase 2 — Interactive questions (ask in ONE batch via AskUserQuestion)
Ask up front rather than iterating. Typical questions:
- Structure: flat single file or modular (
makefiles/*.mk)? - Help style: rich categorized help with emoji headers, or minimal?
- Postgres port (if Postgres used):
5433(default, dodges host Homebrew Postgres on 5432) or5432? - Test granularity: single
dev-test(small/medium projects) or splittest-unit/test-integration/test-e2e(larger projects)? - Prod runtime: need a
run-api-prodtarget against a remote DB (Render/Fly/etc.)? - OpenAPI spec export (FastAPI): always include
api-export-specunless user declines — enables client SDK generation and spec-diff in CI.
Skip questions whose answer is already implied by an existing Makefile or strong project signal.
Phase 3 — Scaffold
Emit (in this order):
Makefile+makefiles/*.mk(if modular)..template.envat repo root (committed)..envis NOT created — leave that tomake env-template. Add.envto.gitignoreif not already there..env.prod— ifrun-api-prodwas requested, confirm.env.prodis in.gitignore(it MUST be — production credentials).scripts/export_openapi_spec.py(if FastAPI + api-export-spec).
Phase 4 — Verify
make help— clean categorized output.make help-unclassified— should be empty or minimal.make -n run-api-local db-migrate api-export-spec— dry-run the critical paths.- Grep for any
_check-env/_check-postgresguards you added to confirm they fire when expected.
Naming Conventions
Use kebab-case with consistent prefix-based grouping:
# Good - consistent prefixes (hyphens, not underscores)
build-release, build-zip, build-clean # Build tasks
dev-run, dev-clean # Development tasks
db-start, db-stop, db-migrate # Database tasks
env-local, env-prod, env-show # Environment tasks
# Internal targets - prefix with underscore to hide from help
_build-zip-internal, _prompt-version # Not shown in make help
# Bad - inconsistent
run-dev, localEnv, test_net
build_release, dev_test # Underscores - don't use
Exception — universal unprefixed names. A handful of names are so de-facto standard across ecosystems (npm, cargo, go, make itself) that prefixing them with dev- adds noise without adding signal. Keep these unprefixed:
test(notdev-test)build(notdev-build) — only if the project has no competingbuild-*grouprun(notdev-run) — same caveatformat/lint— same caveat; if you havedev-formatalready, stay consistent within the project
Rule of thumb: if the unprefixed name would collide with a prefix group you already have (e.g., already have build-release, build-zip), keep the dev- prefix for consistency. Otherwise, drop it.
Name targets after the action, not the tool:
# Good - describes what it does
remove-bg # Removes background from image
format-code # Formats code
lint-check # Runs linting
# Bad - names the tool
rembg # What does this do?
prettier # Is this running prettier or configuring it?
eslint # Unclear
Key Patterns
Binary Distribution
For projects distributed as pre-built binaries via GitHub Releases:
GITHUB_REPO ?= owner/repo
OS := $(shell uname -s | tr '[:upper:]' '[:lower:]')
ARCH := $(shell uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/')
.PHONY: install-cli
install-cli: ## Download and install CLI from latest GitHub release
@RELEASE=$$(curl -fsSL https://api.github.com/repos/$(GITHUB_REPO)/releases/latest | grep tag_name | cut -d'"' -f4); \
echo "Installing $$RELEASE for $(OS)/$(ARCH)..."; \
curl -fsSL -o ~/.local/bin/cli \
"https://github.com/$(GITHUB_REPO)/releases/download/$$RELEASE/cli-$(OS)-$(ARCH)"; \
chmod +x ~/.local/bin/cli
Key considerations:
- Detect OS and architecture automatically
- Download from GitHub Releases (no Python/uv required)
- Install to
~/.local/bin(user-writable, in PATH) - Preserve existing config files during updates
Always Use uv run for Python
# Good - uses uv run with ruff (modern tooling)
dev-check:
uv run ruff check src/ tests/
uv run ruff format --check src/ tests/
uv run mypy src/
dev-format:
uv run ruff check --fix src/ tests/
uv run ruff format src/ tests/
# Bad - relies on manual venv activation
dev-format:
ruff format .
Use uv sync (not pip install)
For Python projects, treat pyproject.toml and uv.lock as the source of truth.
Do not add pip install or requirements.txt fallback guidance to uv-based templates.
env-install:
uv sync # Uses pyproject.toml + lock file
Categorized Help (for 5+ targets)
Help is generated, never hand-written. Declare a section with ##@, document
targets with ##, and the renderer does the rest — see Help System.
##@ 🚀 API
api-run: ## Start server on port `$(PORT)` (`uvicorn --reload`)
api-run-prod: ## Start without reload, binds `0.0.0.0` (⚠️ PROD)
Makefile ordering rule - help targets go LAST, just before catch-all:
- Configuration (
?=variables) HELP_*configuration (HELP_TITLE,HELP_ICON,HELP_VARS, …)- Imports (
include ./makefiles/*.mk) — include order IS help section order - Main targets (grouped by function, each group under a
##@header) help:andhelp-unclassified:targets (orinclude ./makefiles/help.mk)- Catch-all
%:rule (absolute last)
Preflight Checks
_check-docker:
@docker info >/dev/null 2>&1 || { echo "Docker not running"; exit 1; }
db-start: _check-docker # Runs check first
docker compose up -d
External Tool Dependencies
When a target requires an external tool (not a system service):
- Don't create public install targets (no
make install-foo) - Use internal check as dependency (prefix with
_, no##comment) - Show install command on failure - tell user what to run, don't do it for them
# Internal check - hidden from help (no ##)
_check-rembg:
@command -v rembg >/dev/null 2>&1 || { \
printf "$(RED)$(CROSS) rembg not installed$(RESET)\n"; \
printf "$(YELLOW)Run: uv tool install \"rembg[cli]\"$(RESET)\n"; \
exit 1; \
}
# Public target - uses check as dependency
.PHONY: remove-bg
remove-bg: _check-rembg ## Remove background from image
rembg i "$(IN)" "$(OUT)"
Key points:
- Name target after the action (
remove-bg), not the tool (rembg) - Check runs automatically - user just runs
make remove-bg - If tool missing, user sees exactly what command to run
Env File Loading
Primary recommendation: inline-source per recipe. This is the only pattern that overrides stale shell-exported vars, which is the pitfall you'll actually hit in practice.
# Inline-source: recipe's DATABASE_URL comes from .env, not the user's shell
db-upgrade:
@set -a && . ./.env && set +a && uv run python -m alembic upgrade head
run-api-local:
@set -a && . ./.env && set +a && uv run uvicorn app.main:app --reload
Per-target override (e.g., test env, prod env):
# Allow: E2E_ENV=.test.env make test-e2e
test-e2e:
@set -a && . "$${E2E_ENV:-.env}" && set +a && uv run pytest tests/e2e/
Secondary (simpler but weaker): top-of-Makefile load. Fine for projects where no one exports the same vars in their shell. Does not override an already-exported shell var — so don't use this for DB URLs or anything that commonly lives in shell profiles.
# At top of Makefile, after .DEFAULT_GOAL
-include .env
.EXPORT_ALL_VARIABLES:
⚠️ Shell-override footgun. If a user has
export DATABASE_URL=...in their.zshrc(or manually in the current shell), the-includeform silently loses: their shell env wins over.env. Alembic/uvicorn will hit the wrong DB with zero warning. Use the inline-source pattern for any recipe that depends on a specific.envvalue.
.env / .template.env Bootstrap
Always ship a .template.env. Never ship a .env.
.template.envis committed to git. It tracks the schema of env vars the project expects — every new env var in code gets a placeholder here in the same PR..envis gitignored. Each developer fills in real values locally.- Every recipe that sources
.envshould preflight-check its existence and print a friendly "runmake env-template" if missing. - Ship a
make env-templatetarget that copies.template.env→.envbut never overwrites an existing.env.
_check-env:
@if [ ! -f .env ]; then \
printf "$(RED)$(CROSS) .env not found$(RESET)\n"; \
printf "$(YELLOW)$(INFO) Run 'make env-template' or 'cp .template.env .env'$(RESET)\n"; \
exit 1; \
fi
env-template: ## Create .env from .template.env (safe: never overwrites)
@if [ -f .env ]; then \
printf "$(YELLOW)$(INFO) .env already exists — leaving it alone$(RESET)\n"; \
elif [ ! -f .template.env ]; then \
printf "$(RED)$(CROSS) .template.env not found$(RESET)\n"; exit 1; \
else \
cp .template.env .env; \
printf "$(GREEN)$(CHECK) Created .env from .template.env — fill in real values$(RESET)\n"; \
fi
run-api-local: _check-env
@set -a && . ./.env && set +a && uv run uvicorn app.main:app --reload
Why not just -include .env at the top of the Makefile? See §"Env File Loading" above — -include silently loses to already-exported shell vars. The .template.env + _check-env + inline-source pattern is robust against that footgun AND gives new contributors a one-command bootstrap.
OpenAPI Spec Export (FastAPI)
Ship a standard api-export-spec target whenever you scaffold a FastAPI project. Benefits:
- Enables spec-diff in CI (catch accidental breaking API changes in PRs).
- Unblocks typed client generation (
openapi-typescript,datamodel-code-generator, etc.). - Gives external consumers a stable URL-less artifact to pin against.
Pair it with templates/python-fastapi-scripts/export_openapi_spec.py:
api-export-spec: ## Export OpenAPI spec to openapi.json
uv run python scripts/export_openapi_spec.py
# scripts/export_openapi_spec.py
from app.main import app
import json
from pathlib import Path
(Path(__file__).parents[1] / "openapi.json").write_text(
json.dumps(app.openapi(), indent=2) + "\n"
)
Local vs Prod DB Runs
Apps often need to run the same server against two DBs: local Docker for development, remote prod for debugging/one-off migrations. Split into two explicit targets; never let one be the ambient default.
.PHONY: run-api-local run-api-prod
run-api-local: ## Run API against local DB (loads .env, --reload)
@set -a && . ./.env && set +a && uv run uvicorn app.main:app --reload
run-api-prod: ## Run API against REMOTE prod DB (loads .env.prod)
@if [ ! -f .env.prod ]; then \
printf "$(RED)$(CROSS) .env.prod not found$(RESET)\n"; \
printf "$(YELLOW)Create it locally with the prod DATABASE_URL (gitignored)$(RESET)\n"; \
exit 1; \
fi
@printf "$(RED)$(BOLD)$(WARN) LOCAL APP -> REMOTE PRODUCTION DB$(RESET)\n"
@printf "$(YELLOW)Writes hit prod. Ctrl-C within 3s to abort.$(RESET)\n"
@sleep 3
@set -a && . ./.env.prod && set +a && uv run uvicorn app.main:app
Rules:
.env.prodMUST be gitignored (production credentials). Add it to.gitignorebefore creating the file.- Prod target: no
--reload(code changes auto-reloading against prod is a footgun), visible red warning, 3-second sleep so it isn't silent when fired by reflex. - Preflight: fail fast if
.env.prodis missing rather than silently falling back to.env. - Same pattern works for
run-worker-local/run-worker-prod,db-shell-prod(connect local psql to remote), etc.
FIX Variable for Check/Format Targets
Use a FIX variable to toggle between check-only and auto-fix modes:
FIX ?= false
dev-check: ## Run linting and type checks (FIX=false: check only)
$(call print_section,Running checks)
ifeq ($(FIX),true)
uv run ruff check --fix src/ tests/
uv run ruff format src/ tests/
else
uv run ruff check src/ tests/
uv run ruff format --check src/ tests/
endif
uv run mypy src/
$(call print_success,All checks passed)
In help output, show usage:
@printf "$(CYAN)%-25s$(RESET) %s\n" "dev-check" "Run linting (FIX=false: check only)"
@printf "%-25s $(GREEN)make dev-check FIX=true$(RESET) <- auto-fix issues\n" ""
Per-Module Test Targets
For projects with multiple modules or platform adapters, create per-module test targets using tool-specific filtering:
# Unit tests - filter by test file
.PHONY: test-unit-auth
test-unit-auth: ## Run auth module unit tests
$(call print_section,Running auth unit tests)
$(Q)$(NPM) exec vitest -- run tests/auth.test.js
# E2E tests - filter by grep pattern
.PHONY: test-e2e-checkout
test-e2e-checkout: ## Run checkout E2E tests
$(call print_section,Running checkout E2E tests)
$(Q)$(NPM) exec playwright -- test --grep "checkout"
Key points:
- Use
$(NPM) exec(not barenpx) for consistency with the$(NPM)variable - Unit tests filter by file path, E2E tests filter by
--greppattern - Keep the generic
test-unitandtest-e2etargets for running everything - Put per-module targets in
test.mk, notdev.mk
When to Modularize
Default to modular for any new Makefile with 5+ targets.
Use flat file only when:
- Simple scripts or single-purpose tools
- User explicitly requests it
- < 5 targets with no expected growth
Standard modular structure:
Makefile # Config, imports, help, catch-all
makefiles/
colors.mk # ANSI colors & print helpers
common.mk # Shell flags, VERBOSE, guards
<domain>.mk # Actual targets (build.mk, dev.mk, etc.)
Legacy Compatibility
Default: NO legacy aliases. Only add when:
- User explicitly requests backwards compatibility
- Existing CI/scripts depend on old names (verify with
rg "make old-name")
When legacy IS needed, put them in a clearly marked section AFTER main targets but BEFORE help:
############################
### Legacy Target Aliases ##
############################
.PHONY: old-name
old-name: new_name ## (Legacy) Description
Key Rules
- Always read existing Makefile before changes
- Search codebase before renaming targets (
rg "make old-target") - Test with
make helpandmake -n target - Update docs after Makefile changes - When adding new targets:
- Add to
make helpoutput (in the appropriate section) - Update
CLAUDE.mdif the project has one (document new targets) - Update any other relevant docs (README.md, Agents.md, etc.)
- Add to
- Never add targets without clear purpose
- No line-specific references - Avoid patterns like "Makefile:44" in docs/comments; use target names instead
- Single source of truth - Config vars defined once in root Makefile, not duplicated in modules
- Root Makefile = help + imports + catch-all only - Recipe bodies live in
makefiles/*.mk. When a recipe leaks into the root file, other contributors copy that pattern and the modular structure drifts back to flat. Ifsetup/status/whatever lives in root, move it to the most relevant module (e.g.,env.mk). - Help coverage audit - All targets with
##must appear in eithermake helpormake help-unclassified
Help System
Help is GENERATED from the makefiles, never hand-maintained. A hand-written
help block authors every target twice — once as a rule, once as a printf — and
the two drift apart the first time someone is in a hurry. Copy
modules/help.mk (or the inline block in templates/base.mk) and never write a
printf per target again.
Two kinds of comment drive the entire output:
##@ 🐘 Database & Migrations # declares a section (emoji lives HERE, only here)
db-migrate: ## Apply migrations (alembic upgrade head)
db-reset: ## Destroy volume, recreate, migrate (⚠️ DESTRUCTIVE)
Renders as:
═══ 🐘 Database & Migrations ═══
db-migrate Apply migrations (alembic upgrade head)
db-reset Destroy volume, recreate, migrate (⚠️ DESTRUCTIVE)
The four rules
-
Emoji on section headers, NEVER on target lines. Per-target emoji have inconsistent display widths —
⬆️ ♻️ 🖥️ ☁️(variation-selector emoji) render 1 cell,🐘 🚀 📦render 2 — so the description column jitters line to line and the list stops being scannable. One emoji per section can't misalign anything below it. This is the single highest-impact readability rule here. -
Sections render in file order, files in include order. The include list in the root Makefile IS the help ordering — no alphabetical sort, so
quickstartcan actually be first. Includehelp.mkLAST so ❓ Help renders last. -
Every
.mkdeclares its own##@before its targets (the section resets at each file boundary).make help-unclassifiedcatches the ones that forgot. -
Three color tiers, standard 16-color ANSI (theme-adaptive — the same file must read correctly on light and dark terminals; never pin 256-color values):
Tier Color Example Section header $(BOLD)$(BLUE)═══ 🐘 Database & Migrations ═══Target name $(CYAN)db-migrateDescription prose default fg Apply migrations,(needs db + redis)Inline literal $(GREEN)`alembic upgrade head`,`openapi.json`Secondary detail $(DIM)# uv sync --all-extrasInline danger $(YELLOW)(⚠️ DESTRUCTIVE)Headers and target names must never share a hue. Two hierarchy levels in one apparent color flattens the list into an unscannable wall — this is the most common failure mode in real Makefiles. Avoid
$(BOLD)$(MAGENTA)(reads purple, clashes with most themes); bold-with-no-color gets lost entirely.
Banner
ASCII box title with a project-branded emoji on the right. The box anchors the top of make help; the right-side emoji gives the project a glanceable identity (leaf/herb for Grove, rocket for an SDK, lock for a security tool, etc.). Keep the emoji on the right — left-side placement crowds the title text.
The renderer computes the box from HELP_TITLE / HELP_ICON / HELP_WIDTH, so
it stays square when the project is renamed:
HELP_TITLE ?= Grove App — Make Targets # text only, no emoji (see below)
HELP_ICON ?= 🌿
HELP_TAGLINE ?= Every command runs via uv.
HELP_WIDTH ?= 46
⚠️ Emoji width gotcha. Most emojis render as 2 terminal columns but count as 1 char, so padding computed from string length comes out short. The renderer budgets exactly 2 cells for
HELP_ICON— keep emoji OUT ofHELP_TITLEor the right║will not line up.
Expanding make vars in descriptions
awk reads raw file text, so make never expands $(PORT) inside a ## comment
— help would print the literal $(PORT). List such vars in HELP_VARS and they
are substituted at render time:
HELP_VARS ?= PORT HEALTH_PATH
api-health: ## Check API health endpoint ($(HEALTH_PATH)) # renders: (/health)
NO_COLOR
Guard the palette with ifdef NO_COLOR so make help > FILE doesn't embed raw
escapes. Do not try to auto-detect a TTY: $(shell test -t 1) always reports
false (subshell stdout is a pipe), and MAKE_TERMOUT needs GNU make ≥ 4, which
macOS doesn't ship.
Emoji vocabulary for help sections (pick from this list; reuse the same emoji for the same concept across projects so the visual language transfers):
| Section concept | Emoji | Notes |
|---|---|---|
| Quick Start / Getting started | 🚀 | Primary entry point for new contributors |
| Run / dev server / start service | 🏃 | Short-running ergonomic entry points |
| Build / compile / package | 🏗️ | dev-build, artifact creation |
| Development / lint / format / typecheck | 🛠️ | Quality gate targets |
| Tests | 🧪 | test, test-e2e, coverage |
| Database | 🗄️ | db-start, db-migrate, db-reset |
| Environment / config | 🌐 | env-setup, env-status, env-show |
| Secrets / auth / keys | 🔑 | env-pull-*, credential management |
| Deploy / release | 🛫 | deploy, release, publish |
| Cleanup / reset | 🧹 | clean-* family |
| Help / reference | ❓ | help, help-unclassified |
Emoji here go on the ##@ header only — a variation-selector emoji (🛠️, 🗄️, 🏗️) may consume an extra column, which is harmless in a header and would wreck a target line.
Quick Start is a 2-step instruction list, not a target list. If the real entry point is a short sequence (make env-setup && make run-prod), make quickstart a target that prints numbered instructions — do NOT list the same targets under both Quick Start and their "real" section (Environment Utilities, Run, etc.). Duplication doubles the help height and dilutes signal.
# Good - one quickstart target printing an ordered sequence
##@ 🚀 Quick Start
quickstart: ## Print ordered first-run steps
@printf " $(GREEN)1.$(RESET) make env-install $(DIM)# uv sync --all-extras$(RESET)\n"
@printf " $(GREEN)2.$(RESET) make run-local\n"
# Bad - the same targets listed under Quick Start AND their real section
Key help patterns:
help- Main categorized help, generated from##@+##help-unclassified- Documented targets with no##@section above them (audit)- Hidden targets: prefix with
_and give them NO##comment (e.g.,_check-docker) - Legacy targets: label with
## (Legacy)and park them under a##@ 🗄️ Legacysection
Always give help and help-unclassified their own section — put ##@ ❓ Help
directly above them, otherwise the two most basic targets are missing from help
and clutter help-unclassified instead. modules/help.mk already does this.
help-unclassified needs no exclusion list. The old prefix-regex approach
(grep -v -E '^(env-|dev-|clean|help)') fell out of sync every time a prefix was
added. The generated version asks a structural question instead — "is there a
##@ above this target?" — which stays correct forever:
help-unclassified: ## List documented targets with no ##@ section above them
@awk 'FNR == 1 { section = "" } \
/^##@ / { section = substr($$0, 5); next } \
/^[a-zA-Z0-9_-]+:.*## / && section == "" { print " " $$0 }' $(MAKEFILE_LIST)
Description format — one plain-text line, in the ## comment:
# Good - one line, says what it affects, args shown inline
scrape: ## Fetch posts into SQLite (make scrape SUBREDDITS=python LIMIT=10)
dev-check: ## Lint + type-check — add FIX=true to auto-fix
clean-build: ## Remove the .next build directory
run-local: ## Run API against local DB (localhost:$(PORT))
# Bad - a paragraph in a help line
setup: ## Install Python dependencies using uv. Run this once after cloning. Creates .venv/ and installs from pyproject.toml.
Help description rules:
- One line max — it shares a row with the target name; anything longer wraps and breaks the column.
- Include what it affects — "creates .venv", "exports to CSV", "destroys the volume".
- No raw ANSI —
$(YELLOW)inside a##comment prints as the literal string$(YELLOW). Use backticks (below) for emphasis; only names listed inHELP_VARSare expanded. - Fold the example into the line —
(make foo ARG=val)or— add FIX=truerather than a second printf line. The generated renderer emits one row per target by design. - Skip examples for simple targets — if there are no parameters, no example is needed.
Wrap literals in backticks. The renderer colors the contents green and strips the delimiters, so the tokens a reader actually reaches for — commands, files, paths, tool names, target names, env vars — pop out of the prose:
env-install: ## Install all deps incl extras (`uv sync --all-extras`)
api-export-spec: ## Export OpenAPI spec to `openapi.json`
db-shell: ## Open `psql` in the Postgres container
test: ## Run unit tests (alias of `test-unit`)
Renders as Install all deps incl extras (uv sync --all-extras) with the
backticked span in green. Leave prose parentheticals plain — (needs db + redis),
(prod-like, guarded). Marking up everything is the same as marking up nothing.
Note this is markup, not detection: a "color whatever is in parens" rule would
miss openapi.json (not in parens) and wrongly color (needs db + redis).
Danger annotations are the other styled element. A trailing (⚠️ …) renders in
yellow. Use it for targets that destroy data or touch production, and nothing
else — three per project keeps the marker meaningful, thirty makes it wallpaper:
db-reset: ## Destroy volume, recreate, and migrate (⚠️ DESTRUCTIVE)
run-api-prod: ## Run API against REMOTE prod DB, no reload (⚠️ PROD)
clean-all: clean ## Clean caches and remove .venv (⚠️ DESTRUCTIVE)
URL-in-parens formula for run-* targets. When a run target has a canonical destination (localhost port, API URL), append it in parens at the end of the description — denser than a separate info line, and it matches how contributors actually scan help. Use HELP_VARS so the port is the real one:
HELP_VARS ?= PORT
run-local: ## Local API + testnet chains (localhost:$(PORT))
run-testnet: ## Testnet API + testnet chains (api.testnet.grove.city)
run-mainnet: ## Production API + mainnet chains (api.grove.city) (⚠️ PROD)
Catch-all for unknown targets
Suggest the closest documented targets instead of dumping the whole help page — a typo is usually one character off, and reprinting 40 targets buries the fix:
%:
@printf "$(RED)$(CROSS) Unknown target '$@'$(RESET)\n"
@targets=$$(awk -F: '/^[a-zA-Z0-9_-]+:.*## /{print $$1}' $(MAKEFILE_LIST) | sort -u); \
near=$$(printf '%s\n' "$$targets" | grep -i -- "$$(printf '%s' '$@' | cut -c1-4)" | head -5 || true); \
if [ -n "$$near" ]; then \
printf "$(DIM)Did you mean:$(RESET)\n"; \
printf " $(CYAN)%s$(RESET)\n" $$near; \
fi; \
printf "$(DIM)Run '$(RESET)$(CYAN)make help$(RESET)$(DIM)' for all targets.$(RESET)\n"; \
exit 1
Three rules, each learned from a real failure:
| # | Rule | Failure it prevents |
|---|---|---|
| 1 | Keep it last in the file | A match-anything rule shadows every pattern rule defined after it |
| 2 | Give every -include <file> an empty <file>: ; rule |
-include .env makes .env a goal, the catch-all claims it, and a fresh clone greets you with ✗ Unknown target '.env' on every run |
| 3 | Never let an ignore-regex match target-shaped names | An ignore-list ending in [a-z]+([-][a-z]+)* swallows every typo — make dev-tes exits 0 and CI goes green on a target that does not exist |
Rule 2 in practice, at the top of the file:
-include .env
# Optionally-included files are NOT build targets. An explicit rule beats a
# match-anything rule, so this silences the catch-all for good.
.env .env.prod .template.env: ;
An ignore-list is only for bare arguments passed as extra goals (make send-tx 0xabc…,
a URL, a number, a TICKER) — never for anything that could be a mistyped target.
TODO Tracking
TODO.md is a generated view over the code, never a hand-maintained backlog.
Include modules/todo.mk and copy modules/todo-scripts/gen_todo.py to
scripts/gen_todo.py. Rationale:
Move Fast & Document Things.
- A hand-written list is "the one document to rule them all" — it goes stale the moment someone fixes something without updating it.
- The TODO lives next to the code it concerns; the index is regenerated.
- Filing a TODO is cheaper than filing a ticket. That is the point.
make todo-checkfails whenTODO.mdis stale — wire it into CI.make todo-listneeds no Python and works in any language.
# TODO_TECHDEBT: drop these casts if redis-py restores a typed client.
# Blocked on upstream: redis-py 8 removed Generic[_StrType], so `Redis[str]`
# raises at runtime. Re-check on the next major bump.
| # | Severity | Prefix | Act when |
|---|---|---|---|
| 1 | 🔴 | FIXME |
Now — it is broken today |
| 2 | 🔴 | TODO_IN_THIS_PR |
Before merge |
| 3 | 🟠 | HACK |
Before it bites someone |
| 4 | 🟠 | TODO_REMOVE_LATER |
When its stated exit condition is met |
| 5 | 🟡 | TODO_TECHDEBT / TODO_BETA / TODO_PROD / TODO_OPTIMIZE |
Next cleanup, or at the named gate |
| 6 | 🟢 | TODO_IMPROVE / TODO_CONSIDERATION / TODO_FUTURE / TODO_IDEA / TODO |
Opportunistically |
- Annotate with
TODO_PROD(#123, @olshansk):to record an issue and/or owner. - Always say what to do and why it is deferred — a bare
TODO: fix thishelps nobody. NOTE:is not tracked; it marks an explanation, not work.
Runtime Output for Long-Running Targets
Help output is one concern; runtime output from run-* / dev-* / db-* targets that activate config and then hand off to a long-running subprocess (next dev, uvicorn, docker compose up) is a second concern. Without structure, the output from "env activation → warning → config summary → subprocess banner → subprocess logs" interleaves into one undifferentiated wall of ℹ️ / ✓ / ⚠️ lines, and the developer has to read everything to find what matters. The patterns below break that into visually distinct phases.
Phase Banners
Bracket each logical phase with a horizontal rule + emoji + title + horizontal rule. Add two small reusable macros to colors.mk:
# Horizontal rule separator
define print_hr
@printf "$(DIM)━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━$(RESET)\n"
endef
# Phase banner: horizontal rule + emoji + title + horizontal rule
# Usage: $(call print_phase,🔑,ENV → LOCAL + MAINNET)
define print_phase
@printf "\n$(DIM)━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━$(RESET)\n"
@printf "$(BOLD)$(CYAN) $(1) $(2)$(RESET)\n"
@printf "$(DIM)━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━$(RESET)\n\n"
endef
Suggested phase-emoji vocabulary (use consistently across a project so the reader learns the shorthand):
| Phase | Emoji | Use when |
|---|---|---|
| Env / setup / secrets | 🔑 | Activating .env, loading secrets, switching config |
| Config / URLs / network | 🌐 | Showing resolved config, API endpoints, RPC URLs |
| App / server / service starting | 🚀 | Right before handing off to next dev / uvicorn / etc. |
| Database | 🗄️ | db-start, db-migrate, db-reset |
| Build | 📦 | build, dev-build, packaging |
| Tests | 🧪 | Before pytest / vitest / playwright output |
| Deploy | 🛫 | Before render deploy / fly deploy / vercel |
Critical-Action Warnings (Triple-Emoji Pattern)
For irreversible actions — real-money mainnet runs, production DB writes, force pushes, anything with "you probably can't undo this" — use a standalone triple-emoji line that sits between phases, not inside one. The triple flanking is visually louder than any single-line warning and the isolation ensures it isn't scanned past as ambient info.
# In colors.mk
define print_mainnet_warning
@printf "\n$(RED)$(BOLD)⚠️⚠️⚠️ MAINNET — REAL MONEY. DOUBLE-CHECK BEFORE TX. ⚠️⚠️⚠️$(RESET)\n"
endef
define print_prod_db_warning
@printf "\n$(RED)$(BOLD)⚠️⚠️⚠️ WRITES HIT PRODUCTION DATABASE — CTRL-C IN 3s TO ABORT ⚠️⚠️⚠️$(RESET)\n"
endef
Rules:
- Reserve the triple-⚠️ pattern for genuinely irreversible / costly actions. If you use it on every soft warning, it loses all signal.
- Single ⚠️ (or 🟡) for soft warnings ("secrets file missing, OAuth won't work"); triple ⚠️⚠️⚠️ for hard ones ("real money", "prod DB", "about to overwrite remote").
- Always render in
$(RED)$(BOLD)and on its own line with blank lines around it — a boxed or inlined version loses punch.
"Subprocess Logs Below" Divider
Right before handing off to a long-running subprocess (npm run dev, uvicorn, docker compose up), print a muted divider line that names whose logs are about to appear. Tells the user the Makefile's own output has ended, and anything below is coming from a child process with its own formatting conventions.
.PHONY: dev-run
dev-run:
$(call print_phase,🚀,APP)
# ... URLs, Ctrl+C hint, etc ...
@printf "\n$(DIM)─────────── Next.js logs below ───────────$(RESET)\n\n"
$(Q)npm run dev
Name the subprocess explicitly (Next.js logs, uvicorn logs, Postgres logs) — a generic "logs below" is less useful because the reader still has to guess whose formatting conventions to expect.
Actionable-Control Hint Before Hand-Off
Print one line right before the subprocess divider that tells the user what their keyboard controls are and what to expect. Compact, one line, bold the key combo:
@printf "\n$(CYAN)$(INFO) Auto-reload enabled · Press $(BOLD)Ctrl+C$(RESET)$(CYAN) to stop$(RESET)\n"
Bad alternative: a multi-line "Server running. Press Ctrl+C to stop. Changes auto-reload." block — same information, 3× the vertical space, no denser.
Parsed Key-Value Grid Over Raw grep Dumps
For env-show / status / db-info / any target whose job is to show "the current state of things," parse the underlying file and print a compact key-value grid rather than dumping raw KEY=value lines from grep. The parsed version is scannable; the raw dump is a wall of NEXT_PUBLIC_FOO=bar prefixes that the eye has to filter.
# Bad - raw grep dump, 3 subsections, 8 lines
env-show:
@printf "\n$(BOLD)Configuration:$(RESET)\n"
@grep "^NEXT_PUBLIC_ENV=" .env.local | sed 's/^/ /'
@grep "^NEXT_PUBLIC_CHAIN_ENV=" .env.local | sed 's/^/ /'
@printf "\n$(BOLD)API Endpoints:$(RESET)\n"
@grep "NEXT_PUBLIC_.*_URL=" .env.local | sed 's/^/ /'
@printf "\n$(BOLD)Services:$(RESET)\n"
@grep "^NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=" .env.local | sed 's/^/ /'
# Good - parsed grid under a phase banner, 4 lines, aligned columns
env-show:
$(call print_phase,🌐,CONFIG)
@ENV=$$(grep "^NEXT_PUBLIC_ENV=" .env.local | cut -d= -f2); \
GROVE=$$(grep "^NEXT_PUBLIC_GROVE_API_BASE_URL=" .env.local | cut -d= -f2); \
BASE=$$(grep "^NEXT_PUBLIC_BASE_RPC_URL=" .env.local | cut -d= -f2 | sed 's|https://||'); \
SOL=$$(grep "^NEXT_PUBLIC_SOLANA_RPC_URL=" .env.local | cut -d= -f2 | sed 's|https://||'); \
printf " $(BOLD)%-8s$(RESET) $(YELLOW)%s$(RESET)\n" "Env" "$$ENV"; \
printf " $(BOLD)%-8s$(RESET) $(YELLOW)%s$(RESET)\n" "API" "$$GROVE"; \
printf " $(BOLD)%-8s$(RESET) $(YELLOW)%s$(RESET) · $(YELLOW)%s$(RESET)\n" "RPC" "$$BASE" "$$SOL"
Rules:
- Fixed-width label column (
%-8s/%-10s) so values align vertically. - Values colored
$(YELLOW)(same convention as help-description paths/URLs). - Strip noise (e.g.
https://prefixes on RPC URLs) when the protocol doesn't add information. - Mask secrets (
WALLETCONNECT_SECRET=***hidden***). - If a value is missing or placeholder, print
$(RED)✗ not configured$(RESET)— don't silently omit the row.
Full-Flow Example
Applying all five patterns to a run-mainnet target produces:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔑 ENV → MAINNET
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✓ envs/mainnet.env + envs/.mainnet.secrets → .env.local
⚠️⚠️⚠️ MAINNET — REAL MONEY. DOUBLE-CHECK BEFORE TX. ⚠️⚠️⚠️
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🌐 CONFIG
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Env MAINNET (Base Mainnet · Solana Mainnet)
API https://api.grove.city
RPC mainnet.base.org · api.mainnet-beta.solana.com
Wallet WalletConnect a06ebd2a…
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🚀 APP
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🌐 URLs:
http://localhost:3000 (local)
http://192.168.1.15:3000 (LAN / mobile — cross-network? make run-ngrok)
ℹ️ Auto-reload enabled · Press Ctrl+C to stop
─────────── Next.js logs below ───────────
▲ Next.js 16.2.1 (Turbopack) · Ready in 400ms
Common Pitfalls
| Issue | Fix |
|---|---|
$var in shell loops |
Use $$var to escape for make |
Catch-all %: shows error |
Redirect to @$(MAKE) help instead |
| Config vars scattered | Put all ?= overridable defaults at TOP of root Makefile |
HELP_PATTERNS mismatch |
Must match grep patterns in help target exactly |
| Duplicate defs in modules | Define once in root, reference in modules |
| Trailing whitespace in vars | Causes path splitting bugs - trim all variable definitions |
.PHONY on file targets |
Only use .PHONY for non-file targets |
| Too many public targets | Don't expose install-X or check-X - use internal _check-X dependencies |
$(DIM) for usage text |
Appears grey/unreadable - use $(GREEN) instead |
Color codes inside %s |
ANSI codes in %s args print as literals - put colors in format string |
| Section header same color as title/targets | Use a distinct color (default $(BOLD)$(BLUE) — avoid $(MAGENTA)/purple, clashes with most terminal themes) + \n\n after the header. $(BOLD) alone renders as terminal-default and gets lost. |
| Section headers have no emoji | Every section gets a leading emoji (🚀 Quick Start, 🏃 Run, 🛠️ Development, 🌐 Environment, 🧹 Cleanup, 🧪 Tests). Emojis give the reader a glanceable landmark so they can skip to the section they want without parsing words. See "Emoji vocabulary" table in the Help System section. |
| Title box has no project emoji | The make help title box should carry a project-branded emoji on the right (🌿 for Grove, 🚀 for SDKs, 🔒 for security tools, etc.). Right-side placement; left-side crowds the title. |
| Runtime output is a wall of ℹ️/⚠️/✓ with no structure | Wrap each logical phase (env activation, config summary, subprocess hand-off) in a print_phase banner — see §"Runtime Output for Long-Running Targets". |
env-show / status dumps raw KEY=value grep output |
Parse the values in shell and print a compact aligned key-value grid (Env / API / RPC / Wallet) — scannable instead of a wall of NEXT_PUBLIC_FOO=bar. |
| ⚠️ used on every warning, so no warning stands out | Reserve triple-⚠️⚠️⚠️ $(RED)$(BOLD) for irreversible / costly actions (prod writes, real money, force push). Single ⚠️ for soft warnings. |
| Target named after tool | Name after the action: remove-bg not rembg |
Too many granular dev-* quality targets |
Collapse dev-lint + dev-typecheck + dev-format + dev-check into one dev-format (runs all three — prettier+eslint+tsc usually <5s for Node projects). Split only if CI parallelizes them. Same for dev-test + dev-test-e2e → one test. |
run-*-all / "Full Stack" Cartesian section |
Projects with a docs/ sibling grow run-testnet-all / run-mainnet-all / run-local-all / run-local-mainnet-all that just background the docs site. These are almost never used — users open a second terminal. Keep one run-docs target in the "Run" section and drop the Cartesian matrix. |
| Quick Start repeats targets | If Quick Start lists setup and status, and Environment Utilities lists setup and status again, you're doubling help height. Make Quick Start a numbered instruction list (1. make env-setup, 2. make run-prod) and let targets live once in their real section. |
help-unclassified shows filename |
Use sed 's/^[^:]*://' to strip Makefile: prefix |
No .env export |
Inline-source in the recipe: @set -a && . ./.env && set +a && $(CMD) (or -include .env for weaker cases — see Env File Loading) |
Stale shell DATABASE_URL silently overrides .env |
Use inline set -a && . ./.env && set +a in any recipe that depends on a specific .env value. -include alone loses to already-exported shell vars. |
| Secret committed to git | Add gitignored file (e.g. .env.prod), verify with git check-ignore, grep staged diff for a secret fragment before git add: git diff --cached | grep -c "$FRAGMENT" |
Single-service docker-compose.yml |
For one Postgres container, a plain docker run in db-start is lighter than a compose file. Compose pays off only when you have 2+ services. |
| Dockerized Postgres on port 5432 clashes with host Homebrew Postgres | Default dev container to PG_PORT ?= 5433 (and update DATABASE_URL accordingly). 5432 is nearly always claimed on macOS dev machines. |
pgcli rejects postgresql+psycopg://... URL |
pgcli doesn't understand SQLAlchemy dialect markers. Strip before use: PGCLI_URL=$$(echo "$$DATABASE_URL" | sed 's/+psycopg//') && pgcli "$$PGCLI_URL". |
FastAPI api-export-spec hardcodes model import |
The export script imports app.main:app. Parameterize via the APP_MODULE make variable if your entrypoint differs. |
Cleanup Makefile Workflow
When user says "cleanup my makefiles":
IMPORTANT: Build a plan first and explain it to the user before implementing anything.
Phase 1: Audit (no changes yet)
make help # See categorized targets
make help-unclassified # Find orphaned targets
cat Makefile # Read structure
ls makefiles/*.mk 2>/dev/null # Check if modular
rg "make " --type md # Find external dependencies
grep -E '\s+$' Makefile makefiles/*.mk # Trailing whitespace
Phase 2: Build & Present Plan
Create a checklist of proposed changes:
- Structure - Convert flat → modular (if 5+ targets) or vice versa
- Legacy removal - List specific targets to delete (with dependency check)
- Duplicates - List targets to consolidate
- Renames - List
old_name→new-namechanges - Description rewrites - List vague descriptions to improve
- Missing targets - Suggest targets that should exist (e.g.,
help-unclassified) - Ordering fixes - Config → imports → targets → help → catch-all
Ask user to approve the plan before proceeding.
Phase 3: Implement (after approval)
- Restructure (if needed) - Create
makefiles/directory, split into modules - Remove legacy - Delete approved targets
- Consolidate duplicates - Merge into single targets
- Rename targets - Apply hyphen convention, add
_prefix for internal - Rewrite descriptions - Make each
##explain the purpose - Fix formatting
- Usage examples in yellow:
$(YELLOW)make foo$(RESET) - Remove trailing whitespace
.PHONYonly on non-file targets
- Usage examples in yellow:
- Add missing pieces -
help-unclassified, catch-all%:, etc.
Phase 4: Verify
make help # Clean output?
make help-unclassified # Should be empty or minimal
make -n <target> # Dry-run key targets
What NOT to do without asking:
- Rename targets that CI/scripts depend on
- Remove targets that look unused
- Change structure (flat ↔ modular) without approval
Files in This Skill
reference.md- Detailed patterns, categorized help, error handlingtemplates/- Full copy-paste Makefiles for each stackmodules/- Reusable pieces for complex projectsmodules/help.mk- Generatedmake help(##@sections). Include LAST.modules/colors.mk- ANSI palette, color tiers,NO_COLORguardmodules/common.mk- Shell settings, guards, preflight checksmodules/todo.mk- GeneratedTODO.mdfrom prefixed code commentsmodules/todo-scripts/gen_todo.py- companion formake todo
🚧 Generated-help migration status.
base.mk,python-uv.mk, andpython-fastapi.mkuse the generated##@renderer. The six templates still hand-writing their help block carry aTODO_FUTUREat the top of the file — find them withgrep -rn "TODO_FUTURE" templates/(currentlygo.mk,nodejs.mk,flutter.mk,electron.mk,chrome-extension.mk,static-site.mk). Convert one whenever you touch it for another reason.
Example: Adding a Target
User: "Add a target to run my tests"
.PHONY: test
test: ## Run tests
$(call print_section,Running tests)
uv run pytest tests/ -v
$(call print_success,Tests passed)
User: "Add database targets"
.PHONY: db-start db-stop db-migrate
db-start: _check-docker ## Start database
docker compose up -d postgres
db-stop: ## Stop database
docker compose down
db-migrate: _check-postgres ## Run migrations
uv run alembic upgrade head