# LazyBoy — developer Makefile
# Wraps docker compose + cargo so the whole stack can be built and launched
# with a few targets. See `make help`.

.DEFAULT_GOAL := help

COMPOSE        ?= docker compose
COMPUTER_IMAGE ?= lazyboy/computer:local
WEB_DIR        ?= apps/web
DATA_DIR       ?= ./data
BUILDX_BUILDER  ?= lazyboy
# PUSH=1 publishes the manifest list instead of writing an OCI archive.
MULTI_FLAGS ?=
$(if $(filter 1,$(PUSH)),$(eval MULTI_FLAGS := --push))

.PHONY: help env env-force \
        up logs ps health down purge \
        computer computer-multi images-multi postgres postgres-down pg-collation \
        cua-smoke prepare-screen-network \
        build build-api build-supervisor build-controld \
        fmt fmt-check clippy lint audit test clean \
        web \
        dev dev-supervisor dev-api

help: ## Show this help
	@echo "LazyBoy — make targets"
	@echo ""
	@echo "  Full stack (recommended, everything in Docker):"
	@echo "    make env            Create .env with generated tokens (idempotent)"
	@echo "    make up             Build all images + start postgres/computer/supervisor/api"
	@echo "    make health         Curl the API health endpoint"
	@echo "    make logs           Tail container logs"
	@echo "    make ps             Show container status"
	@echo "    make down           Stop + remove containers (keep postgres data)"
	@echo "    make purge          Stop + remove containers AND the postgres volume"
	@echo ""
	@echo "  Individual pieces:"
	@echo "    make computer       Build the heavy Debian desktop image (lazyboy/computer:local)"
	@echo "    make computer-multi Cross-build the desktop image for amd64 + arm64 (PUSH=1 to publish)"
	@echo "    make images-multi   Cross-build desktop + api + supervisor for amd64 + arm64"
	@echo "    make cua-smoke      Run Cua Driver smoke test in a disposable desktop container"
	@echo "    make postgres       Start only postgres (127.0.0.1:5434) and wait for ready"
	@echo "    make postgres-down  Stop postgres"
	@echo "    make pg-collation   Repair a Postgres collation version mismatch (see docs)"
	@echo ""
	@echo "  Local dev (postgres in Docker, Rust services on the host):"
	@echo "    make dev            Prep .env + postgres + computer image, then print run steps"
	@echo "    make dev-supervisor Run lazyboy-supervisor on the host (terminal 1)"
	@echo "    make dev-api        Run lazyboy-api on the host (terminal 2)"
	@echo ""
	@echo "  Rust / web:"
	@echo "    make build          cargo build --release (whole workspace)"
	@echo "    make build-api      cargo build --release -p lazyboy-api"
	@echo "    make fmt            cargo fmt --all"
	@echo "    make fmt-check      Report files rustfmt would change (legacy drift exists)"
	@echo "    make clippy         cargo clippy (deny warnings, reads clippy.toml)"
	@echo "    make lint           The Rust gate: clippy with -D warnings"
	@echo "    make audit          cargo deny: RustSec advisories, licenses, sources"
	@echo "    make test           cargo test --workspace (DB tests need: make postgres)"
	@echo "    make test-agent-computer  Screenshot / locator / native-file contract tests"
	@echo "    make web            Build the frontend in $(WEB_DIR) (needs node/npm)"
	@echo "    make clean          cargo clean"
	@echo ""
	@echo "  After 'make up': open http://127.0.0.1:3101, register an account, and add a model API key in 設定 → 模型."

# --- Environment -----------------------------------------------------------

env: ## Create .env with independent random secrets (preserve existing .env)
	@python3 scripts/init-env.py

env-force: ## Refuse destructive key regeneration; existing vault keys must be preserved
	@echo "Refusing to overwrite .env: this can orphan saved passwords. See docs/security-and-harness-review.md for safe rotation."
	@exit 1

# --- Full Docker stack -----------------------------------------------------

up: env ## Build every image and start the whole stack in Docker
	@echo "building images + starting stack (first run is slow: builds desktop + rust + web)..."
	@$(MAKE) --no-print-directory prepare-screen-network
	$(COMPOSE) up -d --build
	@echo ""
	@echo "stack launched. open http://127.0.0.1:3101 and register your account."
	$(COMPOSE) ps

# Compose owns `lazyboy_screen` (internal, labeled). A leftover from host-dev or
# an older supervisor has empty labels, and Compose v2+ then fails after the
# image build. Recreate only when nothing is attached.
prepare-screen-network:
	@set -a; [ -f .env ] && . ./.env; set +a; \
	network="$${LAZYBOY_SCREEN_NETWORK:-lazyboy_screen}"; \
	if ! docker network inspect "$$network" >/dev/null 2>&1; then exit 0; fi; \
	label=$$(docker network inspect -f '{{index .Labels "com.docker.compose.network"}}' "$$network"); \
	if [ "$$label" = "screen" ]; then exit 0; fi; \
	count=$$(docker network inspect -f '{{len .Containers}}' "$$network"); \
	if [ "$$count" != "0" ]; then \
	  echo "error: Docker network $$network exists without Compose labels and still has $$count container(s)." >&2; \
	  echo "Stop those containers, then: docker network rm $$network" >&2; \
	  exit 1; \
	fi; \
	echo "removing leftover Docker network $$network so Compose can recreate it"; \
	docker network rm "$$network"

logs: ## Tail logs for all services
	$(COMPOSE) logs -f

ps: ## Show container status
	$(COMPOSE) ps

health: ## Check the API health endpoint
	@curl -fsS http://127.0.0.1:3101/api/health && echo "  <- ok"

down: ## Stop and remove containers (keeps postgres volume)
	$(COMPOSE) down

purge: ## Stop containers and delete the postgres data volume
	$(COMPOSE) down -v

# --- Individual pieces -----------------------------------------------------

computer: ## Build the Debian desktop image used to spawn bot computers
	./scripts/build-image.sh --tag $(COMPUTER_IMAGE)

# Cross-builds every supported CPU architecture into one manifest list. Needs a
# docker-container builder + QEMU binfmt; both are bootstrapped by the script.
# PUSH=1 publishes to a registry, otherwise an OCI archive is written.
computer-multi: ## Cross-build the desktop image for all CPU architectures
	./scripts/build-image.sh --file image/computer/Dockerfile --multi $(MULTI_FLAGS)

# The api and supervisor images carry per-architecture binaries as well (ONNX
# Runtime, node, the distroless libc), so they get the same treatment as the
# desktop image instead of only ever existing for the build host.
images-multi: ## Cross-build every shipped image for all CPU architectures
	@for dockerfile in image/computer/Dockerfile image/supervisor/Dockerfile \
		image/api/Dockerfile; do \
	  echo "== $$dockerfile"; \
	  ./scripts/build-image.sh --file "$$dockerfile" --multi $(MULTI_FLAGS) || exit 1; \
	done

cua-smoke: computer ## Run the Cua Driver smoke test inside a disposable desktop container
	./scripts/cua-smoke-test.sh --docker --image $(COMPUTER_IMAGE)

postgres: ## Start only postgres and wait until it is ready
	$(COMPOSE) -f docker-compose.yml -f docker-compose.dev.yml up -d postgres
	@echo "waiting for postgres (127.0.0.1:5434) to be ready..."
	@for i in $$(seq 1 40); do if $(COMPOSE) exec -T postgres pg_isready -U lazyboy >/dev/null 2>&1; then echo "postgres ready"; break; fi; sleep 0.5; done

postgres-down: ## Stop postgres
	$(COMPOSE) down postgres

# A pgvector image rebuilt on another glibc leaves every database recording the old
# collation version; Postgres then refuses CREATE DATABASE and `cargo test` hangs on
# PoolTimedOut. Stop the api first, then reindex + refresh each database in place.
pg-collation: ## Repair a Postgres collation version mismatch after an image update
	$(COMPOSE) exec -T postgres psql -X -v ON_ERROR_STOP=1 -U lazyboy -d template1 -c "REINDEX DATABASE template1;" -c "ALTER DATABASE template1 REFRESH COLLATION VERSION;"
	$(COMPOSE) exec -T postgres psql -X -v ON_ERROR_STOP=1 -U lazyboy -d postgres -c "REINDEX DATABASE postgres;" -c "ALTER DATABASE postgres REFRESH COLLATION VERSION;"
	$(COMPOSE) exec -T postgres psql -X -v ON_ERROR_STOP=1 -U lazyboy -d lazyboy -c "REINDEX DATABASE lazyboy;" -c "ALTER DATABASE lazyboy REFRESH COLLATION VERSION;"

# --- Rust / web ------------------------------------------------------------

build: ## Release build of the whole workspace
	cargo build --release

build-api: ## Release build of the API binary
	cargo build --release -p lazyboy-api

build-supervisor: ## Release build of the supervisor binary
	cargo build --release -p lazyboy-supervisor

build-controld: ## Release build of the controld binary
	cargo build --release -p lazyboy-controld

fmt: ## Format all Rust code
	cargo fmt --all

fmt-check: ## Check formatting without touching files
	cargo fmt --all --check

# Lint policy lives in [workspace.lints] in the root Cargo.toml; the thresholds
# (e.g. too-many-arguments-threshold) live in clippy.toml, which cargo-clippy
# only reads from the directory it is started in - keep running this at the root.
clippy: ## Run clippy over the workspace, denying warnings
	cargo clippy --workspace --all-targets -- -D warnings

lint: clippy ## The Rust quality gate used by CI

# cargo-deny is a separate CLI: cargo install --locked cargo-deny
audit: ## Supply-chain check (RustSec advisories, licenses, dependency sources)
	@command -v cargo-deny >/dev/null 2>&1 || { \
	  echo "cargo-deny is not installed: cargo install --locked cargo-deny"; exit 1; }
	@echo "(advisories need the RustSec advisory DB, cloned on first run)"
	cargo deny check

test: ## Run the test suite
	cargo test --workspace

test-agent-computer: ## Agent-computer contract tests (screenshot, locators, native files, jobs, connectors)
	cargo test --workspace --lib -- \
	  should_deliver_observation_image \
	  classify_browser_locator \
	  native_file_payload \
	  file_bytes_prefer_base64 \
	  file_list_refuses \
	  fake_sandbox_keeps_invalid_utf8 \
	  pixel_actions_need_vision \
	  typed_browser_errors \
	  chat_tools_do_not_need_the_desktop \
	  plan_batch_modify \
	  classify_graph_item \
	  validate_manifest \
	  zip_slip \
	  pausing_agent \
	  same_operation \
	  journal_failure \
	  cancel_stops \
	  gmail_with_grant \
	  policy_denied \
	  slot_zero \
	  wait_counts \
	  canary_is_stripped \
	  stale_hash \
	  team_computers_share \
	  short_command_returns \
	  form_fill_and_computer_mcp \
	  form_locator_resolves \
	  begin_then_complete \
	  running_count_and_quota \
	  operation_id_is_not_part \
	  team_packages_live \
	  catalog_stdio_is_not_an_api_child \
	  native_exec_does_not_need_desktop \
	  revoke_drops_computer_mcp \
	  update_pins_old_version \
	  t59_keeps_ac \
	  native_tools_do_not_boot \
	  mcp_rs_does_not_spawn \
	  agent_computer_migration_does_not_unique \
	  native_exec_and_files_do_not_call_ensure_screen \
	  update_switches_the_single_ready_binding \
	  start_sh_skips_desktop \
	  background_exec_runs_on_the_computer \
	  computer_background_launch \
	  background_launch_keeps_posix_quoted \
	  exec_background_does_not_spawn \
	  reap_background_job_failed \
	  command_result_json_keeps \
	  supervisor_exec_returns_full
	cargo test -p lazyboy-api -- \
	  native_file_payload \
	  pixel_actions_need_vision \
	  typed_browser_errors \
	  native_exec_is_in_the_schema \
	  chat_tools_do_not_need_the_desktop \
	  form_fill_and_computer_mcp \
	  form_locator_resolves \
	  operation_id_is_not_part \
	  team_packages_live \
	  mcp_rs_does_not_spawn \
	  native_tools_do_not_boot \
	  agent_computer_migration_does_not_unique \
	  exec_background_does_not_spawn

web: ## Build the frontend (needs node/npm)
	cd $(WEB_DIR) && npm install && npm run build

clean: ## Remove cargo build artifacts
	cargo clean

# --- Local dev (postgres in Docker, Rust on the host) ----------------------

dev: env postgres computer ## Prep local dev, then print the two run commands
	@echo ""
	@echo "prep complete. run these in TWO terminals:"
	@echo "  terminal 1:  make dev-supervisor"
	@echo "  terminal 2:  make dev-api"
	@echo "then open http://127.0.0.1:3101"

dev-supervisor: ## Run lazyboy-supervisor on the host (terminal 1)
	@test -f .env || { echo "run 'make env' first"; exit 1; }
	@set -a; . ./.env; set +a; \
	 export DATA_DIR="$${DATA_DIR:-$$PWD/data}"; mkdir -p "$$DATA_DIR"; \
	 export SUPERVISOR_BIND="$${SUPERVISOR_BIND:-127.0.0.1:7091}"; \
	 export LAZYBOY_COMPUTER_IMAGE="$${LAZYBOY_COMPUTER_IMAGE:-$(COMPUTER_IMAGE)}"; \
	 echo "starting supervisor on $$SUPERVISOR_BIND (log: stderr)"; \
	 exec cargo run -p lazyboy-supervisor

dev-api: ## Run lazyboy-api on the host (terminal 2)
	@test -f .env || { echo "run 'make env' first"; exit 1; }
	@set -a; . ./.env; set +a; \
	 export DATABASE_URL="$${DATABASE_URL:-postgres://lazyboy:lazyboy@127.0.0.1:5434/lazyboy}"; \
	 export SANDBOX_PROVIDER="$${SANDBOX_PROVIDER:-docker}"; \
	 export SANDBOX_SUPERVISOR_URL="$${SANDBOX_SUPERVISOR_URL:-http://127.0.0.1:7091}"; \
	 export DATA_DIR="$${DATA_DIR:-$$PWD/data}"; \
	 export API_BIND="$${API_BIND:-0.0.0.0:3101}"; \
	 export LAZYBOY_WEB_DIR="$${LAZYBOY_WEB_DIR:-$$PWD/$(WEB_DIR)}"; \
	 mkdir -p "$$DATA_DIR"; \
	 export ORT_DYLIB_PATH="$${ORT_DYLIB_PATH:-$$(./scripts/fetch-onnxruntime.sh "$$DATA_DIR/onnxruntime")}"; \
	 echo "starting api on $$API_BIND (web dir $$LAZYBOY_WEB_DIR)"; \
	 exec cargo run -p lazyboy-api
