.PHONY: install install-cli db db-tools migrate seed api dashboard check start clean register-project validate-adr add-domain rename-domain add-repo list-repos cleanup-stale tunnel tunnel-daemon tunnel-loop tunnel-status tunnel-stop COMPOSE = docker compose -f infra/docker-compose.yml --env-file .env install: uv sync ## Symlink the custodian CLI into ~/.local/bin so it's on PATH system-wide install-cli: install mkdir -p ~/.local/bin ln -sf "$(shell pwd)/.venv/bin/custodian" ~/.local/bin/custodian @echo "Installed: custodian → $$(readlink -f ~/.local/bin/custodian)" @echo "Make sure ~/.local/bin is on your PATH:" @echo " echo 'export PATH=\"\$$HOME/.local/bin:\$$PATH\"' >> ~/.bashrc && source ~/.bashrc" db: $(COMPOSE) up -d postgres db-tools: $(COMPOSE) --profile tools up -d migrate: uv run alembic upgrade head seed: uv run python scripts/seed.py api: uv run uvicorn api.main:app --reload --host 127.0.0.1 --port 8000 dashboard: cd dashboard && npm run dev check: curl -sf http://127.0.0.1:8000/state/health | python3 -m json.tool ## COULOMBCORE host (default target for tunnel targets) COULOMBCORE ?= tegwick@92.205.130.254 TUNNEL_PORT ?= 8000 ## Foreground reverse tunnel — good for debugging. Ctrl-C to stop. ## Usage: make tunnel HOST=tegwick@92.205.130.254 tunnel: @test -n "$(HOST)" || (echo "ERROR: HOST is required. Usage: make tunnel HOST=user@hostname"; exit 1) @echo "Opening reverse tunnel → $(HOST) (remote :$(TUNNEL_PORT) → local :$(TUNNEL_PORT))" @echo "Keep this terminal open. Ctrl-C to close the tunnel." ssh -N -o "ServerAliveInterval=30" -o "ServerAliveCountMax=3" \ -R $(TUNNEL_PORT):127.0.0.1:$(TUNNEL_PORT) $(HOST) ## Background tunnel to COULOMBCORE with auto-reconnect. ## Uses autossh if available; prints install hint and exits if not. ## After running, COULOMBCORE can reach the State Hub at http://127.0.0.1:8000 tunnel-daemon: @if command -v autossh >/dev/null 2>&1; then \ echo "Starting autossh tunnel → $(COULOMBCORE)"; \ autossh -f -N -M 0 \ -o "ServerAliveInterval=30" \ -o "ServerAliveCountMax=3" \ -o "ExitOnForwardFailure=yes" \ -R $(TUNNEL_PORT):127.0.0.1:$(TUNNEL_PORT) $(COULOMBCORE); \ echo "Tunnel running in background. Use 'make tunnel-status' to check."; \ else \ echo "autossh not found — install it: sudo apt-get install autossh"; \ echo "Fallback: run 'make tunnel-loop HOST=$(COULOMBCORE)' in a dedicated terminal."; \ exit 1; \ fi ## Reconnect loop — works without autossh. Run in a terminal you can leave open. ## Usage: make tunnel-loop HOST=tegwick@92.205.130.254 tunnel-loop: @test -n "$(HOST)" || (echo "ERROR: HOST is required. Usage: make tunnel-loop HOST=user@hostname"; exit 1) @echo "Reconnect loop → $(HOST). Ctrl-C to stop." @while true; do \ echo "[$(shell date -u +%Y-%m-%dT%H:%M:%SZ)] Connecting..."; \ ssh -N -o "ServerAliveInterval=30" -o "ServerAliveCountMax=3" \ -o "ExitOnForwardFailure=yes" \ -R $(TUNNEL_PORT):127.0.0.1:$(TUNNEL_PORT) $(HOST) || true; \ echo "[$(shell date -u +%Y-%m-%dT%H:%M:%SZ)] Connection lost — retrying in 5s..."; \ sleep 5; \ done ## Check whether a tunnel is currently active tunnel-status: @if command -v autossh >/dev/null 2>&1 && pgrep -f "autossh.*$(TUNNEL_PORT)" > /dev/null 2>&1; then \ echo "autossh tunnel: RUNNING (PIDs: $$(pgrep -f 'autossh.*$(TUNNEL_PORT)' | tr '\n' ' '))"; \ elif pgrep -f "ssh.*-R $(TUNNEL_PORT)" > /dev/null 2>&1; then \ echo "ssh tunnel: RUNNING (PIDs: $$(pgrep -f 'ssh.*-R $(TUNNEL_PORT)' | tr '\n' ' '))"; \ else \ echo "Tunnel: NOT running"; \ fi ## Stop any active tunnel (autossh or plain ssh) tunnel-stop: @pkill -f "autossh.*$(TUNNEL_PORT)" 2>/dev/null && echo "autossh stopped" || true @pkill -f "ssh.*-R $(TUNNEL_PORT)" 2>/dev/null && echo "ssh loop stopped" || true start: db sleep 3 $(MAKE) migrate $(MAKE) api ## Register a project: make register-project DOMAIN=railiance PROJECT_PATH=/home/worsch/railiance register-project: @test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required. Usage: make register-project DOMAIN= PROJECT_PATH="; exit 1) @test -n "$(PROJECT_PATH)" || (echo "ERROR: PROJECT_PATH is required."; exit 1) scripts/register_project.sh "$(DOMAIN)" "$(PROJECT_PATH)" ## Add a second repo to an existing domain: make add-repo DOMAIN=railiance REPO_PATH=/home/worsch/railiance-infra add-repo: @test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required."; exit 1) @test -n "$(REPO_PATH)" || (echo "ERROR: REPO_PATH is required."; exit 1) scripts/register_project.sh "$(DOMAIN)" "$(REPO_PATH)" --additional ## Create a new domain: make add-domain DOMAIN=my_domain NAME="My Domain" add-domain: @test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required (slug)."; exit 1) @test -n "$(NAME)" || (echo "ERROR: NAME is required (display name)."; exit 1) curl -sf -X POST http://127.0.0.1:8000/domains/ \ -H "Content-Type: application/json" \ -d "{\"slug\": \"$(DOMAIN)\", \"name\": \"$(NAME)\"}" | python3 -m json.tool ## Rename a domain: make rename-domain DOMAIN=old_slug NEW_SLUG=new_slug NEW_NAME="New Name" rename-domain: @test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN (old slug) is required."; exit 1) @test -n "$(NEW_SLUG)" || (echo "ERROR: NEW_SLUG is required."; exit 1) @test -n "$(NEW_NAME)" || (echo "ERROR: NEW_NAME is required."; exit 1) curl -sf -X PATCH http://127.0.0.1:8000/domains/$(DOMAIN)/rename \ -H "Content-Type: application/json" \ -d "{\"new_slug\": \"$(NEW_SLUG)\", \"new_name\": \"$(NEW_NAME)\"}" | python3 -m json.tool ## List repos for a domain: make list-repos DOMAIN=railiance list-repos: @test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required."; exit 1) curl -sf "http://127.0.0.1:8000/repos/?domain=$(DOMAIN)" | python3 -m json.tool ## Ingest SBOM data for a repo (all mechanisms: lockfiles + ansible + sbom-tools.yaml). ## Auto-detect all sources: make ingest-sbom REPO=the-custodian REPO_PATH=/home/worsch/the-custodian ## Single lockfile (explicit): make ingest-sbom REPO=the-custodian LOCKFILE=/path/to/uv.lock ## Dry-run (no submit): make ingest-sbom REPO=the-custodian REPO_PATH=... DRY_RUN=1 ## Tip: run capture-tools first for repos with system-level tool dependencies. ingest-sbom: @test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1) uv run python scripts/ingest_sbom.py --repo "$(REPO)" \ $(if $(LOCKFILE),--lockfile "$(LOCKFILE)") \ $(if $(REPO_PATH),--repo-path "$(REPO_PATH)") \ $(if $(DRY_RUN),--dry-run) ## Run SBOM capture agent for a repo — generates/updates sbom-tools.yaml. ## Usage: make capture-tools REPO=railiance-infra [REPO_PATH=/home/worsch/railiance-infra] ## Add DRY_RUN=1 to preview without writing. capture-tools: @test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1) uv run python scripts/capture_sbom_tools.py --repo "$(REPO)" \ $(if $(REPO_PATH),--repo-path "$(REPO_PATH)") \ $(if $(DRY_RUN),--dry-run) ## Check a repo for ADR-001 compliance: make validate-adr REPO=/path/to/repo [DOMAIN=custodian] validate-adr: @test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make validate-adr REPO= [DOMAIN=]"; exit 1) uv run python scripts/validate_repo_adr.py "$(REPO)" $(if $(DOMAIN),--domain "$(DOMAIN)",) ## Check a single repo for ADR-001 consistency: make check-consistency REPO=the-custodian check-consistency: @test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make check-consistency REPO="; exit 1) uv run python scripts/consistency_check.py --repo "$(REPO)" $(if $(API_BASE),--api-base "$(API_BASE)",) ## Check and auto-fix a single repo: make fix-consistency REPO=the-custodian fix-consistency: @test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make fix-consistency REPO="; exit 1) uv run python scripts/consistency_check.py --repo "$(REPO)" --fix $(if $(API_BASE),--api-base "$(API_BASE)",) ## Check all registered repos for ADR-001 consistency check-consistency-all: uv run python scripts/consistency_check.py --all $(if $(API_BASE),--api-base "$(API_BASE)",) ## Check and auto-fix all registered repos fix-consistency-all: uv run python scripts/consistency_check.py --all --fix $(if $(API_BASE),--api-base "$(API_BASE)",) ## Cancel open tasks belonging to completed/archived workstreams. ## Safe to run at any time; also suitable for a daily cron job. ## Cron example: 0 3 * * * cd ~/the-custodian/state-hub && make cleanup-stale cleanup-stale: uv run python scripts/cleanup_stale_tasks.py clean: $(COMPOSE) down -v