BINARY := rabbithole # A config file to import on start (optional; settings live in the store). CONFIG ?= CONFIG_FLAG := $(if $(CONFIG),--config $(CONFIG),) PKG := ./... NO_THINK ?= THINK_FLAG := $(if $(NO_THINK),--no-think,) ADDR ?= 127.0.0.1:8080 DEBUG ?= DEBUG_FLAG := $(if $(DEBUG),--debug,) DEV ?= DEV_FLAG := $(if $(DEV),--dev,) GOLDEN ?= ./docs/examples/golden.yaml # Repo work keeps its store here, so it never touches the personal one in the user # data dir and never sees the legacy-location warning. A RABBITHOLE_DB in the # shell is respected (the binary reads it), and DB= on the command line wins over # both. make does not read .env; the binary does. DB ?= ./data/rabbithole.db ENV_DB := $(and $(filter file,$(origin DB)),$(RABBITHOLE_DB)) DB_FLAG := $(if $(ENV_DB),,--db '$(value DB)') DB_FROM := $(if $(ENV_DB),from your shell environment,$(if $(filter file,$(origin DB)),the repo development default,from DB=)) FORMAT ?= text # Nearest git tag plus any commits past it. VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) LDFLAGS := -X github.com/DanielBlei/rabbithole/cmd.version=$(VERSION) .DEFAULT_GOAL := help ##@ General .PHONY: help help: ## Display this help. @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST) # Says where a make target keeps its data, and how to change it. USER_DATA_DIR := $(if $(filter Darwin,$(shell uname -s)),~/Library/Application Support/rabbithole,~/.local/share/rabbithole) .PHONY: db-hint db-hint: @printf '\033[33m▌\033[0m make keeps your data in \033[36m$(if $(ENV_DB),the store RABBITHOLE_DB names,$(DB))\033[0m ($(DB_FROM))\n' @printf '\033[33m▌\033[0m Use another local store: \033[36mmake $(or $(firstword $(MAKECMDGOALS)),serve) DB=/path/to/rabbithole.db\033[0m\n' @printf '\033[33m▌\033[0m Use Postgres: \033[36mmake $(or $(firstword $(MAKECMDGOALS)),serve) DB=postgres://user@host:5432/rabbithole\033[0m\n' @printf '\033[33m▌\033[0m with \033[36mRABBITHOLE_DB_PASSWORD\033[0m set in .env or your shell profile\n' @printf '\033[33m▌\033[0m (a password in the URL is accepted, but not recommended)\n' @printf '\033[33m▌\033[0m Run without make (prod): \033[36m./$(BINARY)\033[0m uses \033[36m$(USER_DATA_DIR)\033[0m, or RABBITHOLE_DB (URL/Path) when set\n' .PHONY: setup setup: ## Create .env from .env.example (if missing); the binary reads it itself @test -f .env || (cp .env.example .env && chmod 600 .env && echo "created .env (the binary reads it; RABBITHOLE_* keys only)") ##@ Build .PHONY: build build: ## Compile the binary to ./rabbithole go build -ldflags "$(LDFLAGS)" -o $(BINARY) . .PHONY: tidy tidy: ## Sync go.mod/go.sum go mod tidy .PHONY: clean clean: ## Remove build artifacts (DATA=1 also deletes ./data: the database and digests) rm -f $(BINARY) coverage.out @if [ -n "$(DATA)" ] && [ -d data ]; then \ printf 'Delete ./data (database, digests)? This cannot be undone. [y/N] '; \ read ans; case "$$ans" in [yY]*) rm -rf data; echo "removed ./data";; *) echo "kept";; esac; \ fi .PHONY: clean-feeds clean-feeds: ## Delete every feed item from the store, keeping todos, ideas and run history (CONFIG optional) @printf 'Delete every feed item from the store?\n' @printf 'Bookmarks, ratings and notes go too. Todos, ideas and run history are kept.\n' @printf 'The next ingest re-fetches and re-scores anything still in your feeds. [y/N] ' @read ans; case "$$ans" in \ [yY]*) go run . items prune --all --include-saved $(DB_FLAG) $(CONFIG_FLAG);; \ *) echo "kept";; \ esac ##@ ingest .PHONY: sources sources: db-hint build ## Add the example sources (docs/examples/feeds.yaml) to the store; repeat-safe ./$(BINARY) sources import docs/examples/feeds.yaml $(DB_FLAG) .PHONY: ingest ingest: db-hint ## Fetch, rank, and record to the store (CONFIG optional, NO_THINK=1 to disable thinking) go run . ingest $(DB_FLAG) $(CONFIG_FLAG) $(THINK_FLAG) $(DEBUG_FLAG) .PHONY: dry-run dry-run: db-hint ## Print the ingest to stdout without writing files or recording items go run . ingest $(DB_FLAG) $(CONFIG_FLAG) --dry-run $(THINK_FLAG) .PHONY: heuristic heuristic: db-hint ## Offline ingest with the model-free keyword scorer (no Ollama needed) go run . ingest $(DB_FLAG) $(CONFIG_FLAG) --provider heuristic $(THINK_FLAG) ##@ Eval .PHONY: eval eval: ## Benchmark scoring against GOLDEN (GOLDEN, FORMAT; CONFIG optional) go run . eval benchmark $(DB_FLAG) $(GOLDEN) $(CONFIG_FLAG) --format $(FORMAT) $(THINK_FLAG) .PHONY: eval-example eval-example: ## Benchmark the shipped example dataset with the model-free scorer (no Ollama needed) go run . eval benchmark $(DB_FLAG) docs/examples/golden.yaml $(CONFIG_FLAG) --provider heuristic .PHONY: eval-smoke eval-smoke: ## Render every report format from the example dataset, model-free @for format in text markdown json; do \ printf '\033[36m==> %s\033[0m\n' "$$format"; \ go run . eval benchmark $(DB_FLAG) docs/examples/golden.yaml --config docs/examples/config.yaml \ --provider heuristic --format "$$format" > /dev/null || exit 1; \ done @go run . eval benchmark $(DB_FLAG) docs/examples/golden.yaml --config docs/examples/config.yaml \ --provider heuristic | awk 'length > 120 { print "line over 120 columns: " $$0; fail=1 } END { exit fail }' ##@ Serve .PHONY: serve serve: db-hint build ## Serve the web UI and items API over HTTP (CONFIG optional, ADDR; DEBUG=1 for verbose ingest logging, DEV=1 to serve assets no-cache) ./$(BINARY) serve $(DB_FLAG) $(CONFIG_FLAG) --addr $(ADDR) $(DEBUG_FLAG) $(DEV_FLAG) ##@ Test .PHONY: test test: ## Run the test suite @RABBITHOLE_DB= RABBITHOLE_DB_PASSWORD= go test $(PKG) .PHONY: test-v test-v: ## Run the test suite verbosely @RABBITHOLE_DB= RABBITHOLE_DB_PASSWORD= go test -v $(PKG) .PHONY: test-race test-race: ## Run tests with the race detector @RABBITHOLE_DB= RABBITHOLE_DB_PASSWORD= go test -race $(PKG) .PHONY: bench bench: ## Run ranking and store benchmarks @RABBITHOLE_DB= RABBITHOLE_DB_PASSWORD= go test -bench=. -benchmem ./internal/rank ./internal/store .PHONY: cover cover: ## Run tests and open an HTML coverage report @RABBITHOLE_DB= RABBITHOLE_DB_PASSWORD= go test -coverprofile=coverage.out $(PKG) @RABBITHOLE_DB= RABBITHOLE_DB_PASSWORD= go tool cover -html=coverage.out # The suite runs on SQLite by default and needs nothing installed. These three # run the same tests a second time against Postgres, which is opt-in: `check` # does not depend on them, and neither does CI. .PHONY: pg-up pg-up: ## Start a throwaway Postgres for the store tests @./scripts/dev-postgres.sh up .PHONY: pg-down pg-down: ## Stop the throwaway Postgres and drop its data @./scripts/dev-postgres.sh down .PHONY: test-pg test-pg: ## Run the store suite against the throwaway Postgres @./scripts/dev-postgres.sh status >/dev/null || { echo "not running; start it with 'make pg-up'" >&2; exit 1; } @RABBITHOLE_DB= RABBITHOLE_TEST_POSTGRES="$$(./scripts/dev-postgres.sh dsn)" go test -count=1 ./internal/store/... ##@ Quality .PHONY: vet vet: ## Run go vet go vet $(PKG) .PHONY: fmt fmt: ## Format all Go source gofmt -w -s . NEED_GOLANGCI = command -v golangci-lint >/dev/null 2>&1 || { echo "golangci-lint is required (CI runs it)."; echo "install: https://golangci-lint.run/welcome/install/"; exit 1; } .PHONY: lint lint: ## Run golangci-lint (also covers gofmt/goimports/golines and vet) @$(NEED_GOLANGCI) golangci-lint run .PHONY: lint-fix lint-fix: ## Run golangci-lint with --fix (formats + autofixes lint issues) @$(NEED_GOLANGCI) golangci-lint run --fix # Unpinned: a stale scanner defeats the purpose. GOVULNCHECK ?= go run golang.org/x/vuln/cmd/govulncheck@latest .PHONY: vulncheck vulncheck: ## Scan dependencies for known vulnerabilities $(GOVULNCHECK) $(PKG) .PHONY: check check: lint test-race build ## The gate CI runs (run before committing; needs golangci-lint) ##@ Library # library-check is offline; `check` covers it through test-race. library-urls and # library-icons need the network, so they are not part of `check`. CI runs # library-urls when library paths change and weekly (.github/workflows/library.yml). # library-icons regenerates library/icons.json; library-check fails when a source # has no entry there. RABBITHOLE_LIBRARY_ICONS_ONLY_MISSING=1 fetches only sources # the file lacks and keeps every other entry as it is; RABBITHOLE_LIBRARY_ICONS_DROP=1 # lets a site that now has no icon lose the icon it had (otherwise it is kept). .PHONY: library-check library-check: ## Check library/ format, uniqueness and feeds.yaml parity (offline) @RABBITHOLE_DB= RABBITHOLE_DB_PASSWORD= go test -count=1 ./internal/library/... @RABBITHOLE_DB= RABBITHOLE_DB_PASSWORD= go test -count=1 -run TestLibrary ./internal/ingest/ ./internal/config/ .PHONY: library-urls library-urls: ## Fetch every library source through the app's fetchers (network) @RABBITHOLE_DB= RABBITHOLE_DB_PASSWORD= go test -tags=network -count=1 -timeout 20m -run TestLibrarySourcesLive ./internal/ingest/ .PHONY: library-icons library-icons: ## Fetch every library source's site icon and write library/icons.json (network) @RABBITHOLE_DB= RABBITHOLE_DB_PASSWORD= RABBITHOLE_LIBRARY_ICONS_WRITE=1 go test -tags=network -count=1 -timeout 20m -v -run TestLibraryIconsWrite ./internal/library/ ##@ Debug .PHONY: debug debug: ## Run the ingest with verbose logging go run . ingest $(DB_FLAG) $(CONFIG_FLAG) --debug $(THINK_FLAG) .PHONY: trace trace: ## Run the ingest with trace logging (raw model prompts/responses) go run . ingest $(DB_FLAG) $(CONFIG_FLAG) --trace $(THINK_FLAG) .PHONY: db-dump-items db-dump: ## Dump the items table via the sqlite3 CLI (uses DB) sqlite3 -header -column $(DB) "SELECT id, source, title, status, llm_score, user_score, digested_on, created_at FROM items ORDER BY created_at;"