Makefile
Rama master · réplica de lectura del repositorio original.
# Caprock — build, test, docs, and release targets.
#
# Go module lives at the repo root; the dashboard lives in ui/ and is embedded into
# the binary from internal/api/dist (built by `make ui`). That directory is
# committed so `go install` embeds a real UI, but only master's dist workflow
# commits it: a pull request never carries it (ADR-068).
DOCS := $(shell find . -name '*.md' \
-not -path './node_modules/*' -not -path './.git/*' \
-not -path './vendor/*' -not -path './ui/node_modules/*' \
-not -path './ui/dist/*' -not -path './.claude/*' \
-not -path './app/node_modules/*' -not -path './app/src-tauri/target/*' \
-not -path './relay/node_modules/*' -not -path './relay/.wrangler/*' \
-not -path './ios-app/node_modules/*' -not -path './ios-app/ios/App/App/public/*' | sort)
# Every recipe below is POSIX shell: pipelines, `[ -d ]`, single-quoted
# -ldflags, `2>/dev/null`. On Windows, GNU make with no `sh` on PATH falls back
# to cmd.exe, which understands none of it — the first Windows contributor to
# clone the repo hit `mkdir -p` on the very first command in CONTRIBUTING.md
# (FB-025).
#
# CI never caught it because CI does not use make on Windows: it runs `go build`
# and `go test` directly, so the Windows job is green while `make build` is
# broken. A green matrix is not the same as a working repository.
#
# Rather than rewrite ~20 recipes in a dialect no one here can test, point make
# at the Bash that is already on every Windows dev machine: Git for Windows
# ships one, and Go and Node are prerequisites anyway. If it is on PATH under
# any of its usual names, use it; otherwise fall back and let the recipe fail
# with a clear message rather than a mangled one.
ifeq ($(OS),Windows_NT)
SHELL := $(shell where bash 2>NUL | findstr /R "." || echo bash)
.SHELLFLAGS := -c
# `go build -o bin/caprock` writes exactly that name, with no extension —
# `go build` only appends .exe when it is choosing the name itself. The
# result is a file Windows will not run by name, which is a broken build that
# exits 0. The CI job added with this fix is what caught it.
EXE := .exe
endif
BIN := bin
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
LDFLAGS := -s -w -X github.com/dspv/caprock/internal/version.Version=$(VERSION)
# Mail's sign-in client IDs (docs/mail-oauth.md), from the environment when
# set (release.yml passes the GitHub secrets of the same names); empty, Mail
# offers app passwords instead of the sign-in buttons.
MAIL_OAUTH := github.com/dspv/caprock/internal/mail/oauth
LDFLAGS += -X $(MAIL_OAUTH).GoogleClientID=$(CAPROCK_MAIL_GOOGLE_CLIENT_ID) -X $(MAIL_OAUTH).GoogleClientSecret=$(CAPROCK_MAIL_GOOGLE_CLIENT_SECRET) -X $(MAIL_OAUTH).MicrosoftClientID=$(CAPROCK_MAIL_MICROSOFT_CLIENT_ID)
GOFLAGS := -trimpath
export CGO_ENABLED = 0
.DEFAULT_GOAL := help
# --- help -----------------------------------------------------------------
.PHONY: help
help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
| sort | awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-14s\033[0m %s\n", $$1, $$2}'
# --- build ----------------------------------------------------------------
.PHONY: build
build: ui ## Build ./bin/caprock, ./bin/caprock-hook and ./bin/caprock-pty-host (with the embedded UI)
@mkdir -p $(BIN)
go build $(GOFLAGS) -ldflags '$(LDFLAGS)' -o $(BIN)/caprock$(EXE) ./cmd/caprock
go build $(GOFLAGS) -ldflags '$(LDFLAGS)' -o $(BIN)/caprock-hook$(EXE) ./cmd/caprock-hook
go build $(GOFLAGS) -ldflags '$(LDFLAGS)' -o $(BIN)/caprock-pty-host$(EXE) ./cmd/caprock-pty-host
.PHONY: build-go
build-go: ## Build the Go binaries only (uses whatever UI is already embedded)
@mkdir -p $(BIN)
go build $(GOFLAGS) -ldflags '$(LDFLAGS)' -o $(BIN)/caprock$(EXE) ./cmd/caprock
go build $(GOFLAGS) -ldflags '$(LDFLAGS)' -o $(BIN)/caprock-hook$(EXE) ./cmd/caprock-hook
go build $(GOFLAGS) -ldflags '$(LDFLAGS)' -o $(BIN)/caprock-pty-host$(EXE) ./cmd/caprock-pty-host
.PHONY: ui
ui: ## Build the dashboard into internal/api/dist (installs deps on a fresh clone; never commit the result)
cd ui && { [ -d node_modules ] || npm ci; }
cd ui && npm run build
# internal/api/dist is committed for `go install`, and rebuilt on master by
# .github/workflows/dist.yml after every merge that touches ui/ (ADR-068). A
# branch never changes it: two branches that both rebuilt it always conflicted
# there, whatever else they touched. A local `make ui` leaves the directory
# modified; that is expected, and nothing here asks you to commit it.
.PHONY: dist-untouched
dist-untouched: ## Fail if this branch's commits change internal/api/dist (master rebuilds it; ADR-068)
@base=$$(git merge-base HEAD origin/master 2>/dev/null) || { \
echo "dist-untouched: no origin/master here, skipped"; exit 0; }; \
if ! git diff --quiet "$$base" HEAD -- internal/api/dist; then \
echo "This branch changes internal/api/dist. dist is rebuilt on master after merge; drop your dist changes:"; \
echo " make dist-reset && git commit -m 'chore: drop the dist changes'"; \
git --no-pager diff --stat "$$base" HEAD -- internal/api/dist | tail -3; exit 1; fi
@echo "dist is untouched by this branch"
.PHONY: dist-reset
dist-reset: ## Put internal/api/dist back as this branch's base on master had it (staged; commit to drop a branch's dist changes)
@base=$$(git merge-base HEAD origin/master) && \
git rm -rq --cached --ignore-unmatch -- internal/api/dist && \
git checkout "$$base" -- internal/api/dist && \
git clean -qfd -- internal/api/dist && \
echo "internal/api/dist is as $$(git rev-parse --short $$base) has it (staged)"
.PHONY: dist-check
dist-check: ui ## Fail if the committed internal/api/dist/ is not this commit's ui/ built (the release gate for `go install`)
@git status --porcelain --untracked-files=all -- internal/api/dist \
| grep -vE '^[MARD] ' | grep -q . && { \
echo "internal/api/dist/ is not ui/ built. Releases tag the commit after master's dist rebuild:"; \
echo " wait for the dist workflow (gh run list -w dist -L 3), git pull, then tag; never commit dist by hand"; \
git --no-pager status --short -- internal/api/dist | tail -5; exit 1; } || true
@echo "dist is in sync with ui/"
.PHONY: ui-install
ui-install: ## npm ci for the dashboard
cd ui && npm ci
.PHONY: dev
dev: ## Run daemon (go run) + vite dev server; UI on :5173 proxies API to :22776
@./scripts/dev.sh
# --- test / lint ----------------------------------------------------------
.PHONY: test
test: test-go test-ui test-scripts ## All tests
.PHONY: test-go
test-go: ## Go tests
go test ./...
# Every statement the Go tests run, explained against the latest schema
# (internal/store/plans_test.go). The OpenCode database on this machine is
# left out, as on CI, where the check runs on the go job's own test run.
PLANS_DIR := bin/plans
.PHONY: plans
plans: ## Rewrite internal/store/testdata/query-plans.txt from a run of the Go tests; review its diff
rm -rf $(PLANS_DIR)
CAPROCK_PLAN_LOG=$(CURDIR)/$(PLANS_DIR) OPENCODE_DB=:memory: go test -count=1 ./...
CAPROCK_PLANS_FROM=$(CURDIR)/$(PLANS_DIR) CAPROCK_PLANS_UPDATE=1 go test -count=1 -run '^TestQueryPlans$$' ./internal/store
.PHONY: test-scripts
test-scripts: ## Release script tests (latest.json for the app updater, changelog fragments)
python3 -m unittest discover -s scripts -p 'test_*.py'
.PHONY: test-ui
test-ui: ## Dashboard tests
cd ui && npm test -- --run
.PHONY: lint
lint: lint-go lint-ui ## All linters
.PHONY: lint-go
lint-go: ## go vet + golangci-lint
go vet ./...
golangci-lint run
.PHONY: lint-ui
lint-ui: ## tsc --noEmit
cd ui && npm run typecheck
.PHONY: smoke
smoke: build-go ## Phase 0 DoD scenario + the Phase 2 e2e (what CI's smoke step runs)
go test -tags smoke -count=1 ./internal/smoke/... ./internal/board/...
# The app's golden paths in a headless browser (ui/e2e, .ai/10-infrastructure.md
# § E2E): this checkout's dashboard, built into a temp directory and served by
# `vite preview`, in front of a daemon under `env -i` with a temp HOME and data
# dir, a free port and testdata/fakeclaude as its claude. The browser is
# Playwright's own headless shell, never the system Chrome. About 20 s.
.PHONY: e2e
e2e: build-go ## The app's golden paths in Playwright's headless shell against a sandboxed daemon (ui/e2e)
cd ui && { [ -d node_modules ] || npm ci; }
cd ui && node e2e/has-browser.mjs
cd ui && npx playwright test -c e2e $(E2E_ARGS)
# --- desktop app (app/, Tauri v2; ADR-038) ----------------------------------
# The app bundles the daemon as a Tauri sidecar, which Tauri looks for as
# app/src-tauri/binaries/caprock-<rust host triple>[.exe]; app-sidecar builds
# it from this checkout so the app always carries the daemon it was built with,
# and caprock-pty-host beside it, the binary the daemon holds terminals in.
APP_SRC := app/src-tauri
APP_TRIPLE = $(shell rustc -vV 2>/dev/null | sed -n 's/^host: //p')
# Bundle formats for app-bundle; empty means every format tauri.conf.json lists
# for this OS. `make app-bundle APP_BUNDLES=app` skips Tauri's macOS .dmg, whose
# layout step drives Finder (releases and CI make theirs with scripts/app-dmg.sh).
APP_BUNDLES ?=
# The app's version for app-bundle (release jobs pass the tag without the v);
# empty keeps tauri.conf.json's.
APP_VERSION ?=
# With TAURI_SIGNING_PRIVATE_KEY (and _PASSWORD) in the environment,
# app-bundle also writes the signed updater bundles (F20, ADR-042):
# tauri.updater.conf.json turns createUpdaterArtifacts on. Without the key
# it builds exactly as before.
.PHONY: app-sidecar
app-sidecar: ## Build the daemon the app bundles (app/src-tauri/binaries/caprock[-pty-host]-<triple>)
@[ -n "$(APP_TRIPLE)" ] || { echo "rustc not found: install Rust with rustup (see app/README.md)"; exit 1; }
@mkdir -p $(APP_SRC)/binaries
go build $(GOFLAGS) -ldflags '$(LDFLAGS)' -o $(APP_SRC)/binaries/caprock-$(APP_TRIPLE)$(EXE) ./cmd/caprock
go build $(GOFLAGS) -ldflags '$(LDFLAGS)' -o $(APP_SRC)/binaries/caprock-pty-host-$(APP_TRIPLE)$(EXE) ./cmd/caprock-pty-host
.PHONY: app
app: app-sidecar ## Run the desktop app in development (debug build; finds or starts a daemon)
cargo run --manifest-path $(APP_SRC)/Cargo.toml
.PHONY: app-test
app-test: app-sidecar ## Desktop app: cargo fmt --check, clippy -D warnings, cargo test
cargo fmt --manifest-path $(APP_SRC)/Cargo.toml --check
cargo clippy --manifest-path $(APP_SRC)/Cargo.toml --all-targets -- -D warnings
cargo test --manifest-path $(APP_SRC)/Cargo.toml
.PHONY: app-bundle
app-bundle: app-sidecar ## Build the release app bundle(s) under app/src-tauri/target/release/bundle
cd app && { [ -d node_modules ] || npm ci; }
cd app && npx tauri build $(if $(APP_BUNDLES),--bundles $(APP_BUNDLES),) $(if $(APP_VERSION),--config '{"version":"$(APP_VERSION)"}',) $(if $(TAURI_SIGNING_PRIVATE_KEY),--config src-tauri/tauri.updater.conf.json,)
.PHONY: app-local
app-local: ## macOS: build this checkout's app (host arch, release) and install it over /Applications/Caprock.app
@bash scripts/app-local.sh
.PHONY: app-local-revert
app-local-revert: ## macOS: back from a local build to the released app (brew reinstall --cask dspv/tap/caprock-app)
@bash scripts/app-local.sh --revert
.PHONY: app-release
app-release: ## macOS: build the universal .dmg for TAG=vX.Y.Z, check it, attach it to the release, render the cask
@[ -n "$(TAG)" ] || { echo "usage: make app-release TAG=vX.Y.Z [ARGS='--no-upload --clobber --cask-pr']"; exit 1; }
@bash scripts/app-release.sh $(TAG) $(ARGS)
.PHONY: app-latest
app-latest: ## Attach version-less copies of TAG=vX.Y.Z's app files, add them to checksums.txt, mark it Latest
@[ -n "$(TAG)" ] || { echo "usage: make app-latest TAG=vX.Y.Z [ARGS=--allow-missing]"; exit 1; }
@bash scripts/app-latest.sh $(TAG) $(ARGS)
# --- docs -----------------------------------------------------------------
.PHONY: docs-fmt
docs-fmt: ## Tight-align all markdown tables (run after editing any table)
@python3 scripts/align-tables.py $(DOCS)
.PHONY: docs-check
docs-check: ## Fail if any markdown table is unaligned or a changelog fragment is malformed (CI gate)
@python3 scripts/align-tables.py --check $(DOCS)
@python3 scripts/changelog.py check
.PHONY: docs-links
docs-links: ## Fail if any relative markdown link does not resolve
@python3 scripts/check-links.py $(DOCS)
.PHONY: lang-check
lang-check: ## Fail if any tracked text file holds Cyrillic (generated dist and binaries skipped)
@python3 scripts/check-no-cyrillic.py
.PHONY: pr-check
pr-check: ## Before opening a PR: changed Go packages vet + test -race -count=3, Windows vet + test compile, the whole UI suite when ui/ changed, make e2e when ui/ or internal/api changed
@./scripts/pr-check.sh
.PHONY: check
check: check-go check-ui ## Docs gates + lint + test + dist untouched + smoke (what CI runs, minus the OS matrix)
# The two halves of check, which share nothing, so release.yml's verify runs
# them as two jobs side by side. Together they are exactly check.
.PHONY: check-go
check-go: docs-check docs-links lang-check lint-go test-go smoke ## check's Go half: docs gates, vet + golangci-lint, go test, smoke
.PHONY: check-ui
check-ui: lint-ui test-ui test-scripts dist-untouched ## check's dashboard half: typecheck, vitest, release script tests, dist untouched
.PHONY: release-ready
release-ready: check dist-check ## Everything `check` does, plus the things only a release forgets
@# dist-check: the tag must hold its own ui/ built, or `go install @vX.Y.Z`
@# embeds an older dashboard; release.yml's verify fails the same way, but
@# only after the tag is pushed, and a tag is never moved.
@echo ""
@echo "→ the site's changelog"
@# The site keeps its own copy of CHANGELOG.md, regenerated by a script in
@# that repository. Nothing enforces running it, so a release cycle that
@# skips the step leaves caprock.dev showing a version that no longer
@# exists — which is what happened on 0.49.1 through 0.49.3, and was only
@# noticed because a stray background job was still watching for a string.
@#
@# A warning rather than a failure: the site is a sibling checkout that not
@# everyone building this repo has, and a gate that cannot run everywhere is
@# a gate people learn to skip.
@if [ -d ../caprock-web ]; then \
if ! (cd ../caprock-web && node scripts/sync-changelog.mjs --check >/dev/null 2>&1); then \
echo " ✗ caprock.dev is behind CHANGELOG.md."; \
echo " cd ../caprock-web && npm run sync:changelog && git commit -am 'docs(changelog): sync' && git push"; \
exit 1; \
fi; \
echo " ✓ the site's changelog matches"; \
else \
echo " – no ../caprock-web checkout; sync it wherever you keep it"; \
fi
@echo ""
@echo "Ready. Tag with: git tag -a vX.Y.Z -m '...' && git push origin vX.Y.Z"
.PHONY: shots
shots: ## Re-take the documented screenshots and open a PR (needs a real database)
@bash scripts/refresh-shots.sh
.PHONY: record
record: ## Serve a scrubbed dashboard on :4291 to record video from (Ctrl-C to stop)
@bash scripts/record-stand.sh
.PHONY: gif
gif: ## Re-record a README GIF from the stand (make record first). SCENE=context-tax
@python3 scripts/record-gif.py $(or $(SCENE),context-tax)
.PHONY: reload
reload: build ## Build the dashboard + binaries and restart the running daemon on this machine
@# Runs ./bin/caprock rather than installing over the copy on PATH.
@#
@# It used to `install` over whatever `command -v caprock` found, which on a
@# Homebrew machine replaces brew's symlink with a plain file: `brew upgrade`
@# then reports success while PATH still resolves to the dev build, and the
@# next `brew doctor` complains. Exactly the breakage the update dialog warns
@# about, caused by our own dev loop. Recovering meant `brew link --overwrite`.
@$(BIN)/caprock down >/dev/null 2>&1 || true
@$(BIN)/caprock up --no-open >/dev/null 2>&1 || true
@echo "reloaded → $$($(BIN)/caprock --version) (from ./bin; the copy on PATH is untouched)"