-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMakefile
More file actions
393 lines (346 loc) · 21.3 KB
/
Copy pathMakefile
File metadata and controls
393 lines (346 loc) · 21.3 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
# malmo dev orchestration. The fast inner loop runs everything natively on the
# host (no VM): host-agent + brain as Go processes, Caddy as a container, the
# UI on Vite. The VM is the outer loop for host-integrated parts (boot, LUKS,
# systemd) and is not wired here yet.
GO ?= $(shell command -v go || echo $(HOME)/.local/go/bin/go)
# gofmt from the same toolchain as $(GO), so `make check` matches CI exactly
# regardless of what's on PATH.
GOFMT ?= $(shell $(GO) env GOROOT)/bin/gofmt
DEV_DIR := .dev
STATE_DIR := $(DEV_DIR)/state
AGENT_SOCK := $(abspath $(DEV_DIR)/agent.sock)
# Build identity (BUILD.md # Versioning): one repo VERSION for the whole
# monorepo, plus the git commit a build was cut from — two stamped fields, no
# "-dev" suffix logic (DECISIONS.md 2026-07-16). VERSION is read from the repo
# root; the commit falls back to "unknown" outside a git checkout (e.g. a
# container build context with no .git) rather than failing the build.
MALMO_VERSION := $(shell cat $(CURDIR)/VERSION)
MALMO_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
LDFLAGS := -X github.com/malmoos/malmo/internal/version.Version=$(MALMO_VERSION) \
-X github.com/malmoos/malmo/internal/version.Commit=$(MALMO_COMMIT)
export MALMO_AGENT_SOCK := $(AGENT_SOCK)
export MALMO_STATE_DIR := $(STATE_DIR)
# The brain syncs the catalog from the control plane (MALMO_CATALOG_URL, default
# the public apex) and caches it last-good here. Point the cache at a writable dev
# path so `make dev` (native, non-root) can persist it; set MALMO_CATALOG_URL to a
# local control plane to develop the store offline.
export MALMO_CATALOG_CACHE_DIR := ./.dev/catalog-cache
.PHONY: build host-agent brain host-agent-real host-agent-real-hosted brain-image ui-image control-plane-images build-cloud-image check check-web fmt fmt-check vet test test-nopam test-caddy test-avahi test-netstate test-health test-usermgr test-usermgr-nspawn test-boot-chain-nspawn test-medium-qemu test-cloud-qemu run-agent run-brain net caddy caddy-down ui dev dev-app seed-catalog stop openapi openapi-check clean check-state-owner help
# msteinert/pam v2.1.0 uses RTLD_NEXT, a GNU extension that requires
# _GNU_SOURCE at C compile time. Apply globally; harmless to non-cgo builds.
export CGO_CFLAGS := -D_GNU_SOURCE
help:
@echo "make build - compile brain + host-agent"
@echo "make build-cloud-image - build the self-bootstrapping hosted cloud VM image via mkosi (needs sudo; #203/#242)"
@echo "make caddy - start the dev Caddy reverse proxy (container)"
@echo "make caddy-down - stop the dev Caddy"
@echo "make check - pre-PR gate: gofmt + vet + full test suite (Go). Run before every PR."
@echo "make check-web - pre-PR gate for frontend changes: web-ui typecheck + build"
@echo "make clean - stop apps, remove dev state"
@echo "make control-plane-images - build malmo-brain + malmo-ui images and docker-save the control-plane bundle to .dev/"
@echo "make dev - all three foreground procs in one terminal (recommended)"
@echo "make dev-app APP=<id> [STORE=../store] - boot ONE store app under curation: seed its catalog snapshot, then make dev with an inert catalog URL"
@echo "make fmt - rewrite Go sources into gofmt-canonical form (autofix)"
@echo "make host-agent-real-hosted - build the slim hosted-cloud host-agent (-tags hosted; #204/C1c)"
@echo "make net - create the malmo-ingress docker network"
@echo "make openapi - regenerate api/openapi.{json,yaml} from the brain (no server)"
@echo "make run-agent - run the fake host-agent (foreground)"
@echo "make run-brain - run the brain (foreground)"
@echo "make stop - stop the native dev stack (brain/host-agent/vite)"
@echo "make test - run the full Go test suite (needs libpam0g-dev)"
@echo "make test-avahi - Avahi DBus publisher integration test (needs avahi-daemon)"
@echo "make test-boot-chain-nspawn - boot dist/systemd units in nspawn + assert shape (needs sudo)"
@echo "make test-caddy - end-to-end Caddy routing test (requires make dev)"
@echo "make test-cloud-qemu - QEMU boot of the hosted cloud image; control plane up (needs sudo; no swtpm/LUKS)"
@echo "make test-health - end-to-end storage-health pipeline (self-contained, ~3s)"
@echo "make test-medium-qemu - QEMU+swtpm boot with real kernel + TPM (needs sudo; first run ~5 min)"
@echo "make test-netstate - NetworkManager LAN-interface integration test"
@echo "make test-nopam - full test suite minus pamverifier (no libpam0g-dev needed)"
@echo "make test-usermgr - LinuxUserManager integration test (needs sudo; nspawn lane recommended instead)"
@echo "make test-usermgr-nspawn - run usermgrtest in systemd-nspawn (needs sudo)"
@echo "make ui - run the Vite dev server (web-ui/)"
@echo ""
@echo "One-terminal: make dev (Caddy started detached; Ctrl-C stops the rest)"
@echo "Four terminals: make caddy ; make run-agent ; make run-brain ; make ui"
# ---- Quality gate -------------------------------------------------------
# `make check` is the pre-PR gate. It mirrors CI's Go job and the
# definition-of-done in docs/dev/contributing.md: gofmt-clean, vet-clean, and
# the full test suite green. Cheapest checks run first so it fails fast.
# Frontend changes additionally need `make check-web`. The full test suite
# needs libpam0g-dev (see docs/dev/running-locally.md); use the individual
# targets if you don't have the headers.
check: fmt-check vet openapi-check test
# Web typecheck + production build (mirrors CI's web job). Needs node/npm.
# Regenerates the OpenAPI TS client from the committed spec and fails if the
# checked-in copy (web-ui/src/generated/openapi.ts) is stale — keeps the
# generated client honest the way openapi-check keeps the spec honest.
check-web:
cd web-ui && npm ci && npm run gen:api
@git diff --quiet web-ui/src/generated/openapi.ts || { \
echo "web-ui/src/generated/openapi.ts is stale — regenerate with: (cd web-ui && npm run gen:api)"; exit 1; }
cd web-ui && npm run build
# Rewrite Go sources into gofmt-canonical form (autofix).
fmt:
$(GOFMT) -w $$(git ls-files '*.go')
# Fail (listing offenders) if any Go source isn't gofmt-clean. Pure check —
# never mutates the tree; run `make fmt` to fix.
fmt-check:
@out=$$($(GOFMT) -l $$(git ls-files '*.go')); \
if [ -n "$$out" ]; then \
echo "These files are not gofmt-clean:"; echo "$$out"; \
echo "Fix with: make fmt"; exit 1; \
fi
vet:
$(GO) vet ./...
# `build` stays host-agent (fake) + brain, unchanged from before this slice.
# host-agent-real is deliberately NOT folded in: it's Linux + CGO +
# libpam0g-dev always (see its header comment — both its build tags need real
# PAM), so it already doesn't build on macOS/Windows/WSL2-without-headers.
# Making it part of the default `build` would break `make build` on exactly
# the machines the inner loop is supposed to work on with no platform-specific
# setup (CLAUDE.md # Developing). It's still stamped — see its own target below
# — for anyone building it directly or via the cloud-image / nspawn lanes.
build: host-agent brain
# The fake host-agent is stamped too — it prints --version like the real
# binaries and its self-reported agent_version (internal/hostagent.AgentVersion)
# derives from the same stamped internal/version.Version, so a dev build's fake
# agent and dev brain agree without a separate hardcoded constant.
host-agent:
$(GO) build -ldflags "$(LDFLAGS)" -o $(DEV_DIR)/host-agent ./cmd/host-agent
host-agent-real:
$(GO) build -ldflags "$(LDFLAGS)" -o $(DEV_DIR)/host-agent-real ./cmd/host-agent-real
# Slim hosted-cloud host-agent (ENVIRONMENT.md # How the profile is realized —
# "A build-tagged slim cloud host-agent"; #204/C1c). The same production binary
# with the appliance's LAN/discovery stack — NetworkManager (netstate) + Avahi
# mDNS publish (avahipublisher) + the network watcher — compiled out via
# `-tags hosted`; the kept seams (PAM verify, user mgmt, health/system reporters,
# per-app logs, reboot, brain launch) are identical. Linux + CGO + libpam0g-dev,
# same as host-agent-real. The cloud image build (#203/C1b, #205/C2) consumes it.
host-agent-real-hosted:
$(GO) build -tags hosted -ldflags "$(LDFLAGS)" -o $(DEV_DIR)/host-agent-real-hosted ./cmd/host-agent-real
brain:
$(GO) build -ldflags "$(LDFLAGS)" -o $(DEV_DIR)/brain ./cmd/brain
# ---- Control-plane images (M0, #163) -----------------------------------
# Build the two malmo OCI images and `docker save` them — together with the two
# third-party control-plane images the brain's compose pulls — into a tarball
# bundle under .dev/ (BUILD.md # 5 / # 5b; TESTING.md # Full-stack control-plane
# integration). The medium-lane VM bakes this bundle and docker-loads it at
# first boot; it has no network, so the third-party images must be in the bundle
# too. Needs only Docker (the images build hermetically — no host Go/Node).
CP_IMAGE_DIR := $(DEV_DIR)/control-plane
BRAIN_IMAGE := malmo-brain:dev
UI_IMAGE := malmo-ui:dev
CADDY_IMAGE := caddy:2-alpine
PROXY_IMAGE := tecnativa/docker-socket-proxy:v0.4.2
brain-image:
docker build -f cmd/brain/Dockerfile --build-arg MALMO_COMMIT=$(MALMO_COMMIT) -t $(BRAIN_IMAGE) .
ui-image:
docker build -f web-ui/Dockerfile -t $(UI_IMAGE) web-ui
control-plane-images: brain-image ui-image
@mkdir -p $(CP_IMAGE_DIR)
docker pull $(CADDY_IMAGE)
docker pull $(PROXY_IMAGE)
docker save $(BRAIN_IMAGE) -o $(CP_IMAGE_DIR)/malmo-brain.tar
docker save $(UI_IMAGE) -o $(CP_IMAGE_DIR)/malmo-ui.tar
docker save $(CADDY_IMAGE) -o $(CP_IMAGE_DIR)/caddy.tar
docker save $(PROXY_IMAGE) -o $(CP_IMAGE_DIR)/docker-socket-proxy.tar
@echo "saved control-plane image bundle to $(CP_IMAGE_DIR)/"
# Run the full suite. Requires libpam0g-dev for the pamverifier package.
test:
$(GO) test ./...
# Skip the pamverifier package (no libpam0g-dev required).
test-nopam:
$(GO) test $$($(GO) list ./... | grep -v pamverifier)
# Integration tests for the Avahi DBus publisher. Requires avahi-daemon
# running on the host. No sudo needed (default DBus policy allows it).
test-avahi:
$(GO) test -tags avahitest ./internal/hostagent/avahipublisher/
# Integration tests for the NetworkManager LAN-interface provider. Requires
# NetworkManager running on the host. No sudo needed (read-only DBus calls).
test-netstate:
$(GO) test -tags nmtest ./internal/hostagent/netstate/
# Integration tests for LinuxUserManager. Exercises real useradd + chpasswd
# against /etc/passwd and /etc/shadow. MUST run as root and is intended for
# the nspawn lane — do NOT run on a developer laptop. See
# docs/progress/0015-host-agent-set-password.md.
test-usermgr:
sudo -E $(GO) test -tags usermgrtest ./internal/hostagent/usermgr/
# Run the usermgrtest-tagged tests inside systemd-nspawn (fast lane per
# docs/specs/TESTING.md). Bootstraps a minimal Debian rootfs at
# .dev/nspawn/rootfs on first run (cached after); each test invocation
# runs in an ephemeral overlay. Requires mmdebstrap + systemd-container.
# See docs/progress/0018-nspawn-usermgr-lane.md.
test-usermgr-nspawn:
sudo -E ./dev/test-nspawn/run-usermgr-tests.sh
# Boot-chain fast-lane test: systemd-nspawn --boot of the dist/systemd
# units, asserting dependency shape, drop-in application, and end-to-end
# storage-verify reporter execution. Reuses the .dev/nspawn/rootfs
# bootstrapped by run-usermgr-tests.sh (bumped to v2 for systemd-sysv).
# See docs/progress/0020-nspawn-boot-chain-lane.md.
test-boot-chain-nspawn:
sudo -E ./dev/test-nspawn/run-boot-chain-tests.sh
# Medium-lane test: QEMU+swtpm boot of a mkosi-built bookworm image
# with a real kernel, real systemd userspace, and an emulated TPM.
# Proves the scaffolding for the TESTING.md # Medium lane is operational.
# First run builds the image (~3-5 min); subsequent runs ~1-2 min.
# Requires mkosi v22+, swtpm, qemu-system-x86, ovmf — bootstrap.sh
# prints an install pointer if anything is missing.
# See docs/progress/0021-qemu-medium-lane-scaffolding.md.
test-medium-qemu:
sudo -E ./dev/test-qemu/run-medium-tests.sh
# Cloud-lane boot proof (C2, #205): build the hosted cloud image, convert it to
# the qcow2 cloud artifact, and boot it ONCE in QEMU to prove the control plane
# comes up and serves — no swtpm, no LUKS, no installer ("the disk IS the
# installed system", ENVIRONMENT.md # Provisioning). The in-VM self-check
# (cloud-assertions.sh) asserts the baked images loaded, the four control-plane
# containers run, the dashboard answers through Caddy, and the hosted /setup gate
# returns 503 (no seed). Air-gapped (restrict=on) so a stray pull hard-fails.
# Requires mkosi v22+, qemu-system-x86, ovmf, docker, go, libpam0g-dev —
# bootstrap.sh prints an install pointer if anything is missing.
# NOTE: do not run test-cloud-qemu and build-cloud-image in parallel — both stage
# into dev/cloud/mkosi.extra.wiring/ and will race on the rm -rf at the start.
# See docs/progress/cloud-vm-boot-proof.md.
test-cloud-qemu:
sudo -E ./dev/cloud/run-cloud-tests.sh
# Build the hosted cloud-VM image (C1b #203; first-boot wiring #242) via mkosi:
# the lean Debian + docker base PLUS the baked first-boot runtime wiring (slim
# host-agent, networkd DHCP config, control-plane image bundle, seed materializer)
# so a provisioned box self-bootstraps instead of booting network-less (#242). Then
# assert it is still lean (no NetworkManager/Avahi/Samba/mergerfs/cryptsetup/tpm2-
# tools — the wiring adds no apt packages) with /etc/malmo/profile=hosted. Output: a
# raw GPT disk image under .dev/cloud/; the cloud repo snapshots it as the tenant
# image. Needs root (control-plane image build + mkosi disk ops) + mkosi v22+, go,
# docker, libpam0g-dev; bootstrap.sh prints an install pointer if anything is missing.
# NOTE: do not run build-cloud-image and test-cloud-qemu in parallel — both stage
# into dev/cloud/mkosi.extra.wiring/ and will race on the rm -rf at the start.
# See docs/progress/hosted-cloud-image.md, docs/progress/cloud-image-first-boot-wiring.md.
build-cloud-image:
sudo -E ./dev/cloud/bootstrap.sh
# End-to-end Caddy routing verification. Assumes `make dev` is running.
# Tests Host-header routing, confirms path-based routing does NOT work,
# and verifies route withdrawal after uninstall.
test-caddy:
./dev/test-caddy-routing.sh
# Self-contained end-to-end test of the storage-health pipeline
# (docs/progress/0019). Builds the three binaries, spins up the fake
# host-agent + brain in a tempdir, exercises six cases through the real
# wire format, and tears down. ~3 seconds. No daemons required.
test-health:
./dev/test-health.sh
net:
@docker network inspect malmo-ingress >/dev/null 2>&1 || docker network create malmo-ingress
caddy: net
docker compose -f dev/docker-compose.yml up -d
caddy-down:
docker compose -f dev/docker-compose.yml down
run-agent: host-agent
@mkdir -p $(DEV_DIR)
$(DEV_DIR)/host-agent
run-brain: brain net
@mkdir -p $(STATE_DIR)
$(DEV_DIR)/brain
# WEB_UI.md specifies pnpm; npm is used here until pnpm is set up on the box.
ui:
cd web-ui && npm install && npm run dev
# Guard against a root-owned dev state dir. App containers run as root in the
# skeleton and write root-owned files into instances/<id>/; a privileged run or
# a half-finished manual `rm` (which can't remove that root-owned data) can leave
# instances/ itself root-owned. The brain then fails an install mid-transaction
# with a cryptic `mkdir … permission denied` (after a SQLite row already exists).
# Catch it up front with an actionable message. The supported reset is `make
# clean` (reclaims root-owned data via a throwaway root container), never a hand `rm`.
check-state-owner:
@if [ -d "$(STATE_DIR)/instances" ] && [ "$$(stat -c %u "$(STATE_DIR)/instances")" != "$$(id -u)" ]; then \
echo "error: $(STATE_DIR)/instances is owned by uid $$(stat -c %u "$(STATE_DIR)/instances"), not you (uid $$(id -u))."; \
echo " App installs will fail with 'mkdir … permission denied'."; \
echo " Reset with: make clean (or: sudo chown -R $$(id -un) $(STATE_DIR))"; \
exit 1; \
fi
# One-terminal dev loop. Pure bash: backgrounds the three foreground procs,
# prefixes their output with [agent]/[brain]/[ui], and the trap kills the
# whole process group on Ctrl-C. Caddy is started detached because it's
# already a long-running container — no point supervising it here.
dev: check-state-owner build caddy
@mkdir -p $(STATE_DIR)
@cd web-ui && [ -d node_modules ] || npm install
@trap 'kill 0' INT TERM EXIT; \
(MALMO_DEV_AVAHI=1 $(DEV_DIR)/host-agent 2>&1 | sed -u 's/^/[agent] /') & \
($(DEV_DIR)/brain 2>&1 | sed -u 's/^/[brain] /') & \
(cd web-ui && npm run dev 2>&1 | sed -u 's/^/[ui] /') & \
wait
# ---- Curate one store app against the brain (store #22) --------------------
# Post-catalog-cutover (cloud #62) there is no baked os/catalog/ to boot from —
# the brain is a thin HTTP client of the control plane. `make dev-app APP=<id>`
# restores the inner loop for authoring/curating a store app: it seeds the
# brain's last-good catalog cache from a store checkout (STORE/apps/APP) with
# mkcatalog, then runs the normal dev stack. The brain loads that cache at boot
# exactly as it would a synced-then-offline snapshot (internal/catalog/remote.go
# # loadCache) and installs the app from it.
#
# It uses mkcatalog, NOT cloud's catalog-sync, on purpose: catalog-sync publishes
# only listed:true records, but an app under curation has no verdict yet — you
# boot it to *decide* whether it is full or degraded. mkcatalog reads the
# manifest+compose directly and ignores status.yml, so no provisional listed:true
# is ever needed (and can't be committed by accident).
STORE ?= ../store
seed-catalog:
@[ -n "$(APP)" ] || { echo "usage: make dev-app APP=<id> [STORE=../store]" >&2; exit 2; }
@[ -d "$(STORE)/apps/$(APP)" ] || { echo "error: no app package at $(STORE)/apps/$(APP)" >&2; exit 2; }
@mkdir -p $(DEV_DIR)/catalog-cache
@$(GO) run ./dev/mkcatalog -pkg "$(STORE)/apps/$(APP)" -environments appliance,hosted -out $(DEV_DIR)/catalog-cache/catalog.json
@echo "seeded $(APP) -> $(DEV_DIR)/catalog-cache/catalog.json (visible on: appliance, hosted)"
# The inert catalog URL is a target-specific, exported variable, so it is in
# effect for the `dev` prerequisite's recipe too — the brain reads
# MALMO_CATALOG_URL from the env, and cmd/brain defaults it to the real apex
# (https://malmo.network). Without the override the first background sync would
# succeed and overwrite the seed with the published catalog, silently dropping
# the app under test. Port 1 has nothing listening, so the sync fails fast (same
# inert-URL trick as dev/test-health.sh). seed-catalog runs first and aborts the
# whole target if APP is missing, so `dev` never starts against a bad seed.
dev-app: export MALMO_CATALOG_URL := http://127.0.0.1:1
dev-app: seed-catalog dev
# Regenerate the committed OpenAPI spec (api/openapi.{json,yaml}) from the huma
# handler registrations — no running brain, no port (BRAIN_UI_PROTOCOL.md
# # Codegen). The spec is the substrate for the web-ui's generated TS client
# (web-ui `npm run gen:api`) and the freshness gate below.
openapi:
@go run ./cmd/openapi-gen -o api && echo "wrote api/openapi.json, api/openapi.yaml"
# Fail if the committed spec is stale (re-emit to a scratch dir and diff). Pure
# check — never mutates the tree; run `make openapi` to refresh. Mirrors the
# fmt-check pattern; wired into `make check` and CI so a brain DTO change that
# isn't regenerated can't merge silently.
openapi-check:
@tmp=$$(mktemp -d); \
go run ./cmd/openapi-gen -o $$tmp || { rm -rf $$tmp; exit 1; }; \
if ! diff -q api/openapi.json $$tmp/openapi.json >/dev/null || ! diff -q api/openapi.yaml $$tmp/openapi.yaml >/dev/null; then \
echo "api/openapi.{json,yaml} is stale — regenerate with: make openapi"; \
diff -u api/openapi.json $$tmp/openapi.json || true; \
rm -rf $$tmp; exit 1; \
fi; \
rm -rf $$tmp; echo "openapi spec is fresh"
# Stop the native dev stack (`make dev` runs brain/host-agent/vite outside
# Docker). Without this, `clean` leaves the brain running with the deleted
# malmo.db still open (deleted-but-open inode), so it keeps serving the old
# state and the wiped DB silently comes back — `clean` looks like a no-op.
# Best-effort: pkill exits non-zero when nothing matches, hence the `-` prefix.
# The supervisor is matched by its MALMO_DEV_AVAHI env prefix; the binaries by
# their $(DEV_DIR) path; vite by this repo's absolute path so we don't reap an
# unrelated Vite on the box.
stop:
-@pkill -f 'MALMO_DEV_AVAHI=1' 2>/dev/null
-@pkill -f '$(DEV_DIR)/brain' 2>/dev/null
-@pkill -f '$(DEV_DIR)/host-agent' 2>/dev/null
-@pkill -f '$(CURDIR)/web-ui/node_modules/.bin/vite' 2>/dev/null
@echo "stopped native dev stack (brain/host-agent/vite)"
# clean = back to a blank slate: stop the native stack (stop) and the Caddy
# container (caddy-down), remove app containers/networks, then wipe dev state.
# stop must run before the rm or the live brain keeps the DB inode alive.
clean: stop caddy-down
-@docker ps -aq --filter "label=com.docker.compose.project" --filter "name=malmo-" | xargs -r docker rm -f
-@docker network ls -q --filter "name=malmo-app-" | xargs -r docker network rm
@# App containers (Postgres et al.) write their data as root inside bind
@# mounts, so instances/<id>/data is root-owned on the host — same as prod,
@# where the privileged uninstall path removes it. A plain `rm` as the dev
@# user can't, so reclaim it via a throwaway root container first. No sudo.
-@docker run --rm -v $(abspath $(DEV_DIR)/state):/state alpine:3 rm -rf /state 2>/dev/null || true
rm -rf $(DEV_DIR)/state