diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml
index c508dc4..f72e84c 100644
--- a/.gitea/workflows/ci.yml
+++ b/.gitea/workflows/ci.yml
@@ -34,10 +34,8 @@ jobs:
with:
filters: |
odin:
- - 'src/**'
- - 'lib/**'
+ - 'gui/**'
- 'scripts/**'
- - 'odinfmt.json'
api:
- 'api/**'
@@ -65,14 +63,10 @@ jobs:
fi
- name: Run Odin unit tests
- run: |
- mkdir -p lib/local
- odin test src -collection:lib=lib/local
+ run: make -C gui test
- name: Verify build
- run: |
- mkdir -p bin
- odin build src -collection:lib=lib/local -o:speed -out:bin/desi_explorer
+ run: make -C gui build
api:
name: API unit tests and lint
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
index 35e2d12..3965797 100644
--- a/.gitea/workflows/deploy.yml
+++ b/.gitea/workflows/deploy.yml
@@ -53,10 +53,8 @@ jobs:
base: ${{ steps.prev.outputs.prev }}
filters: |
odin:
- - 'src/**'
- - 'lib/**'
+ - 'gui/**'
- 'scripts/**'
- - 'odinfmt.json'
api:
- 'api/**'
infra:
@@ -84,7 +82,7 @@ jobs:
- name: Build WebAssembly
run: |
source /tmp/emsdk/emsdk_env.sh
- scripts/build_web.sh
+ gui/scripts/build_web.sh
- name: Package
run: |
diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml
index 94bd31a..f3e347e 100644
--- a/.gitea/workflows/release.yml
+++ b/.gitea/workflows/release.yml
@@ -49,8 +49,8 @@ jobs:
- name: Build
run: |
- mkdir -p lib/local build/linux
- odin build src -o:speed -collection:lib=lib/local -out:build/linux/desi_explorer
+ mkdir -p build/linux
+ odin build gui/src -o:speed -collection:lib=gui/lib/local -out:build/linux/desi_explorer
- name: Package
run: |
@@ -110,10 +110,10 @@ jobs:
# next to desi_explorer.exe.
- name: Build
run: |
- mkdir -p lib/local build/windows
- odin build src -target:windows_amd64 -build-mode:obj \
+ mkdir -p build/windows
+ odin build gui/src -target:windows_amd64 -build-mode:obj \
-define:RAYLIB_SHARED=true -o:speed \
- -collection:lib=lib/local -out:build/windows/desi_explorer
+ -collection:lib=gui/lib/local -out:build/windows/desi_explorer
x86_64-w64-mingw32-gcc -O2 -o build/windows/desi_explorer.exe \
build/windows/desi_explorer.obj \
/tmp/odin-win/dist/vendor/raylib/windows/raylibdll.lib \
@@ -151,7 +151,7 @@ jobs:
- name: Build WebAssembly
run: |
source /tmp/emsdk/emsdk_env.sh
- scripts/build_web.sh
+ gui/scripts/build_web.sh
- name: Package
run: |
diff --git a/AGENTS.md b/AGENTS.md
index d2b56d9..3046628 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -2,27 +2,28 @@
Odin + raylib interactive 3D universe map (DESI data), a Rust + axum API, with Go + Pulumi infra and Gitea Actions CI. A clean `odin build` + `cargo test` + `go test` is the verification step.
-## Commands (root Makefile)
-- Run: `make run` (`odin run src`); Build: `make build` (`odin build src -collection:lib=lib/local -out:bin/desi_explorer`)
-- Web: `make build-web` (Emscripten/WASM -> build/web); Debug: `make build-debug`
-- Test: `make test` (runs `odin test` for src/, `go test` for infra/, and `cargo test` for api/); Clean: `make clean`
-- API: `make api-run` (`cargo run` in api/), `make api-test`, `make api-check` (`cargo fmt --check` + `clippy -D warnings`)
+## Commands
+The root `Makefile` is a lean delegator: base commands (`run`, `build`, `test`, ...) forward into the per-project Makefiles in `gui/`, `api/`, and `infra/`. Project-specific commands are reached via `make -C
`.
+- Run: `make run`; Build: `make build` (`-> bin/desi_explorer`); Web: `make build-web` (Emscripten/WASM -> `build/web`); Debug: `make build-debug`
+- Test: `make test` (runs `odin test` in gui/, `cargo test` in api/, `go test` in infra/); Clean: `make clean`; Format: `make fmt`
+- API: `make api-run`, `make api-test`, `make api-check` (thin passthroughs to `make -C api `)
- Setup: `make setup` (submodules + `go mod tidy` in infra/)
-- Infra: `make infra-preview`, `make infra-up`, `make infra-down`, `make infra-refresh` (Pulumi, `--cwd infra`)
+- Infra: `make infra-preview`, `make infra-up`, `make infra-down`, `make infra-refresh` (passthroughs to `make -C infra `)
- Renovate: `make renovate-validate` (npx renovate-config-validator)
-- Format Odin with `odinfmt` (config `odinfmt.json`: tabs, width 80); format Rust with `cargo fmt`
+- Format Odin with `odinfmt` (config `gui/odinfmt.json`: tabs, width 80); format Rust with `cargo fmt`
## Layout
-- `src/main.odin` — entrypoint; loop is `update()` → `draw()`; resizable window with an orbital `Camera3D`; point cloud generated procedurally in `make_universe`.
-- `lib/` — third-party Odin deps (submodules / vendored libs). raylib is **not** here: it ships vendored with Odin and is imported as `vendor:raylib`. `lib/local/` is the default `-collection:lib` target and is currently empty.
-- `api/` — Rust + axum backend API that will serve DESI catalog data. `src/lib.rs` exposes `routes`/`models`/`config`; `src/main.rs` is the server entrypoint (binds `API_BIND_ADDR`, default `0.0.0.0:8080`). Crate name is `desi-explorer-api` (lib `desi_explorer_api`); Rust version pinned in `api/rust-toolchain.toml`.
-- `infra/` — Go + Pulumi (`runtime: go`) targeting the on-prem k3s cluster via the default kubeconfig, mirroring the `homelab` repo's pattern. Module name is `desi-explorer-infra`.
+- `gui/` — the Odin + raylib renderer. `gui/src/main.odin` is the entrypoint (loop is `update()` → `draw()`; resizable window with an orbital `Camera3D`; point cloud generated procedurally in `make_universe`). `gui/lib/` holds third-party Odin deps (see below), `gui/www/` the Web GUI (WASM shell / static web assets), and `gui/scripts/` the build scripts. `gui/Makefile` owns the Odin targets.
+- `gui/lib/` — third-party Odin deps (submodules / vendored libs). raylib is **not** here: it ships vendored with Odin and is imported as `vendor:raylib`. `gui/lib/local/` is the default `-collection:lib` target and is currently empty.
+- `api/` — Rust + axum backend API that will serve DESI catalog data. `src/lib.rs` exposes `routes`/`models`/`config`; `src/main.rs` is the server entrypoint (binds `API_BIND_ADDR`, default `0.0.0.0:8080`). Crate name is `desi-explorer-api` (lib `desi_explorer_api`); Rust version pinned in `api/rust-toolchain.toml`. `api/Makefile` owns the Rust targets.
+- `infra/` — Go + Pulumi (`runtime: go`) targeting the on-prem k3s cluster via the default kubeconfig, mirroring the `homelab` repo's pattern. Module name is `desi-explorer-infra`. `infra/Makefile` owns the Pulumi/Go targets.
+- `scripts/` — repo-level build helpers (Odin toolchain install).
- `.gitea/workflows/` — `ci.yml` (PR: install Odin, Rust, `odin test` + build, `cargo test` + clippy, `go test`) and `release.yml` (tag `v*`: build native + WASM + publish a Gitea release).
## Conventions
-- Odin code lives in `src/`; external deps go in `lib/` and are wired via `-collection:lib=lib/local` (or a git submodule imported by relative path).
+- Odin code lives in `gui/src/`; external deps go in `gui/lib/` and are wired via `-collection:lib=lib/local` (or a git submodule imported by relative path).
- Everything is plain `make` — no Taskfile — so CI (Gitea Actions) can call `make` directly.
-- Keep the renderer (src/), API (api/), and infra (infra/) logically separated; the root Makefile is the only place that ties them together.
+- Keep the renderer (gui/), API (api/), and infra (infra/) logically separated; each owns its own Makefile, and the root Makefile is the only place that ties them together.
## Gotchas
- Odin version is pinned in `.gitea/workflows/*.yml` (`ODIN_VERSION`) and defaults in `scripts/install_odin.sh`; bump both together when tracking a new release.
diff --git a/Makefile b/Makefile
index ae8cca2..efc5735 100644
--- a/Makefile
+++ b/Makefile
@@ -1,94 +1,69 @@
# DESI Explorer — root Makefile
#
-# Orchestrates the Odin renderer (src/) and the Go/Pulumi infrastructure
-# (infra/). Intended to be called directly by Gitea Actions CI.
+# Lean orchestrator: the "base" commands (`run`, `build`, `test`, ...) delegate
+# into the per-project Makefiles in `gui/`, `api/`, and `infra/`. Project-
+# specific commands live in those directories and are reached with
+# `make -C ` (or the thin passthrough targets below).
-ODIN ?= odin
-CARGO ?= cargo
-BIN ?= bin
-BINARY := $(BIN)/desi_explorer
-ODIN_FLAGS := -collection:lib=lib/local
-WASM_DEFINE := RAYLIB_WASM_LIB=env.o
+GUI := gui
+API := api
+INFRA := infra
.PHONY: help setup run build build-debug build-web test clean fmt \
- api-run api-build api-test api-check api-fmt \
- infra-preview infra-up infra-down infra-refresh infra-static \
renovate-validate
help: ## List available targets
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}'
+ @echo ""
+ @echo "Sub-project targets:"
+ @echo " \033[36mmake -C gui help\033[0m Odin renderer (run, build, build-web, test, fmt, ...)"
+ @echo " \033[36mmake -C api help\033[0m Rust API (run, build, test, check, fmt, ...)"
+ @echo " \033[36mmake -C infra help\033[0m Infra (preview, up, down, refresh, test, ...)"
## ---- Setup ---------------------------------------------------------------
setup: ## Pull submodules + tidy Go deps
@git submodule update --init --recursive
- @mkdir -p lib/local
- @cd infra && go mod tidy
+ @$(MAKE) -C $(INFRA) tidy
## ---- Renderer (Odin) -----------------------------------------------------
-run: ## Run the native app
- @mkdir -p lib/local
- $(ODIN) run src $(ODIN_FLAGS)
+run: ## Run the native app (gui/)
+ @$(MAKE) -C $(GUI) run
-build: ## Release build -> bin/desi_explorer
- @mkdir -p lib/local $(BIN)
- $(ODIN) build src $(ODIN_FLAGS) -o:speed -out:$(BINARY)
+build: ## Release build -> bin/desi_explorer (gui/)
+ @$(MAKE) -C $(GUI) build
-build-debug: ## Debug build -> bin/desi_explorer
- @mkdir -p lib/local $(BIN)
- $(ODIN) build src $(ODIN_FLAGS) -o:none -debug -out:$(BINARY)
+build-debug: ## Debug build -> bin/desi_explorer (gui/)
+ @$(MAKE) -C $(GUI) build-debug
-build-web: ## WebAssembly build -> build/web (needs emscripten)
- @scripts/build_web.sh
+build-web: ## WebAssembly build -> build/web (gui/, needs emscripten)
+ @$(MAKE) -C $(GUI) build-web
-test: ## Run tests (Odin src/ + Go infra/ + Rust api/)
- @mkdir -p lib/local
- $(ODIN) test src $(ODIN_FLAGS)
- @cd infra && go test ./...
- @cd api && $(CARGO) test
+## ---- Aggregates ----------------------------------------------------------
-clean: ## Remove build artifacts
- rm -rf $(BIN) build
- @cd api && $(CARGO) clean
+test: ## Test all projects (odin + cargo + go)
+ @$(MAKE) -C $(GUI) test
+ @$(MAKE) -C $(API) test
+ @$(MAKE) -C $(INFRA) test
-fmt: ## Format Odin sources with odinfmt
- odinfmt src
+clean: ## Remove build artifacts from all projects
+ @$(MAKE) -C $(GUI) clean
+ @$(MAKE) -C $(API) clean
+ @$(MAKE) -C $(INFRA) clean
-## ---- API (Rust + axum) ---------------------------------------------------
+fmt: ## Format all projects (odin + rust)
+ @$(MAKE) -C $(GUI) fmt
+ @$(MAKE) -C $(API) fmt
-api-run: ## Run the API server (dev)
- @cd api && $(CARGO) run
+## ---- Passthrough to sub-project Makefiles --------------------------------
-api-build: ## Release build the API -> api/target/release/
- @cd api && $(CARGO) build --release
+api-%: ## Forward a target to the api/ Makefile
+ @$(MAKE) -C $(API) $*
-api-test: ## Run API unit/integration tests
- @cd api && $(CARGO) test
-
-api-check: ## Typecheck + format-check + lint the API
- @cd api && $(CARGO) fmt --all --check && $(CARGO) clippy --all-targets -- -D warnings
-
-api-fmt: ## Format Rust sources with rustfmt
- @cd api && $(CARGO) fmt
-
-## ---- Infrastructure (Go + Pulumi) ----------------------------------------
-
-infra-preview: ## Preview infra changes against the cluster
- @pulumi preview --cwd infra
-
-infra-up: ## Deploy/update infra
- @pulumi up --cwd infra
-
-infra-down: ## Tear down the stack's resources
- @pulumi destroy --cwd infra
-
-infra-refresh: ## Refresh Pulumi state against the live cluster
- @pulumi refresh --cwd infra
-
-infra-static: ## Typecheck + vet the Go infra without Pulumi
- @cd infra && go build -o /dev/null . && go vet ./...
+infra-%: ## Forward a target to the infra/ Makefile
+ @$(MAKE) -C $(INFRA) $*
## ---- Tooling -------------------------------------------------------------
diff --git a/README.md b/README.md
index f9b07ad..fe21849 100644
--- a/README.md
+++ b/README.md
@@ -42,17 +42,20 @@ Early scaffolding. The application currently:
## Repository Layout
```
-src/ Odin source — the interactive 3D experience (entrypoint: src/main.odin)
-lib/ Third-party Odin dependencies (submodules / vendored libs)
-api/ Rust backend API (axum) for serving DESI catalog data
-infra/ Go + Pulumi infrastructure-as-code (deploys the experience to k8s)
-scripts/ Build helpers (Odin install, WASM build)
-.gitea/ Gitea Actions CI/CD workflows
+gui/ Odin renderer — the interactive 3D experience (entrypoint: gui/src/main.odin)
+ gui/lib/ Third-party Odin dependencies (submodules / vendored libs)
+ gui/www/ Web GUI — WASM shell / static web assets
+ gui/scripts/ Build scripts (WASM build helper)
+api/ Rust backend API (axum) for serving DESI catalog data
+infra/ Go + Pulumi infrastructure-as-code (deploys the experience to k8s)
+scripts/ Repo-level build helpers (Odin install)
+.gitea/ Gitea Actions CI/CD workflows
```
-The renderer (`src/`), the API (`api/`), and the infrastructure (`infra/`) are
-kept in separate directories so their logic stays cleanly separated; all are
-orchestrated by the root `Makefile`.
+The renderer (`gui/`), the API (`api/`), and the infrastructure (`infra/`) are
+kept in separate directories so their logic stays cleanly separated. Each
+project carries its own `Makefile`; the root `Makefile` delegates the base
+commands (`run`, `build`, `test`, ...) into them.
## Building & Running
@@ -62,21 +65,33 @@ vendor bindings. The web build additionally requires [Emscripten](https://emscri
pinned via `api/rust-toolchain.toml`). The infrastructure requires
[Go](https://go.dev) and [Pulumi](https://www.pulumi.com).
-All project commands live in the root `Makefile` (`make help` lists them):
+The root `Makefile` delegates everything to the per-project Makefiles
+(`make help` lists the base targets):
```sh
make setup # pull submodules + tidy Go deps
-make run # run the native app
-make build # -> bin/desi_explorer
-make build-debug # debug native build
-make build-web # -> build/web (wasm + html)
+make run # run the native app (gui/)
+make build # -> bin/desi_explorer (gui/)
+make build-debug # debug native build (gui/)
+make build-web # -> build/web (wasm + html, gui/)
make test # odin test + go test + cargo test
-make clean # remove build artifacts
-make api-run # run the Rust API server
-make api-test # cargo test
-make api-check # rustfmt check + clippy
+make clean # remove build artifacts from all projects
+make fmt # format all projects
```
+Project-specific targets live in their own `Makefile` and are reached with
+`make -C `:
+
+```sh
+make -C gui help # Odin renderer (run, build, build-debug, build-web, test, fmt)
+make -C api help # Rust API (run, build, test, check, fmt)
+make -C infra help # Infra (preview, up, down, refresh, static, test)
+```
+
+The root also provides thin passthroughs: `make api-` and
+`make infra-` forward to the corresponding sub-project (e.g.
+`make api-check` -> `make -C api check`).
+
## Dependency Updates (Renovate)
[Renovate](https://github.com/renovatebot/renovate) keeps dependencies up to
@@ -91,5 +106,5 @@ It manages four categories:
- **Infra dependencies** — `infra/go.mod` (`gomod` manager); minor/patch updates grouped into a single PR.
- **API dependencies** — `api/Cargo.toml` (`cargo` manager); minor/patch updates grouped into a single PR.
-- **Submodules** — `lib/` submodules via `.gitmodules` (`git-submodules` manager).
+- **Submodules** — `gui/lib/` submodules via `.gitmodules` (`git-submodules` manager).
- **Workflow actions** — action versions in `.gitea/workflows/*.yml` (`github-actions` manager, which understands Gitea's `.gitea` layout).
diff --git a/api/Makefile b/api/Makefile
new file mode 100644
index 0000000..87ad652
--- /dev/null
+++ b/api/Makefile
@@ -0,0 +1,30 @@
+# api/Makefile — the Rust + axum backend API.
+#
+# Driven directly with `make -C api ` (or via the root Makefile's
+# `api-*` convenience targets).
+
+CARGO ?= cargo
+
+.PHONY: help run build test check fmt clean
+
+help: ## List available targets
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
+ awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}'
+
+run: ## Run the API server (dev)
+ $(CARGO) run
+
+build: ## Release build -> target/release/
+ $(CARGO) build --release
+
+test: ## Run unit/integration tests
+ $(CARGO) test
+
+check: ## Format-check + lint (clippy -D warnings)
+ $(CARGO) fmt --all --check && $(CARGO) clippy --all-targets -- -D warnings
+
+fmt: ## Format Rust sources with rustfmt
+ $(CARGO) fmt
+
+clean: ## Remove Cargo build artifacts
+ $(CARGO) clean
diff --git a/gui/Makefile b/gui/Makefile
new file mode 100644
index 0000000..c90d7de
--- /dev/null
+++ b/gui/Makefile
@@ -0,0 +1,46 @@
+# gui/Makefile — the Odin + raylib renderer (the interactive 3D universe map).
+#
+# This is the Odin sub-project. It is normally invoked from the repo root via
+# `make run`, `make build`, etc. (which delegate here through the root
+# Makefile), but it can also be driven directly with `make -C gui `.
+#
+# `lib/` (third-party Odin deps) lives under `gui/lib`; the `bin/` / `build/`
+# output dirs live at the repo root and are referenced through `$(ROOT)`.
+
+ODIN ?= odin
+ROOT := ..
+BIN := $(ROOT)/bin
+BINARY := $(BIN)/desi_explorer
+ODIN_FLAGS := -collection:lib=lib/local
+WASM_DEFINE := RAYLIB_WASM_LIB=env.o
+
+.PHONY: help run build build-debug build-web test clean fmt
+
+help: ## List available targets
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
+ awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}'
+
+run: ## Run the native app
+ @mkdir -p lib/local
+ $(ODIN) run src $(ODIN_FLAGS)
+
+build: ## Release build -> bin/desi_explorer
+ @mkdir -p lib/local $(BIN)
+ $(ODIN) build src $(ODIN_FLAGS) -o:speed -out:$(BINARY)
+
+build-debug: ## Debug build -> bin/desi_explorer
+ @mkdir -p lib/local $(BIN)
+ $(ODIN) build src $(ODIN_FLAGS) -o:none -debug -out:$(BINARY)
+
+build-web: ## WebAssembly build -> build/web (needs emscripten)
+ @scripts/build_web.sh
+
+test: ## Run Odin unit tests
+ @mkdir -p lib/local
+ $(ODIN) test src $(ODIN_FLAGS)
+
+clean: ## Remove build artifacts
+ rm -rf $(BIN) build
+
+fmt: ## Format Odin sources with odinfmt
+ odinfmt src
diff --git a/lib/README.md b/gui/lib/README.md
similarity index 87%
rename from lib/README.md
rename to gui/lib/README.md
index d9f04a9..3ca6200 100644
--- a/lib/README.md
+++ b/gui/lib/README.md
@@ -1,7 +1,8 @@
# Libraries
Third-party libraries for the DESI Explorer renderer live here, either as git
-submodules or vendored directly and wired in via `-collection:lib=lib/local`.
+submodules or vendored directly and wired in via `-collection:lib=lib/local`
+(as resolved from the `gui/` directory).
- `raylib` is bundled with Odin and imported as `vendor:raylib` — it does **not**
live in this directory.
diff --git a/lib/local/.gitkeep b/gui/lib/local/.gitkeep
similarity index 100%
rename from lib/local/.gitkeep
rename to gui/lib/local/.gitkeep
diff --git a/odinfmt.json b/gui/odinfmt.json
similarity index 100%
rename from odinfmt.json
rename to gui/odinfmt.json
diff --git a/ols.json b/gui/ols.json
similarity index 100%
rename from ols.json
rename to gui/ols.json
diff --git a/scripts/build_web.sh b/gui/scripts/build_web.sh
similarity index 71%
rename from scripts/build_web.sh
rename to gui/scripts/build_web.sh
index 4686ae1..6e0daa4 100755
--- a/scripts/build_web.sh
+++ b/gui/scripts/build_web.sh
@@ -1,30 +1,36 @@
#!/usr/bin/env bash
# Builds the WebAssembly ("web") build of DESI Explorer with Odin
-# (js_wasm32) + emscripten. Output: build/web/index.html (+ index.wasm, odin.js).
+# (js_wasm32) + emscripten. Output: build/web/index.html (+ index.wasm,
+# odin.js) at the repo root. Run via `make -C gui build-web`.
#
# Requires:
# - `odin` on PATH with the js_wasm32 target
# - emcc / emscripten active in PATH
# Optional:
# - ODIN_ROOT (defaults to `odin root`)
-# - OUT_DIR (defaults to build/web)
+# - OUT_DIR (defaults to /build/web)
set -euo pipefail
-OUT_DIR="${OUT_DIR:-build/web}"
+# Resolve the gui/ project root so this script works regardless of CWD.
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+GUI_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
+ROOT_DIR="$(cd "${GUI_DIR}/.." && pwd)"
+
+OUT_DIR="${OUT_DIR:-${ROOT_DIR}/build/web}"
ODIN_ROOT="${ODIN_ROOT:-$(odin root)}"
-mkdir -p lib/local
+mkdir -p "${GUI_DIR}/lib/local"
mkdir -p "$OUT_DIR"
# The Odin compiler emits a wasm object; raylib's C code is linked later by
# emcc. `-define:RAYLIB_WASM_LIB=env.o` stops the compiler from trying to
# link the raylib lib itself (emcc does that; it ends up in the final module).
-odin build src \
+odin build "${GUI_DIR}/src" \
-target:js_wasm32 \
-build-mode:obj \
-define:RAYLIB_WASM_LIB=env.o \
-o:speed \
- -collection:lib=lib/local \
+ -collection:lib="${GUI_DIR}/lib/local" \
-out:"${OUT_DIR}/game"
# Odin's JS runtime glue; the generated module imports functions from it.
@@ -36,7 +42,7 @@ emcc \
-o "${OUT_DIR}/index.html" \
"${OUT_DIR}/game.obj" \
"${ODIN_ROOT}/vendor/raylib/wasm/libraylib.web.a" \
- --shell-file scripts/web/index_template.html \
+ --shell-file "${GUI_DIR}/www/index_template.html" \
-sEXPORTED_RUNTIME_METHODS="['HEAPF32']" \
-sUSE_GLFW=3 \
-sWASM_BIGINT \
diff --git a/src/main.odin b/gui/src/main.odin
similarity index 100%
rename from src/main.odin
rename to gui/src/main.odin
diff --git a/scripts/web/index_template.html b/gui/www/index_template.html
similarity index 100%
rename from scripts/web/index_template.html
rename to gui/www/index_template.html
diff --git a/infra/Makefile b/infra/Makefile
new file mode 100644
index 0000000..8aa8367
--- /dev/null
+++ b/infra/Makefile
@@ -0,0 +1,35 @@
+# infra/Makefile — Go + Pulumi infrastructure-as-code.
+#
+# Driven directly with `make -C infra ` (or via the root Makefile's
+# `infra-*` convenience targets). Targets that interact with the live cluster
+# run Pulumi against the stack defined in Pulumi.yaml.
+
+.PHONY: help preview up down refresh static test clean tidy
+
+help: ## List available targets
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
+ awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}'
+
+preview: ## Preview infra changes against the cluster
+ pulumi preview
+
+up: ## Deploy/update infra
+ pulumi up
+
+down: ## Tear down the stack's resources
+ pulumi destroy
+
+refresh: ## Refresh Pulumi state against the live cluster
+ pulumi refresh
+
+static: ## Typecheck + vet without Pulumi
+ go build -o /dev/null . && go vet ./...
+
+test: ## Run Go unit tests
+ go test ./...
+
+clean: ## Remove local Go build artifact
+ rm -f desi-explorer-infra
+
+tidy: ## Tidy Go modules
+ go mod tidy