codecity turns any git repo into a 3D city. It walks the file tree and git history and builds a world: directories become streets, files become buildings, every commit grows a tree, and every author a firefly.
Try it on any repo (local or remote).
- Docker (Docker Desktop on macOS/Windows; engine + WSL on Windows;
dockeron Linux) - A modern browser with WebGL2 (Chrome, Safari, Firefox, Edge)
docker run --rm --init --pull=always \
-v codecity-cache:/cache \
-p 8080:8080 \
ghcr.io/thalida/codecity- Open http://localhost:8080/ to reach the Projects page
- Enter a repo URL and pick a branch
- Explore your city!
Tips
--pull=alwayskeeps you on the latest image; drop it to pin to your cached copy- Wipe the cache:
docker volume rm codecity-cache - Port in use?
-p 8081:8080
Local repo support is disabled by default. To enable it, set CODECITY_ALLOW_LOCAL_REPOS=1 and mount the directory read-only into the container at the same absolute path:
docker run --rm --init --pull=always \
-e CODECITY_ALLOW_LOCAL_REPOS=1 \
-v "$HOME/Documents/Repos:$HOME/Documents/Repos:ro" \
-v codecity-cache:/cache \
-p 8080:8080 \
ghcr.io/thalida/codecity- Use multiple
-vflags to mount more than one directory - codecity only renders git working trees:
git initfirst to render a non-git directory
For per-project ignores, drop a .codecityignore at the scan root, one pattern per line:
# Skip anywhere named "fixtures"
fixtures
# Skip a specific path (relative to scan root)
tests/fixtures/large-repo
# Un-ignore a default skip (! prefix overrides ALWAYS_SKIP)
!package-lock.jsonAlways ignored, even when tracked (! un-ignores them):
- VCS:
.git,.hg,.svn - JS:
node_modules,package-lock.json,yarn.lock,pnpm-lock.yaml,bun.lock,bun.lockb,deno.lock - Python:
.venv,venv,env,__pycache__,poetry.lock,uv.lock,Pipfile.lock - Rust:
target,.cargo,Cargo.lock - Go:
Gopkg.lock,go.sum - PHP:
composer.lock - Ruby:
Gemfile.lock - Elixir:
mix.lock - CocoaPods:
Podfile.lock - Nix:
flake.lock - Framework caches:
.next,.nuxt,.svelte-kit - Test / coverage:
.pytest_cache,.mypy_cache,.ruff_cache,.tox,.coverage,htmlcov - IDE / OS:
.idea,.vscode,.DS_Store - Generated artifacts:
sbom.json(CycloneDX / SPDX software bill of materials) - Vendored single-file amalgamations:
sqlite3.c,miniz.c,lua.c(one giant.cblob inlining a whole library: 100k+ lines that would otherwise render as a single skyscraper distorting every height-based visual)
- Height: line count (sqrt-interp across the floor range)
- Width & depth: byte size (log-interp, square footprint)
- Hue: file extension
- Saturation: last-modified (recent → vivid)
- Lightness: last-modified (recent → bright)
- Windows: lit-pane density, plus a glow that tracks how recently the file was created (newer files glow brighter)
- Aging: older files get grime streaks and a slight lean
- Media files (images, video) render an ad-panel face on the front above the door
- Width tier: descendant count (step function)
- Length: packed siblings + spacing
- Label: directory name painted on the asphalt
- Placement: oldest commit closest to the gem, newest at the edges
- Height: commit age (older = taller)
- Canopy width: files changed in that commit
- Color: commits-per-day (solo-day vs busy-day color blend)
- Color: per author. Each committer gets their own hue
- Scale: that author's total commit count
- Co-authored commits:
Co-authored-by:trailers parsed out of the commit message. Each distinct contributor on a commit gets their own firefly orbiting that tree, in their own color
- Root marker: floats above the root street
- Click: clears the selection and resets the view
| Tab | Section | Tweaks |
|---|---|---|
| World | Camera | view angle (elevation + azimuth), always aimed at the root gem |
| Scene | sky color, stars | |
| Island | silhouette, materials | |
| Buildings | floor + width ranges, per-extension hue map, palette ranges, facade detail, aging, selection-fade | |
| Streets | width tiers, spacing, colors, label typography | |
| City footprint | the paved apron under the city | |
| Gem | sizing, materials | |
| Trees | visibility, color (commits-per-day), height (by age), width (by files), outlines | |
| Fireflies | visibility, scale range, motion, orbit ring | |
| Effects | bloom, selection outline, level-of-detail | |
| Live updates | Enabled | re-render when files change on disk (off by default) |
| Poll interval | how often to check (min 1s / max 60s) | |
| Appearance | Accent | highlight color (Amber, Green, Cyan, Blue, Purple, Pink) |
| Surface | background palette (Cool, Neutral, Green, Warm) | |
| Syntax theme | file-preview highlighting |
- Clone or read: Remote repos clone into a local cache (current tree only); local folders are read in place.
- Scan: codecity reads only git-tracked files (
git ls-files), honoring.codecityignoreand the default skips, and records each file's created and last-modified dates plus each commit's files, authors, and date. - Stream: Packed into one manifest and streamed to the browser as it's computed: a skeleton city renders first as a placeholder, then fills in with the full scan.
- Layout: An off-main-thread pass packs the streets so nothing overlaps: directories become streets, files line up as buildings, subdirectories branch off at right angles.
- Build: Each building is sized from its file (height = lines, footprint = bytes), one tree per commit (oldest nearest the gem), a firefly per author.
- Render: Drawn with three.js (WebGL).
You need:
git clone https://github.com/thalida/codecity.git
cd codecity
just install-hooks # one-time: pre-push hooks (lint + prettier + tests)The pre-push hook runs the full lint + tests before pushing; bypass with git push --no-verify (Docker must be running).
| Command | What it does |
|---|---|
just dev |
Vite HMR + API auto-reload at http://<slug>.localhost:<port>/ |
just test |
pytest + vitest in containers |
just lint |
ruff, eslint, prettier, and typecheck |
just fmt |
apply Python formatting (ruff) |
just gen-types |
regenerate the frontend wire types from the OpenAPI schema |
just build |
build the local Docker image |
just run |
run the local image like an end user |
just screenshots [names] |
regenerate the README screenshots |
just demo-video |
record the README demo.mp4 |
just clean |
tear down this worktree's containers and volumes |
- Each worktree gets its own
<slug>.localhostURL, so source-picker recents stay isolated per project in localstorage just devandjust runtake a path arg to mount a local repo (just dev ~/Documents/Repos/myproj), which also setsCODECITY_ALLOW_LOCAL_REPOS=1; without it, codecity is git-URL-only
- FastAPI on uvicorn, single process by design (the in-memory scan-root trust set in
api/security.pycan't be split across workers) - scan progress streams over Server-Sent Events (
GET /api/manifest) - API docs at
/api/docs(Scalar); raw schema at/api/openapi.json - that schema is the source of truth for the generated frontend wire types (
just gen-types, drift-guarded byapp/src/types/manifest.contract.ts)
just release v0.2.0just release:
- verifies you're on a clean
mainin sync with origin - creates an annotated tag and pushes it
Pushing the tag triggers GitHub Actions, which:
- builds a multi-arch image (linux/amd64 + linux/arm64)
- pushes to
ghcr.io/thalida/codecitywith all tag aliases - signs with cosign (keyless via OIDC)
- smoke-tests via
/api/health - creates a GitHub Release
cosign verify \
--certificate-identity-regexp 'https://github.com/thalida/codecity/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/thalida/codecity:v0.2.0





