-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathMakefile
More file actions
289 lines (259 loc) · 12.2 KB
/
Copy pathMakefile
File metadata and controls
289 lines (259 loc) · 12.2 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
.PHONY: build install setup uninstall run test test-e2e lint clean fmt vet daemon tui check-go bootstrap fuzz fuzz-quick tapes tapes-check release-check release-snapshot brew-test mcp
BIN_DIR := bin
INSTALL_DIR := $(HOME)/.local/bin
LDFLAGS := -s -w -X main.version=$(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
UNAME_S := $(shell uname -s)
# On macOS, ad-hoc sign each binary so spctl/GateKeeper accepts it when
# launched from a terminal. Without this the OS adds com.apple.provenance
# to the unsigned binary and SIGKILLs it silently on exec, which manifests
# as `ccmux` exiting with no output. Launchd-managed ccmuxd is exempt, so
# this only affects direct TUI/CLI invocations.
ifeq ($(UNAME_S),Darwin)
CODESIGN = codesign --force --sign - $@ 2>/dev/null || true
GO_INSTALL_HINT = brew install go
else ifeq ($(UNAME_S),Linux)
CODESIGN =
GO_INSTALL_HINT = sudo apt install golang-go # or your distro's equivalent / https://go.dev/doc/install
else
CODESIGN =
GO_INSTALL_HINT = https://go.dev/doc/install
endif
# check-go: friendly "install Go first" message instead of the cryptic
# "/bin/sh: go: command not found" make spits out by default. Invoked
# from every build/test/run target — clean and uninstall don't need it.
check-go:
@command -v go >/dev/null 2>&1 || { \
printf "\n\033[1;31m✗\033[0m \`go\` not found on PATH.\n\n"; \
echo "ccmux is built from source — Go 1.26+ is required."; \
echo "Install it with:"; \
echo " $(GO_INSTALL_HINT)"; \
echo ""; \
echo "Then re-run \`make setup\` (or \`make build\`)."; \
echo ""; \
exit 1; \
}
build: check-go $(BIN_DIR)/ccmux $(BIN_DIR)/ccmuxd $(BIN_DIR)/ccmux-mcp
$(BIN_DIR)/ccmux: $(shell find cmd/ccmux internal -type f -name '*.go' 2>/dev/null) go.mod go.sum
@mkdir -p $(BIN_DIR)
go build -ldflags "$(LDFLAGS)" -o $(BIN_DIR)/ccmux ./cmd/ccmux
@$(CODESIGN)
$(BIN_DIR)/ccmuxd: $(shell find cmd/ccmuxd internal -type f -name '*.go' 2>/dev/null) go.mod go.sum
@mkdir -p $(BIN_DIR)
go build -ldflags "$(LDFLAGS)" -o $(BIN_DIR)/ccmuxd ./cmd/ccmuxd
@$(CODESIGN)
$(BIN_DIR)/ccmux-mcp: $(shell find cmd/ccmux-mcp internal/daemon -type f -name '*.go' 2>/dev/null) go.mod go.sum
@mkdir -p $(BIN_DIR)
go build -ldflags "$(LDFLAGS)" -o $(BIN_DIR)/ccmux-mcp ./cmd/ccmux-mcp
@$(CODESIGN)
# `make mcp` builds just the MCP server. Convenience for working on
# the agent-facing API without rebuilding the TUI + daemon.
mcp: check-go $(BIN_DIR)/ccmux-mcp
install: build
@mkdir -p $(INSTALL_DIR)
@# Write to a sibling temp file then rename, so we can replace the
@# currently-running binary. On Linux, `cp` over a running executable
@# fails with ETXTBSY ("Text file busy") because cp truncates the
@# destination, which the kernel forbids while the file backs a live
@# process's text segment. rename(2) is allowed — the old inode stays
@# mapped by the running process and new exec()s pick up the new one.
@# This is what `ccmux update` relies on to reinstall itself.
cp $(BIN_DIR)/ccmux $(INSTALL_DIR)/ccmux.new
mv -f $(INSTALL_DIR)/ccmux.new $(INSTALL_DIR)/ccmux
cp $(BIN_DIR)/ccmuxd $(INSTALL_DIR)/ccmuxd.new
mv -f $(INSTALL_DIR)/ccmuxd.new $(INSTALL_DIR)/ccmuxd
cp $(BIN_DIR)/ccmux-mcp $(INSTALL_DIR)/ccmux-mcp.new
mv -f $(INSTALL_DIR)/ccmux-mcp.new $(INSTALL_DIR)/ccmux-mcp
ifeq ($(UNAME_S),Darwin)
@# On macOS the cp-into-INSTALL_DIR stamps com.apple.provenance on
@# the destination, which can invalidate the ad-hoc signature
@# we applied in the build step. Strip the xattr and re-sign so
@# the installed binary survives GateKeeper checks on exec. Without
@# this, freshly-installed `ccmux setup` got SIGKILLed when spawned
@# by `ccmux update` (the running process was already in memory and
@# fine; the child exec hit the new provenance + stale sig).
@xattr -d com.apple.provenance $(INSTALL_DIR)/ccmux 2>/dev/null || true
@xattr -d com.apple.provenance $(INSTALL_DIR)/ccmuxd 2>/dev/null || true
@xattr -d com.apple.provenance $(INSTALL_DIR)/ccmux-mcp 2>/dev/null || true
@codesign --force --sign - $(INSTALL_DIR)/ccmux 2>/dev/null || true
@codesign --force --sign - $(INSTALL_DIR)/ccmuxd 2>/dev/null || true
@codesign --force --sign - $(INSTALL_DIR)/ccmux-mcp 2>/dev/null || true
endif
@echo "Installed to $(INSTALL_DIR). Make sure it's on your PATH."
@# Best-effort daemon restart so the running ccmuxd picks up the
@# newly-installed binary. Soft-fails (|| true) so a first install
@# (no plist/unit yet) doesn't make `make install` exit non-zero
@# — `ccmux daemon restart` returns an error when the service
@# isn't registered, which is the expected state pre-`ccmux setup`.
@# Without this, `make install` left an old ccmuxd process running
@# the previous binary's route table; mobile/web clients hitting
@# new endpoints would silently 404. See cmd/ccmux/cmd/update.go
@# for the same restart at the end of `ccmux update`.
@$(INSTALL_DIR)/ccmux daemon restart 2>/dev/null || true
# `make bootstrap` is the friendliest entry point for a fresh machine:
# it verifies the build chain (go / git / make / brew on macOS),
# offers to install whatever's missing, then chains into `make setup`.
# Use this when you don't know if Go is installed. Otherwise `make
# setup` skips the dep check and goes straight to build + wizard.
bootstrap:
@./scripts/bootstrap.sh
# `make setup` is the one-shot for new users with a working build chain:
# build → install to PATH → run the interactive setup wizard.
# Existing users can re-run it; the wizard is idempotent and skips
# any step whose underlying state is already good.
setup: install
@echo
@echo "Running ccmux setup wizard…"
@$(INSTALL_DIR)/ccmux setup
# `make uninstall` is a thin wrapper around the real uninstaller.
# Always call `ccmux uninstall` first if you can — it handles the
# daemon, state files, and tmux chrome that this target ignores.
uninstall:
@if command -v $(INSTALL_DIR)/ccmux >/dev/null 2>&1; then \
$(INSTALL_DIR)/ccmux uninstall --yes || true; \
else \
echo "ccmux is not on PATH — removing binaries only"; \
rm -f $(INSTALL_DIR)/ccmux $(INSTALL_DIR)/ccmuxd; \
fi
tui run: check-go
go run ./cmd/ccmux
daemon: check-go
go run ./cmd/ccmuxd
test: check-go
go test ./...
# Bash-level regression test for docs/vhs/render.sh's cleanup pattern.
# Lives outside `make test` because it requires the built binary; run
# via `make test-vhs-cleanup` after `make build`. The Go side of this
# bug (daemon socket-conflict exit code, plist KeepAlive condition)
# is covered by `make test` and `make test-e2e`.
test-vhs-cleanup: build
@bash docs/vhs/cleanup_test.sh
# Integration / e2e suite — builds the binaries and runs every
# //go:build integration test. Requires tmux to be installed.
# Packages covered: internal/e2e (subprocess CUJs) and cmd/ccmuxd
# (poll-loop white-box tests).
test-e2e: check-go build
@tmp="$$(mktemp -d /tmp/ccmux-e2e.XXXXXX)"; \
gomodcache="$$(go env GOMODCACHE)"; \
gocache="$$(go env GOCACHE)"; \
trap 'env -u TMUX HOME="$$tmp" TMUX_TMPDIR="$$tmp" tmux kill-server >/dev/null 2>&1 || true; rm -rf "$$tmp"' EXIT INT TERM; \
env -u TMUX HOME="$$tmp" TMUX_TMPDIR="$$tmp" GOMODCACHE="$$gomodcache" GOCACHE="$$gocache" go test -tags integration -timeout 180s ./internal/e2e/ ./cmd/ccmuxd/
# Native Go fuzzer pass — one round-trip over every FuzzXxx target.
#
# Pairs are listed verbatim (no globbing) so adding a target is a
# deliberate one-line edit here, and a forgotten target is loud
# instead of silently skipped. CI runs `make fuzz FUZZTIME=100000x`
# for a quick PR-time signal; humans run `make fuzz` (default
# 5m/target) before tightening a parser or after touching a heuristic
# surface.
# Bump FUZZTIME=1h for an overnight sweep, e.g. before a release.
#
# Failing seeds land under <pkg>/testdata/fuzz/<FuzzName>/<sha> per
# Go's standard convention — commit them so the next `go test ./...`
# picks them up as regression seeds.
FUZZTIME ?= 5m
FUZZ_TARGETS := \
./internal/agent:FuzzParseID \
./internal/project:FuzzReadAgent \
./internal/clipboard:FuzzOSC52RoundTrip \
./internal/sleeplock:FuzzParsePmsetBatt \
./internal/claude:FuzzClassify \
./internal/tmux:FuzzSessionNameForPath \
./internal/tui:FuzzRenderSessionLine_DegenerateInputs \
./internal/tui:FuzzDialTarget
fuzz: check-go
@for pair in $(FUZZ_TARGETS); do \
pkg=$${pair%:*}; target=$${pair#*:}; \
printf '\n=== %s :: %s (%s) ===\n' "$$pkg" "$$target" "$(FUZZTIME)"; \
go test "$$pkg" -run '^$$' -fuzz="^$${target}$$" -fuzztime=$(FUZZTIME) || exit 1; \
done
# Quick local pass — mirrors CI's per-target execution-count budget so
# `make fuzz-quick` gives the same signal a PR would.
fuzz-quick:
@$(MAKE) fuzz FUZZTIME=100000x
fmt: check-go
gofmt -w .
vet: check-go
go vet ./...
lint: fmt vet
@command -v staticcheck >/dev/null && staticcheck ./... || echo "staticcheck not installed; skipping"
clean:
rm -rf $(BIN_DIR) dist
# --- Release ----------------------------------------------------------
# The real release runs in CI when a v* tag is pushed (see
# .github/workflows/release.yml). These targets are for validating the
# pipeline locally — neither publishes anything.
#
# release-check — lint .goreleaser.yaml
# release-snapshot — full cross-platform build into dist/, no publish
release-check:
@command -v goreleaser >/dev/null 2>&1 || { echo "release: goreleaser not installed — brew install goreleaser"; exit 1; }
goreleaser check
release-snapshot: check-go
@command -v goreleaser >/dev/null 2>&1 || { echo "release: goreleaser not installed — brew install goreleaser"; exit 1; }
goreleaser release --snapshot --clean
# brew-test — install the published formula into a sandbox HOME,
# run `ccmux --help` / `ccmux doctor`, then uninstall. Useful after
# a release to confirm `brew install skzv/tap/ccmux` actually works
# without disturbing your dev install or running ccmuxd. See
# scripts/brew-test.sh for the isolation model + --tap / --keep flags.
brew-test:
@command -v brew >/dev/null 2>&1 || { echo "brew-test: brew not installed"; exit 1; }
bash scripts/brew-test.sh
# --- Demo tapes -------------------------------------------------------
# Renders every CUJ tape to docs/vhs/out/. Requires: make build, vhs, ffmpeg.
TAPES := \
docs/vhs/cuj01_new_project.tape \
docs/vhs/cuj02_dashboard.tape \
docs/vhs/cuj03_attach_detach.tape \
docs/vhs/cuj04_resume.tape \
docs/vhs/cuj05_pick_agent.tape \
docs/vhs/cuj06_notes.tape \
docs/vhs/cuj07_multi_machine.tape \
docs/vhs/cuj08_phone.tape \
docs/vhs/cuj09_agents.tape \
docs/vhs/cuj10_setup_doctor.tape
tapes: build
@command -v vhs >/dev/null || { echo "tapes: vhs not installed — brew install vhs"; exit 1; }
@for t in $(TAPES); do \
echo "tapes: rendering $$t"; \
bash docs/vhs/render.sh "$$t"; \
done
@echo "tapes: rendering docs/vhs/cuj11_update.tape (update demo)"
@CCMUX_UPDATE_DEMO=true bash docs/vhs/render.sh docs/vhs/cuj11_update.tape
# tapes-check enforces the CUJ catalog ⇄ artifact invariant. The
# catalog at docs/01_Specs/01_CUJ_Catalog.md is the source of truth;
# every row whose ID matches `C\d+` must have a tape under docs/vhs/,
# and every row marked `demoable: full` must additionally have its
# rendered GIF under docs/vhs/out/. `stubbed` and `still` entries skip
# the GIF requirement (e.g. C7's GIF is gitignored — the Network
# screen leaks live tailnet peer names/IPs).
#
# Runs cheaply in CI — no VHS or ffmpeg needed.
tapes-check:
@awk -F'|' '/^\| C[0-9]+ \|/ { \
id=$$2; slug=$$3; demoable=$$7; \
gsub(/^ +| +$$/, "", id); \
gsub(/^ +| +$$/, "", slug); \
gsub(/^ +| +$$/, "", demoable); \
print id, slug, demoable; \
}' docs/01_Specs/01_CUJ_Catalog.md > $(BIN_DIR)/.cuj-catalog.tmp; \
missing=0; \
while read id slug demoable; do \
num=$${id#C}; \
if [ $$num -lt 10 ]; then num="0$$num"; fi; \
tape="docs/vhs/cuj$${num}_$${slug}.tape"; \
if [ ! -f "$$tape" ]; then \
echo "tapes-check: missing tape $$tape (catalog $$id)"; \
missing=$$((missing+1)); \
fi; \
if [ "$$demoable" = "full" ]; then \
gif="docs/vhs/out/cuj$${num}_$${slug}.gif"; \
if [ ! -f "$$gif" ]; then \
echo "tapes-check: missing GIF $$gif (catalog $$id demoable=full)"; \
missing=$$((missing+1)); \
fi; \
fi; \
done < $(BIN_DIR)/.cuj-catalog.tmp; \
rm -f $(BIN_DIR)/.cuj-catalog.tmp; \
if [ $$missing -gt 0 ]; then exit $$missing; fi; \
echo "tapes-check: catalog and rendered artifacts in sync"