Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
776 lines (688 loc) · 34.8 KB
/
Copy pathMakefile
File metadata and controls
776 lines (688 loc) · 34.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
.DEFAULT_GOAL := help
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
BUILD_DATE := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
# Public PostHog ingest key. Builds without it send no telemetry.
POSTHOG_PROJECT_KEY ?= phc_AzHd9YvuHR7M5poKzC6eW654d3SgKyBdoQPuwkWhimUf
# Who ships the binary: a package manager such as "homebrew", or "kenn" for
# official direct downloads. Package-manager builds refuse to self-update and
# point users at the package manager. Telemetry reports the value too.
DISTRIBUTION ?=
BINDIR ?= .
# Recursive (=) so target-specific DISTRIBUTION values reach the linker.
LDFLAGS = -X main.version=$(VERSION) \
-X main.commit=$(COMMIT) \
-X main.buildDate=$(BUILD_DATE) \
-X main.distribution=$(DISTRIBUTION) \
-X go.kenn.io/kit/telemetry/posthog.distribution=$(DISTRIBUTION) \
-X go.kenn.io/kit/telemetry/posthog.projectKey=$(POSTHOG_PROJECT_KEY)
LDFLAGS_RELEASE = $(LDFLAGS) -s -w
DESKTOP_DIST_DIR := dist/desktop
GOLANGCI_LINT_VERSION ?= v2.14.0
# Isolate each checkout from stale sibling-worktree fixes and issue positions.
GOLANGCI_LINT_CACHE ?= $(CURDIR)/.golangci-cache
export GOLANGCI_LINT_CACHE
CUSTOM_GCL := ./custom-gcl
GOEXE := $(shell go env GOEXE)
PRICING_SNAPSHOT_FILE := internal/pricing/snapshot/litellm_snapshot.json.gz
# sqlite-vec's cgo bindings #include "sqlite3.h". Without an override the
# compiler falls back to the system header (the macOS SDK one marks
# sqlite3_auto_extension deprecated, warning on every build) which can also
# drift from the amalgamation mattn/go-sqlite3 statically links. Compile
# against the bundled amalgamation's own header instead.
# Setting CGO_CFLAGS replaces Go's built-in default of "-O2 -g", so restate
# it explicitly or the SQLite amalgamation compiles unoptimized (2-3x slower
# queries, caught by the bench gate). Caller-provided flags stay last so
# they can still override.
SQLITE_INCLUDE_DIR := .sqlite-include
export CGO_CFLAGS := -O2 -g -I$(CURDIR)/$(SQLITE_INCLUDE_DIR) $(CGO_CFLAGS)
GOPATH_FIRST := $(shell go env GOPATH | cut -d: -f1)
AIR_BIN := $(shell if command -v air >/dev/null 2>&1; then command -v air; \
elif [ -n "$$(go env GOBIN)" ] && [ -x "$$(go env GOBIN)/air" ]; then printf "%s" "$$(go env GOBIN)/air"; \
elif [ -x "$(GOPATH_FIRST)/bin/air" ]; then printf "%s" "$(GOPATH_FIRST)/bin/air"; \
fi)
# Compile without absolute source paths so every checkout and worktree shares
# one Go build cache instead of each compiling its own copy of every package.
# Read `go env` first so flags saved with `go env -w GOFLAGS=...` survive the
# export. GOFLAGS=-trimpath=false opts out. Debuggers need a source mapping:
# README.md#shared-go-build-cache.
ifeq ($(origin GOFLAGS),undefined)
GOFLAGS := $(shell go env GOFLAGS)
endif
ifeq ($(filter -trimpath -trimpath=%,$(GOFLAGS)),)
override GOFLAGS += -trimpath
endif
export GOFLAGS
.PHONY: build build-release homebrew install install-cjk-fts simple-fts frontend frontend-dev dev check-air air-install desktop-dev desktop-build desktop-macos-app desktop-macos-dmg desktop-windows-installer desktop-linux-appimage desktop-app docs-install docs-build docs-serve docs-check docs-screenshots docs-assets-branch docs-generated-assets-branch docs-deploy-staging docs-deploy test test-short test-evalingest bench-backends bench-gate bench-gate-config bench-pg-usage test-postgres test-postgres-ci test-postgres-raw-sync-ci test-postgres-core-ci test-postgres-core-fast-ci test-postgres-core-heavy-ci test-s3 postgres-up postgres-down test-clickhouse test-clickhouse-ci clickhouse-up clickhouse-down e2e e2e-duckdb memory-e2e vet lint lint-ci lint-golangci lint-golangci-ci nilaway nilaway-golangci-build lint-tools tidy clean release release-darwin-arm64 release-darwin-amd64 release-linux-amd64 install-hooks ensure-embed-dir pricing-snapshot sqlite-vec-header dev-snapshot help
# Ensure go:embed has at least one file (no-op if frontend is built)
ensure-embed-dir:
@mkdir -p internal/web/dist
@test -f internal/web/dist/.keep \
|| printf '%s\n' \
'keep embed dir for generated frontend assets' \
> internal/web/dist/.keep
# Copy the go-sqlite3 amalgamation header where CGO_CFLAGS points.
# Chained from pricing-snapshot so every Go-compiling target stages it
# without lengthening each prerequisite list.
sqlite-vec-header:
@go mod download github.com/mattn/go-sqlite3
@mkdir -p $(SQLITE_INCLUDE_DIR)
@install -m 0644 \
"$$(go list -m -f '{{.Dir}}' github.com/mattn/go-sqlite3)/sqlite3-binding.h" \
$(SQLITE_INCLUDE_DIR)/sqlite3.h
# Restore the generated LiteLLM fallback snapshot from its artifact branch.
# Pinned ref, SHA256, and branch are compiled into the snapshot tool.
pricing-snapshot: sqlite-vec-header
go run ./internal/pricing/cmd/litellm-snapshot -restore
# Build the binary (debug, with embedded pricing snapshot and frontend)
build: pricing-snapshot frontend
CGO_ENABLED=1 go build -tags fts5 -ldflags="$(LDFLAGS)" -o agentsview ./cmd/agentsview
@chmod +x agentsview
# Build with optimizations (release)
build-release: pricing-snapshot frontend
CGO_ENABLED=1 go build -tags fts5 -ldflags="$(LDFLAGS_RELEASE)" -trimpath -o agentsview ./cmd/agentsview
@chmod +x agentsview
# Homebrew build profile: the release build with cgo and
# DISTRIBUTION=homebrew, written to $(BINDIR)/agentsview.
homebrew: DISTRIBUTION := homebrew
homebrew: pricing-snapshot frontend
CGO_ENABLED=1 go build -tags fts5 -ldflags="$(LDFLAGS_RELEASE)" -trimpath -buildvcs=false -o "$(BINDIR)/agentsview" ./cmd/agentsview
# Install to ~/.local/bin, $GOBIN, or $GOPATH/bin.
# Copy to a temp file in the destination directory, then rename into place.
# Rename is atomic and produces a fresh inode, so overwriting the binary while
# an old agentsview is still running does not leave the kernel validating exec
# against stale code-signature pages (which SIGKILLs the new process on macOS).
install: build-release
@if [ -d "$(HOME)/.local/bin" ]; then \
INSTALL_DIR="$(HOME)/.local/bin"; \
else \
INSTALL_DIR="$${GOBIN:-$$(go env GOBIN)}"; \
if [ -z "$$INSTALL_DIR" ]; then \
GOPATH_FIRST="$$(go env GOPATH | cut -d: -f1)"; \
INSTALL_DIR="$$GOPATH_FIRST/bin"; \
fi; \
mkdir -p "$$INSTALL_DIR"; \
fi; \
echo "Installing to $$INSTALL_DIR/agentsview"; \
tmp="$$(mktemp "$$INSTALL_DIR/agentsview.tmp.XXXXXX")" || exit $$?; \
cleanup() { rm -f "$$tmp"; }; \
trap cleanup EXIT HUP INT TERM; \
cp agentsview "$$tmp" && chmod 755 "$$tmp" && mv -f "$$tmp" "$$INSTALL_DIR/agentsview"; \
status=$$?; \
trap - EXIT HUP INT TERM; \
cleanup; \
exit $$status
# Build the optional simple/cppjieba SQLite tokenizer sidecar from pinned
# upstream commits. It remains separate from the Go binary so SQLite can load
# the native extension on every supported platform.
simple-fts:
bash scripts/build-simple-fts.sh dist/agentsview-simple
# Install the binary and its CJK-search sidecar in sibling bin/lib trees.
install-cjk-fts: install simple-fts
@if [ -d "$(HOME)/.local/bin" ]; then \
INSTALL_DIR="$(HOME)/.local/bin"; \
else \
INSTALL_DIR="$${GOBIN:-$$(go env GOBIN)}"; \
if [ -z "$$INSTALL_DIR" ]; then \
GOPATH_FIRST="$$(go env GOPATH | cut -d: -f1)"; \
INSTALL_DIR="$$GOPATH_FIRST/bin"; \
fi; \
fi; \
SIMPLE_DIR="$$(cd "$$INSTALL_DIR/.." && pwd)/lib/agentsview/simple"; \
mkdir -p "$$SIMPLE_DIR/dict" "$$SIMPLE_DIR/licenses"; \
for name in libsimple.so libsimple.dylib simple.dll; do \
if [ -f "dist/agentsview-simple/$$name" ]; then \
install -m 0755 "dist/agentsview-simple/$$name" "$$SIMPLE_DIR/$$name"; \
fi; \
done; \
install -m 0644 dist/agentsview-simple/VERSIONS "$$SIMPLE_DIR/VERSIONS"; \
for name in hmm_model.utf8 idf.utf8 jieba.dict.utf8 stop_words.utf8 user.dict.utf8; do \
install -m 0644 "dist/agentsview-simple/dict/$$name" "$$SIMPLE_DIR/dict/$$name"; \
done; \
for name in simple-LICENSE cppjieba-LICENSE; do \
install -m 0644 "dist/agentsview-simple/licenses/$$name" "$$SIMPLE_DIR/licenses/$$name"; \
done; \
echo "Installed CJK FTS sidecar to $$SIMPLE_DIR"
# Build frontend SPA and copy into embed directory
frontend:
cd frontend && npm ci && npm run build
rm -rf internal/web/dist
cp -r frontend/dist internal/web/dist
printf '%s\n' \
'keep embed dir for generated frontend assets' \
> internal/web/dist/.keep
# Run Vite+ dev server (use alongside `make dev`)
frontend-dev:
cd frontend && npm run dev
# Build and run agentsview against a fresh snapshot of the prod SQLite DB.
# Prod DB is never written; sqlite3 .backup is WAL-safe even with prod running.
# Prod config.toml is NOT copied, so remote PG push is disabled in the snapshot.
# Overrides:
# PROD_DATA_DIR - source data dir (default: $$HOME/.agentsview)
# SNAPSHOT_DIR - destination dir (default: tmp/prod-snapshot)
# RESNAPSHOT=0 - reuse existing snapshot instead of re-cloning
PROD_DATA_DIR ?= $(HOME)/.agentsview
SNAPSHOT_DIR ?= tmp/prod-snapshot
# Resolve SNAPSHOT_DIR so relative and absolute paths both work.
SNAPSHOT_ABS := $(abspath $(SNAPSHOT_DIR))
# Sentinel file written into a snapshot directory the first time
# we populate it. Subsequent runs require this marker before
# touching any contents, so pointing SNAPSHOT_DIR at an
# unrelated existing directory cannot accidentally delete
# someone's files. The previous version used rm -rf on the
# whole directory; that left dangerous edge cases (e.g.
# SNAPSHOT_DIR=tmp wiping unrelated tmp/ contents) even with a
# denylist, so we now only ever delete a small set of files we
# know we wrote.
SNAPSHOT_MARKER := .agentsview-snapshot
dev-snapshot: build
@if [ ! -f "$(PROD_DATA_DIR)/sessions.db" ]; then \
echo "error: prod sessions.db not found at $(PROD_DATA_DIR)/sessions.db" >&2; \
exit 1; \
fi
@if [ -z "$(SNAPSHOT_ABS)" ]; then \
echo "error: SNAPSHOT_DIR resolved to empty path" >&2; \
exit 1; \
fi
@if [ -d "$(SNAPSHOT_ABS)" ] && [ ! -f "$(SNAPSHOT_ABS)/$(SNAPSHOT_MARKER)" ]; then \
if [ -n "$$(ls -A "$(SNAPSHOT_ABS)" 2>/dev/null)" ]; then \
echo "error: $(SNAPSHOT_ABS) is non-empty and missing the $(SNAPSHOT_MARKER) marker" >&2; \
echo " refusing to touch a directory we did not create" >&2; \
echo " remove the directory manually if you want to reuse this path" >&2; \
exit 1; \
fi; \
fi
@mkdir -p "$(SNAPSHOT_ABS)"
@touch "$(SNAPSHOT_ABS)/$(SNAPSHOT_MARKER)"
@if [ "$${RESNAPSHOT:-1}" = "1" ] || [ ! -f "$(SNAPSHOT_ABS)/sessions.db" ]; then \
echo "Snapshotting $(PROD_DATA_DIR)/sessions.db -> $(SNAPSHOT_ABS)/sessions.db"; \
for f in sessions.db sessions.db-wal sessions.db-shm \
config.toml config.json config.json.bak \
debug.log; do \
rm -f "$(SNAPSHOT_ABS)/$$f"; \
done; \
sqlite3 "$(PROD_DATA_DIR)/sessions.db" \
".backup $(SNAPSHOT_ABS)/sessions.db"; \
else \
echo "Reusing existing snapshot at $(SNAPSHOT_ABS)/sessions.db"; \
fi
AGENTSVIEW_DATA_DIR="$(SNAPSHOT_ABS)" ./agentsview serve --port 0
# Ensure air is installed for backend live reload
check-air:
@if [ -z "$(AIR_BIN)" ]; then \
echo "air not found. Install with: make air-install" >&2; \
exit 1; \
fi
# Install air for backend live reload
air-install:
go install github.com/air-verse/air@latest
# Run Go server in dev mode with live reload (use with frontend-dev).
# Edits to .go files trigger a rebuild + restart via air.
dev: pricing-snapshot ensure-embed-dir check-air
"$(AIR_BIN)" -c .air.toml -- $(ARGS)
# Run the Tauri desktop wrapper in development mode
desktop-dev:
cd desktop && npm ci && npm run tauri:dev
# Build desktop app bundles via Tauri
desktop-build:
cd desktop && npm ci && npm run tauri:build
# Build only the macOS .app bundle (skip DMG packaging).
# Skips updater artifact signing when TAURI_SIGNING_PRIVATE_KEY
# is not set so local builds succeed without release keys.
desktop-macos-app:
cd desktop && npm ci && npm run tauri:build:macos-app \
$(if $(TAURI_SIGNING_PRIVATE_KEY),,-- --config '{"bundle":{"createUpdaterArtifacts":false}}')
mkdir -p $(DESKTOP_DIST_DIR)/macos
rm -rf $(DESKTOP_DIST_DIR)/macos/AgentsView.app
cp -R desktop/src-tauri/target/release/bundle/macos/AgentsView.app \
$(DESKTOP_DIST_DIR)/macos/AgentsView.app
@echo "macOS app bundle copied to $(DESKTOP_DIST_DIR)/macos/AgentsView.app"
# Build macOS DMG installer
desktop-macos-dmg:
cd desktop && npm ci && npm run tauri:build:macos-dmg
mkdir -p $(DESKTOP_DIST_DIR)/macos
rm -f $(DESKTOP_DIST_DIR)/macos/*.dmg
@dmg_count=$$(find desktop/src-tauri/target/release/bundle/dmg \
-maxdepth 1 -type f -name '*.dmg' | wc -l | tr -d ' '); \
if [ "$$dmg_count" -eq 0 ]; then \
echo "error: no DMG installer found in bundle output" >&2; \
exit 1; \
fi; \
find desktop/src-tauri/target/release/bundle/dmg \
-maxdepth 1 -type f -name '*.dmg' \
-exec cp {} $(DESKTOP_DIST_DIR)/macos/ \;; \
echo "Copied $$dmg_count DMG installer(s) to $(DESKTOP_DIST_DIR)/macos/"
# Build Windows NSIS installer bundle (.exe)
# Run on Windows runner/host.
desktop-windows-installer:
cd desktop && npm ci && npm run tauri:build:windows
mkdir -p $(DESKTOP_DIST_DIR)/windows
rm -f $(DESKTOP_DIST_DIR)/windows/*.exe
@bundle_dir="desktop/src-tauri/target/release/bundle/nsis"; \
target_triple="$${TAURI_ENV_TARGET_TRIPLE:-$${CARGO_BUILD_TARGET:-}}"; \
if [ -z "$$target_triple" ] && command -v rustc >/dev/null 2>&1; then \
host_triple=$$(rustc -vV | awk '/^host: /{print $$2}'); \
if [ "$$host_triple" = "aarch64-pc-windows-msvc" ]; then \
target_triple="$$host_triple"; \
fi; \
fi; \
if [ -n "$$target_triple" ] && \
[ -d "desktop/src-tauri/target/$$target_triple/release/bundle/nsis" ]; then \
bundle_dir="desktop/src-tauri/target/$$target_triple/release/bundle/nsis"; \
fi; \
if [ ! -d "$$bundle_dir" ]; then \
echo "error: Windows bundle output directory not found: $$bundle_dir" >&2; \
exit 1; \
fi; \
exe_count=$$(find "$$bundle_dir" \
-maxdepth 1 -type f -name '*.exe' | wc -l | tr -d ' '); \
if [ "$$exe_count" -eq 0 ]; then \
echo "error: no Windows installer (.exe) found in $$bundle_dir" >&2; \
exit 1; \
fi; \
find "$$bundle_dir" \
-maxdepth 1 -type f -name '*.exe' \
-exec cp {} $(DESKTOP_DIST_DIR)/windows/ \;; \
echo "Copied $$exe_count Windows installer(s) to $(DESKTOP_DIST_DIR)/windows/"
# Build Linux AppImage bundle
# Run on a Linux host.
desktop-linux-appimage:
cd desktop && npm ci && npm run tauri:build:linux \
$(if $(TAURI_SIGNING_PRIVATE_KEY),,-- --config '{"bundle":{"createUpdaterArtifacts":false}}')
cd desktop && bash scripts/repair-appimage-diricon.sh \
src-tauri/target/release/bundle/appimage/*.AppImage
mkdir -p $(DESKTOP_DIST_DIR)/linux
rm -f $(DESKTOP_DIST_DIR)/linux/*.AppImage
@ai_count=$$(find desktop/src-tauri/target/release/bundle/appimage \
-maxdepth 1 -type f -name '*.AppImage' | wc -l | tr -d ' '); \
if [ "$$ai_count" -eq 0 ]; then \
echo "error: no AppImage found in bundle output" >&2; \
exit 1; \
fi; \
find desktop/src-tauri/target/release/bundle/appimage \
-maxdepth 1 -type f -name '*.AppImage' \
-exec cp {} $(DESKTOP_DIST_DIR)/linux/ \;; \
echo "Copied $$ai_count AppImage(s) to $(DESKTOP_DIST_DIR)/linux/"
# Backward-compatible alias (macOS .app)
desktop-app: desktop-macos-app
# Keep local package/test-process fan-out bounded. A plain `go test ./...`
# otherwise defaults to GOMAXPROCS, which can launch a large number of fresh
# test binaries at once on a developer machine. Override with `GO_TEST_P=`
# when an operator intentionally wants the Go default.
GO_TEST_P ?= 4
GO_TEST_P_FLAG := $(if $(GO_TEST_P),-p $(GO_TEST_P),)
# Run the cacheable unit/integration suite. The external-service lanes below
# intentionally retain `-count=1` because they require fresh state.
test: pricing-snapshot ensure-embed-dir
go test $(GO_TEST_P_FLAG) -tags "fts5" ./... -v
# Reproducible synthetic ingest, steady-state sync, and analytics measurements.
.PHONY: perf-sim
PERF_SIM_FLAGS ?=
perf-sim: pricing-snapshot ensure-embed-dir
CGO_ENABLED=1 go run -buildvcs=true -tags fts5 ./cmd/perfsim $(PERF_SIM_FLAGS)
# Run fast tests only
test-short: pricing-snapshot ensure-embed-dir
go test $(GO_TEST_P_FLAG) -tags "fts5" ./... -short
# Run the quarantined eval-ingest endpoint tests under their build tag.
# Only internal/server contains evalingest-gated code; every other package is
# identical under the tag and already covered by the untagged run.
test-evalingest: pricing-snapshot ensure-embed-dir
CGO_ENABLED=1 go test -tags "fts5,evalingest" \
./internal/server -v -count=1
# Compare db.Store read-query performance across SQLite, DuckDB, and PostgreSQL.
# Requires Docker because the PostgreSQL backend is started with testcontainers.
BENCH_BACKENDS_FLAGS ?= -bench . -run '^$$' -benchmem
BENCH_BACKENDS_SESSIONS ?= 1000
BENCH_BACKENDS_MESSAGES_PER_SESSION ?= 64
bench-backends: pricing-snapshot ensure-embed-dir
AGENTSVIEW_BENCH_SESSIONS=$(BENCH_BACKENDS_SESSIONS) \
AGENTSVIEW_BENCH_MESSAGES_PER_SESSION=$(BENCH_BACKENDS_MESSAGES_PER_SESSION) \
CGO_ENABLED=1 go test -tags "fts5,benchdb" ./internal/backendbench $(BENCH_BACKENDS_FLAGS)
# Local hot-path benchmark comparison. Runs every benchmark in these packages
# (sync engine warm/cold/append, message write paths, usage
# aggregation, secret scanning, signal analysis). For a local comparison,
# run this target before and after a change, then compare the outputs with
# `go run ./cmd/benchgate -old old.txt -new new.txt`.
BENCH_GATE_PACKAGES ?= ./internal/sync ./internal/db ./internal/secrets \
./internal/signals
# Count must stay >= 5: benchgate's time gate needs at least 5
# candidate samples for its significance test. Every -count run
# rebuilds each benchmark's fixture, so this is also the multiplier
# on the 100k-row seeds below.
BENCH_GATE_COUNT ?= 5
# Fixed iterations, not a duration: some gated benchmarks grow their
# fixture as they iterate, so baseline and candidate must run the
# same iteration count to measure identical workloads.
BENCH_GATE_TIME ?= 20x
# Benchmarks whose single iteration costs hundreds of milliseconds to
# seconds (100k-row usage and activity-report fixtures, cold-archive
# ingest) run in a second pass with fewer iterations. Their per-op
# ratios are stable at that scale, and at BENCH_GATE_TIME these few
# benchmarks were most of the gate's wall clock. The regex is matched
# against top-level benchmark names, so sub-benchmarks follow their
# parent; the cheap benchmarks keep the full iteration count because
# their millisecond-scale samples are what needs the averaging.
BENCH_GATE_HEAVY ?= GetDailyUsage|UsageRollup|SQLiteActivityReport|SyncAllColdArchive|ResyncBulk|CopyMixedLegacyPageOrphans|CopySessionMetadataPins
BENCH_GATE_HEAVY_TIME ?= 5x
bench-gate: pricing-snapshot ensure-embed-dir
CGO_ENABLED=1 go test -tags "fts5" -run '^$$' \
-bench . -skip '$(BENCH_GATE_HEAVY)' -benchmem \
-count $(BENCH_GATE_COUNT) -benchtime $(BENCH_GATE_TIME) \
-timeout 25m $(BENCH_GATE_PACKAGES)
CGO_ENABLED=1 go test -tags "fts5" -run '^$$' \
-bench '$(BENCH_GATE_HEAVY)' -benchmem \
-count $(BENCH_GATE_COUNT) -benchtime $(BENCH_GATE_HEAVY_TIME) \
-timeout 25m $(BENCH_GATE_PACKAGES)
# Prints the sample/iteration configuration in shell-evalable form.
# Pass these values to the baseline `make bench-gate` invocation so both
# sides measure identical workloads even when a change updates the defaults
# above (the package list intentionally stays per-side).
bench-gate-config:
@echo "BENCH_GATE_COUNT=$(BENCH_GATE_COUNT) BENCH_GATE_TIME=$(BENCH_GATE_TIME)"
@echo "BENCH_GATE_HEAVY='$(BENCH_GATE_HEAVY)' BENCH_GATE_HEAVY_TIME=$(BENCH_GATE_HEAVY_TIME)"
# Start test PostgreSQL container
postgres-up:
docker compose -f docker-compose.test.yml up -d --wait
# Run opt-in PostgreSQL usage benchmarks against the pinned PG16 service.
bench-pg-usage: pricing-snapshot
docker compose -f docker-compose.test.yml up -d --wait postgres
TEST_PG_URL="postgres://agentsview_test:agentsview_test_password@localhost:5433/agentsview_test?sslmode=disable" \
CGO_ENABLED=1 go test -tags "fts5,pgtest" ./internal/postgres \
-run '^$$' -bench 'BenchmarkPGUsage(Read|Refresh)' -benchmem -count=5
# Stop test PostgreSQL container
postgres-down:
docker compose -f docker-compose.test.yml down
# Run PostgreSQL integration tests (starts postgres automatically)
test-postgres: pricing-snapshot ensure-embed-dir postgres-up
@echo "Waiting for postgres to be ready..."
@sleep 2
TEST_PG_URL="postgres://agentsview_test:agentsview_test_password@localhost:5433/agentsview_test?sslmode=disable" \
CGO_ENABLED=1 go test -tags "fts5,pgtest" -v ./cmd/agentsview \
-run 'TestRawSync.*CleanUploads|TestRawSyncBackfill|TestRawProjectionDialogueRolloutRequeuesCompletedProductionVersion|TestHostedRuntimeHealthRequiresStatusToken|TestReplicaRecipeQueryWithoutConfig|TestReplicaServeWatchPublication' -count=1
TEST_PG_URL="postgres://agentsview_test:agentsview_test_password@localhost:5433/agentsview_test?sslmode=disable" \
CGO_ENABLED=1 go test -tags "fts5,pgtest" -v ./internal/postgres/... ./internal/activity/... -count=1 -timeout=20m
TEST_PG_URL="postgres://agentsview_test:agentsview_test_password@localhost:5433/agentsview_test?sslmode=disable" \
CGO_ENABLED=1 go test -tags "fts5,pgtest" -v ./internal/service -run '^TestPlanUsageCountsParity$$' -count=1
# PostgreSQL integration tests for CI (postgres already running as service)
POSTGRES_CORE_HEAVY_TEST := TestRawProjectionProofLookupsStayBounded
test-postgres-ci:
$(MAKE) -j1 test-postgres-raw-sync-ci test-postgres-core-ci
test-postgres-raw-sync-ci: pricing-snapshot ensure-embed-dir
CGO_ENABLED=1 go test -tags "fts5,pgtest" -v ./cmd/agentsview \
-run 'TestRawSync.*CleanUploads|TestRawSyncBackfill|TestRawProjectionDialogueRolloutRequeuesCompletedProductionVersion|TestHostedRuntimeHealthRequiresStatusToken|TestReplicaRecipeQueryWithoutConfig|TestReplicaServeWatchPublication' -count=1
test-postgres-core-ci:
$(MAKE) -j1 test-postgres-core-fast-ci test-postgres-core-heavy-ci
test-postgres-core-fast-ci: pricing-snapshot ensure-embed-dir
CGO_ENABLED=1 go test -tags "fts5,pgtest" -v ./internal/postgres/... ./internal/activity/... -skip '^$(POSTGRES_CORE_HEAVY_TEST)$$' -count=1 -timeout=20m
CGO_ENABLED=1 go test -tags "fts5,pgtest" -v ./internal/service -run '^TestPlanUsageCountsParity$$' -count=1
test-postgres-core-heavy-ci: pricing-snapshot ensure-embed-dir
CGO_ENABLED=1 go test -tags "fts5,pgtest" ./internal/postgres/... -list '^$(POSTGRES_CORE_HEAVY_TEST)$$' | grep -Fx '$(POSTGRES_CORE_HEAVY_TEST)'
CGO_ENABLED=1 go test -tags "fts5,pgtest" -v ./internal/postgres/... -run '^$(POSTGRES_CORE_HEAVY_TEST)$$' -count=1 -timeout=20m
# Start test ClickHouse container (native 19000, HTTP 18123)
clickhouse-up:
docker compose -f docker-compose.test.yml up -d --wait clickhouse
# Stop test ClickHouse container
clickhouse-down:
docker compose -f docker-compose.test.yml down clickhouse
# Run ClickHouse integration tests (starts clickhouse automatically)
test-clickhouse: pricing-snapshot ensure-embed-dir clickhouse-up
@echo "Waiting for clickhouse to be ready..."
@sleep 2
TEST_CLICKHOUSE_URL="clickhouse://localhost:19000/default" \
CGO_ENABLED=1 go test -tags "fts5,chtest" -v ./internal/clickhouse/... ./internal/activity/... -count=1 -timeout=20m
TEST_CLICKHOUSE_URL="clickhouse://localhost:19000/default" \
CGO_ENABLED=1 go test -tags "fts5,chtest" -v ./internal/service -run '^TestPlanUsageCountsParity$$' -count=1
# ClickHouse integration tests for CI (clickhouse already running as service)
test-clickhouse-ci: pricing-snapshot ensure-embed-dir
CGO_ENABLED=1 go test -tags "fts5,chtest" -v ./internal/clickhouse/... ./internal/activity/... -count=1 -timeout=20m
CGO_ENABLED=1 go test -tags "fts5,chtest" -v ./internal/service -run '^TestPlanUsageCountsParity$$' -count=1
# S3 discovery integration tests. testcontainers starts and tears down a
# rustfs (S3-compatible) container automatically, so only a working Docker
# daemon is required.
test-s3: pricing-snapshot ensure-embed-dir
CGO_ENABLED=1 go test -tags "fts5,s3test" -v ./internal/sync/... -run TestS3 -count=1
# Local E2E builds enable the mapping workspace. Prebuilt servers must opt in
# because their embedded frontend may not include the workspace yet.
PROJECT_MAPPING_WORKSPACE_E2E_ENABLED ?= $(if $(E2E_PREBUILT_SERVER),,true)
# Run Playwright E2E tests
e2e:
cd frontend && \
PROJECT_MAPPING_WORKSPACE_E2E_ENABLED="$(PROJECT_MAPPING_WORKSPACE_E2E_ENABLED)" \
npx playwright test
# Run focused Playwright smoke tests against duckdb serve.
e2e-duckdb:
cd frontend && AGENTSVIEW_E2E_BACKEND=duckdb \
PROJECT_MAPPING_WORKSPACE_E2E_ENABLED="$(PROJECT_MAPPING_WORKSPACE_E2E_ENABLED)" \
npx playwright test \
e2e/duckdb-backend.spec.ts e2e/data-mode.spec.ts \
e2e/session-list.spec.ts --project=chromium
# Run the opt-in native-client conversation-memory release gate. This uses the
# caller's authenticated Claude Code and Codex installations and writes traces
# only under an ignored .test-data-memory-e2e-* directory.
MEMORY_E2E_ARGS ?=
memory-e2e: pricing-snapshot ensure-embed-dir
go run ./scripts/memory-e2e --live $(MEMORY_E2E_ARGS)
# Vet
vet: pricing-snapshot ensure-embed-dir
go vet -tags fts5 ./...
# Lint Go code and auto-fix where possible (local development)
lint: lint-config-check lint-sql lint-golangci nilaway
# Run golangci-lint with auto-fixes for local development.
lint-golangci: pricing-snapshot ensure-embed-dir nilaway-golangci-build
@if ! command -v golangci-lint >/dev/null 2>&1; then \
echo "golangci-lint not found. Install with: make lint-tools" >&2; \
exit 1; \
fi
$(CUSTOM_GCL) run --fix ./...
# Lint Go code without fixing (for CI)
lint-ci: lint-config-check lint-sql lint-golangci-ci nilaway
# Run golangci-lint without auto-fixes for CI.
lint-golangci-ci: pricing-snapshot ensure-embed-dir nilaway-golangci-build
@if ! command -v golangci-lint >/dev/null 2>&1; then \
echo "golangci-lint not found. Install with: make lint-tools" >&2; \
exit 1; \
fi
$(CUSTOM_GCL) run ./...
.PHONY: lint-config-check lint-sql
lint-sql:
go run go.kenn.io/kit/cmd/kennlint@v0.34.1 sql internal/db/schema.sql
lint-config-check:
go run go.kenn.io/kit/cmd/kennlint@v0.34.1 config -check
# Build a custom golangci-lint binary with the kit and NilAway module plugins.
# Strip every repo-local Git env var (GIT_DIR, GIT_INDEX_FILE,
# GIT_CONFIG_PARAMETERS, etc.) and disable VCS stamping so the inner
# `git clone` and `go build` don't inherit the parent repo's state.
# When `make nilaway` runs from a pre-commit hook, git exports those
# vars pointing at the parent repo, which makes the clone and the
# VCS-stamped build fail with exit 128. The list comes from
# `git rev-parse --local-env-vars` so it tracks whatever Git considers
# repo-local at runtime. An existing binary is reused while it is newer than
# .custom-gcl.yml, which pins its plugins, and reports GOLANGCI_LINT_VERSION.
nilaway-golangci-build:
@if [ -x "$(CUSTOM_GCL)$(GOEXE)" ] && [ "$(CUSTOM_GCL)$(GOEXE)" -nt .custom-gcl.yml ]; then case "$$($(CUSTOM_GCL) version --short 2>/dev/null)" in "$(GOLANGCI_LINT_VERSION)"-custom-gcl-*) exit 0;; esac; fi; \
if ! command -v golangci-lint >/dev/null 2>&1; then \
echo "golangci-lint not found. Install with: make lint-tools" >&2; \
exit 1; \
fi; \
unset_args=$$(git rev-parse --local-env-vars 2>/dev/null | sed 's/^/-u /' | tr '\n' ' '); \
env $$unset_args GOFLAGS=-buildvcs=false \
golangci-lint custom --version "$(GOLANGCI_LINT_VERSION)" --name custom-gcl
# Run NilAway through the custom golangci-lint module plugin.
#
# NilAway is run once per package instead of once over ./... because a single
# whole-module run blows up memory. Each invocation therefore keeps its own
# tight caps (GOMAXPROCS=1, GOGC=10, GOMEMLIMIT=512MiB). Those caps are
# per-process, so the ~45 invocations can safely overlap: NILAWAY_JOBS of them
# run at a time, bounding peak usage at roughly NILAWAY_JOBS x GOMEMLIMIT.
# Concurrent invocations share one GOLANGCI_LINT_CACHE, so they need
# --allow-parallel-runners; without it golangci-lint's start-up file lock makes
# every overlapping run fail with "parallel golangci-lint is running".
#
# Each invocation writes its output to its own scratch file, which the parent
# prints in package order once the run finishes. Writing straight to stdout
# would interleave one package's findings with another's as soon as a report
# exceeded the pipe's atomic-write size. NILAWAY_JOBS must be a positive
# integer: `xargs -P 0` means "unlimited" and would remove the memory bound
# this whole arrangement depends on.
NILAWAY_JOBS ?= 4
# Compile exports once before the workers apply their CPU and memory limits.
nilaway: pricing-snapshot ensure-embed-dir nilaway-golangci-build
@set -e; \
case "$(NILAWAY_JOBS)" in \
''|*[!0-9]*) \
echo "nilaway: NILAWAY_JOBS must be a positive integer (got '$(NILAWAY_JOBS)')" >&2; \
exit 1;; \
esac; \
njobs=$$(( $(NILAWAY_JOBS) + 0 )); \
if [ "$$njobs" -lt 1 ]; then \
echo "nilaway: NILAWAY_JOBS must be at least 1 (got '$(NILAWAY_JOBS)')" >&2; \
exit 1; \
fi; \
root=$$(pwd); \
dirs=$$(go list -export -f '{{.Dir}}' ./...); \
pkgs=$$(for dir in $$dirs; do \
if [ "$$dir" = "$$root" ]; then \
printf '%s\n' "."; \
else \
printf '%s\n' "./$${dir#$$root/}"; \
fi; \
done); \
if [ -z "$$pkgs" ]; then echo "nilaway: no packages to lint" >&2; exit 1; fi; \
NILAWAY_OUT_DIR=$$(mktemp -d); \
export NILAWAY_OUT_DIR; \
trap 'rm -f "$$NILAWAY_OUT_DIR"/*; rmdir "$$NILAWAY_OUT_DIR"' EXIT HUP INT TERM; \
set +e; \
printf '%s\n' "$$pkgs" | awk '{ printf "%d:%s\n", NR, $$0 }' | tr '\n' '\0' \
| xargs -0 -n 1 -P "$$njobs" sh -c ' \
n=$${1%%:*}; \
pkg=$${1#*:}; \
cmd="$(CUSTOM_GCL) run --allow-parallel-runners --config .golangci.nilaway.yml $$pkg"; \
out=$$(GOMAXPROCS=$${GOMAXPROCS:-1} GOGC=$${GOGC:-10} GOMEMLIMIT=$${GOMEMLIMIT:-512MiB} \
$(CUSTOM_GCL) run --allow-parallel-runners \
--config .golangci.nilaway.yml "$$pkg" 2>&1); \
status=$$?; \
{ printf "%s\n" "$$cmd"; \
if [ -n "$$out" ]; then printf "%s\n" "$$out"; fi; \
} > "$$NILAWAY_OUT_DIR/$$n"; \
exit $$status' sh; \
rc=$$?; \
set -e; \
ls "$$NILAWAY_OUT_DIR" | sort -n | while IFS= read -r n; do \
cat "$$NILAWAY_OUT_DIR/$$n"; \
done; \
exit $$rc
# Install pinned local lint tools.
lint-tools:
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)
# Tidy dependencies
tidy: pricing-snapshot
go mod tidy
# Clean build artifacts
clean:
rm -f agentsview agentsv
rm -f $(PRICING_SNAPSHOT_FILE)
rm -rf internal/web/dist dist/ tmp/ $(SQLITE_INCLUDE_DIR)
mkdir -p internal/web/dist
printf '%s\n' \
'keep embed dir for generated frontend assets' \
> internal/web/dist/.keep
docs-install:
cd docs && uv sync --frozen --no-dev
docs-build:
cd docs && uv run --frozen bash ./vercel-build.sh
docs-serve:
cd docs && bash assets/hydrate-assets.sh && uv run bash ./zensical-docs.sh serve
docs-preview:
python3 -m http.server 8000 --directory docs/site
docs-check:
bash scripts/check-docs.sh
docs-screenshots:
bash docs/screenshots/run.sh
docs-assets-branch:
bash docs/assets/update-static-assets-branch.sh
docs-generated-assets-branch:
bash docs/screenshots/update-generated-assets-branch.sh
docs-deploy-staging:
vercel deploy docs
docs-deploy:
vercel deploy docs --prod
# Build release binary for current platform (CGO required for sqlite3)
release: pricing-snapshot frontend
mkdir -p dist
CGO_ENABLED=1 go build -tags fts5 \
-ldflags="$(LDFLAGS_RELEASE)" -trimpath \
-o dist/agentsview-$$(go env GOOS)-$$(go env GOARCH) ./cmd/agentsview
# Cross-compile targets (require CC set to target cross-compiler)
release-darwin-arm64: pricing-snapshot frontend
mkdir -p dist
GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build -tags fts5 \
-ldflags="$(LDFLAGS_RELEASE)" -trimpath \
-o dist/agentsview-darwin-arm64 ./cmd/agentsview
release-darwin-amd64: pricing-snapshot frontend
mkdir -p dist
GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 go build -tags fts5 \
-ldflags="$(LDFLAGS_RELEASE)" -trimpath \
-o dist/agentsview-darwin-amd64 ./cmd/agentsview
release-linux-amd64: pricing-snapshot frontend
mkdir -p dist
GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -tags fts5 \
-ldflags="$(LDFLAGS_RELEASE)" -trimpath \
-o dist/agentsview-linux-amd64 ./cmd/agentsview
# Install pre-commit and pre-push hooks via prek
install-hooks:
@if ! command -v prek >/dev/null 2>&1; then \
echo "prek not found. Install with: brew install prek" >&2; \
exit 1; \
fi
prek install -f
# Show help
help:
@echo "agentsview build targets:"
@echo ""
@echo " build - Build with embedded frontend"
@echo " build-release - Release build (optimized, stripped)"
@echo " homebrew - Release build for Homebrew (BINDIR=dir, DISTRIBUTION=homebrew)"
@echo " pricing-snapshot - Restore LiteLLM snapshot from artifact branch"
@echo " install - Build and install to ~/.local/bin or GOPATH"
@echo " install-cjk-fts - Install agentsview with the optional CJK FTS sidecar"
@echo " simple-fts - Build the pinned simple/cppjieba SQLite extension"
@echo ""
@echo " dev - Run Go server with live reload via air (use with frontend-dev)"
@echo " dev-snapshot - Run agentsview against a fresh snapshot of prod sessions.db"
@echo " air-install - Install air for backend live reload"
@echo " frontend - Build frontend SPA"
@echo " frontend-dev - Run Vite+ dev server"
@echo " desktop-dev - Run Tauri desktop wrapper in dev mode"
@echo " desktop-build - Build Tauri desktop app bundles"
@echo " desktop-macos-app - Build macOS .app bundle only"
@echo " desktop-macos-dmg - Build macOS DMG installer"
@echo " desktop-windows-installer - Build Windows NSIS installer"
@echo " desktop-linux-appimage - Build Linux AppImage"
@echo " desktop-app - Alias for desktop-macos-app"
@echo ""
@echo " test - Run all tests"
@echo " test-short - Run fast tests only"
@echo " bench-backends - Benchmark SQLite, DuckDB, and PostgreSQL stores"
@echo " bench-pg-usage - Run opt-in PostgreSQL usage benchmarks against PG16"
@echo " bench-gate - Run hot-path benchmarks for local comparison"
@echo " test-postgres - Run PostgreSQL integration tests"
@echo " test-clickhouse - Run ClickHouse integration tests"
@echo " test-s3 - Run S3 discovery integration tests (Docker)"
@echo " postgres-up - Start test PostgreSQL container"
@echo " postgres-down - Stop test PostgreSQL container"
@echo " clickhouse-up - Start test ClickHouse container"
@echo " clickhouse-down - Stop test ClickHouse container"
@echo " e2e - Run Playwright E2E tests"
@echo " e2e-duckdb - Run DuckDB-backed Playwright smoke tests"
@echo " memory-e2e - Run opt-in Claude Code and Codex memory recall gate"
@echo " vet - Run go vet"
@echo " lint - Run golangci-lint and NilAway (auto-fix golangci issues)"
@echo " lint-ci - Run golangci-lint and NilAway (no fix, for CI)"
@echo " lint-golangci - Run golangci-lint with auto-fix"
@echo " nilaway - Run NilAway through custom golangci-lint"
@echo " lint-tools - Install pinned lint tools"
@echo " tidy - Tidy go.mod"
@echo ""
@echo " release - Release build for current platform"
@echo " clean - Remove build artifacts"
@echo " install-hooks - Install pre-commit and pre-push git hooks"