---
title: lecodes — command reference
---

# lecodes — command reference

Generated from the command definitions by `bun run docs`; do not edit by hand.

## Project

### `lecodes init [dir]`

Scaffold a new LOCAL project (no login, no server)

Writes a starter main.ts, a CLAUDE.md + a passing flow test (the headless render / test loop), the IDE types + tsconfig, and a .lecodesignore. Run it with `lecodes dev` or `lecodes compile`. Idempotent: existing files are kept.

| Option | Meaning |
|---|---|
| `--desktop` | also write an app.json declaring the desktop target (window size; `dev --desktop`, `desktop run` and headless render / test pick it up) |

```
lecodes init myapp
lecodes init --desktop
```

### `lecodes create <name> [dir]`

Create a new project on the server and scaffold it locally

| Option | Meaning |
|---|---|
| `--team <uuid>` | create it inside a team |

### `lecodes clone <uuid|url> [dir]`

Download a project's files + types into a new folder

| Option | Meaning |
|---|---|
| `--no-types` | skip the .d.ts / tsconfig generation |

```
lecodes clone https://le.codes/app/projects/<uuid> myapp
```

### `lecodes link [dir]`

Connect an existing local folder to the platform (the reverse of clone)

Creates a project on the server, writes .lecodes/manifest.json and refreshes the types; nothing is uploaded until you push, so `status` shows what would go up first. `--project` adopts an EXISTING project instead: the manifest is built from its server tree and the command prints exactly what your next push would create / update / delete there before linking.

| Option | Meaning |
|---|---|
| `--name <s>` | project name (default: the folder name) |
| `--team <uuid>` | create it inside a team |
| `--project <uuid|url>` | adopt an existing project instead of creating one |
| `--push` | push right after linking (--publish to deploy it live in the same step) |
| `--no-types` | skip the .d.ts / tsconfig refresh |
| `-m, --message <text>` | checkpoint message (asked for when omitted) |
| `--publish` | also release the project with the push (= `lecodes publish`) |
| `--force-publish` | --publish, and release even when nothing changed — a new SDK on the build service (= `lecodes publish --force`) |
| `--force` | push even if the server moved on (shows + confirms which server files get overwritten) |
| `-y, --yes` | skip the --force overwrite confirmation |

```
lecodes link
lecodes link --project <uuid> && lecodes status
lecodes link --push -m "initial"
```

### `lecodes status`

Show local changes vs the last sync

### `lecodes diff [path…]`

Show a unified diff of local vs the current server state

| Option | Meaning |
|---|---|
| `--name-only` | list changed paths + status only |

### `lecodes pull [path…]`

Overwrite local files with the project's current server state

With paths: pull only those files / folders and keep every other local edit (no dirty check; you still owe a full pull or --force before a clean push).

| Option | Meaning |
|---|---|
| `--force` | discard uncommitted local changes |
| `--no-types` | skip the .d.ts / tsconfig refresh |

### `lecodes push`

Push local changes as one new checkpoint

| Option | Meaning |
|---|---|
| `-m, --message <text>` | checkpoint message (asked for when omitted) |
| `--publish` | also release the project with the push (= `lecodes publish`) |
| `--force-publish` | --publish, and release even when nothing changed — a new SDK on the build service (= `lecodes publish --force`) |
| `--force` | push even if the server moved on (shows + confirms which server files get overwritten) |
| `-y, --yes` | skip the --force overwrite confirmation |

```
lecodes push -m "Tweak the menu"
lecodes push --publish
lecodes push --force-publish
```

### `lecodes publish`

Release the project: push local changes, build, make it the live version

The editor's Publish button. Local changes are pushed first as a checkpoint ("Release", or -m). The platform builds the project, deploys its server to production and makes the build the current release at https://<handle>.le.codes — what the viewers and the store run. A project that builds to what is live already makes no release: "Nothing changed — <version> is current" (--force releases it all the same). A schema change that deletes production data is shown and asked about; without a terminal it needs --allow-destructive. `lecodes releases` lists the releases, `lecodes releases use <version>` rolls back.

| Option | Meaning |
|---|---|
| `-m, --message <text>` | checkpoint message when there are local changes (default "Release") |
| `--force` | rebuild even when nothing changed — a new SDK on the build service |
| `--allow-destructive` | apply a schema change that deletes production data without asking |

```
lecodes publish
lecodes publish -m "Checkout v2"
lecodes publish --force
lecodes publish --allow-destructive
```

### `lecodes releases`

List the project's releases (● = the current one); `releases use <version>` rolls back, `releases upload <app.js>` releases a bundle compiled here

Every `lecodes publish` (or the editor's Publish button) makes a release: an immutable build with its own version. The current one is what https://<handle>.le.codes, the viewers and the store run.

```
lecodes releases
lecodes releases use 1.2.3
lecodes releases upload app.js
```

#### `lecodes releases use <version>`

Make a release current again — a rollback, no compile

The release's bundle becomes what the project's address, the viewers and the store run, and its server is deployed to production again (the database migrates to that release's schema: a change that deletes data is shown and asked about, like a publish).

| Option | Meaning |
|---|---|
| `--allow-destructive` | apply a schema change that deletes production data without asking |

```
lecodes releases use 1.2.3
```

#### `lecodes releases upload <app.js>`

Make a bundle compiled here the current release — no build on the platform

For a bundle the platform's build service cannot make any more: compiled with an older SDK's CLI (`compile --remote-assets`, so it names the project's uploaded resources), it becomes the current release with its own `// sdk:` — what the viewers of that SDK line run. Local changes are pushed first as the release's checkpoint. A project with server code cannot take one (its server is built with the client).

| Option | Meaning |
|---|---|
| `-m, --message <text>` | checkpoint message when there are local changes (default "Release") |

```
lecodes releases upload app.js
lecodes releases upload build/app.js -m "Kungur for the 2.1 app"
```

### `lecodes login`

Authorize in the browser and store an access token

Opens the browser to sign in / authorize on le.codes and captures the token. `--token` stores a token created in the web UI instead (headless / CI). LECODES_API / LECODES_TOKEN in the environment override the stored values.

| Option | Meaning |
|---|---|
| `--token <pat>` | store a personal access token from the web UI instead of the browser flow |
| `--api <url>` | API origin (default: the stored one, else https://le.codes) |
| `--web <url>` | app origin for the browser flow (default: the API origin) |

## Run & test

### `lecodes dev`

Run the project on a device / the desktop / the browser from this machine, with hot reload

Starts a LAN dev server + prints a QR code; scan it with the LeCodes app. Saves recompile + hot-swap the running app; device logs/errors stream back here. No login needed. The /web/ URL (the web player) is served always; --web, --android and --desktop each also open the app on that host.

| Option | Meaning |
|---|---|
| `--port <n>` | server port (default `4499`) |
| `--host <ip>` | LAN address to advertise (default: auto-detect) |
| `--entry <path>` | compile from this entry instead of the detected one |
| `--no-qr` | don't render the QR code |
| `--android` | open the dev url in the LeCodes app on the phone plugged in over adb (adb reverse: no Wi-Fi/LAN address needed); device logs stream here; Ctrl+C force-stops the app |
| `--serial <id>` | (--android) the device to use |
| `--apk <file>` | (--android) install this build first |
| `--package <name>` | (--android) the app's package (default com.letary.lecodes) |
| `--time <s>` | (--android \| --ios) bounded check: screenshot after this many seconds, force-stop, exit (exit 1 on a crash) |
| `--png <file>` | (--android \| --ios, with --time) where the device screenshot goes |
| `--ios` | open the dev url in the LeCodes app on a booted simulator or a plugged-in iPhone (Mac + Xcode: simctl / devicectl); a simulator gets the 127.0.0.1 url, a phone the LAN url; Ctrl+C terminates the app |
| `--udid <id>` | (--ios) the simulator (udid or name) or device (udid or name) to use |
| `--app <dir>` | (--ios) install this LeCodesApp.app first |
| `--bundle <id>` | (--ios) the app's bundle id (default com.letary.lecodes) |
| `--web` | also open the app in the browser (the web player in a device: same bundle + hot reload, the console) |
| `--desktop` | also open the app in the native desktop host (same hot reload; window from app.json's desktop block; closing it keeps the server) |
| `--fps` | (--desktop) draw the host's frame counter |

```
lecodes dev
lecodes dev --web
lecodes dev --android --time 12 --png phone.png
lecodes dev --ios --time 12 --png sim.png
lecodes dev --desktop --fps
```

### `lecodes web`

The project as a web page (Web Lite): serve it with reload on save, build it for a hosting

Web Lite shows the app with the browser's own layout, text and scroll: what a site ships. UI apps for now — a project's 3D and 2D scenes are not shown yet.

#### `lecodes web dev`

Open the project as a web page, reloaded on every save

Serves the project's bundle and assets with the page of Web Lite around them; a save recompiles and runs the app again, the page's console output and errors stream back here. The address's hash is the route the app is opened with (#/product/42 → app.launchUrl). A web half of the project's own (app.json `"ar": { "web": "./plugins/ar-8thwall" }`) is built with every compile and served beside the page, its `web.static` folder from the project.

`--https`: the page over https, for a phone's camera and motion sensors (a browser gives them to a secure page only) — a self-signed certificate for localhost and this machine's LAN addresses, made once and kept in ~/.lecodes/dev-https/. The phone's browser warns about it: accept it once.

| Option | Meaning |
|---|---|
| `--port <n>` | server port (default `4488`) |
| `--entry <path>` | compile from this entry instead of the detected one |
| `--https` | serve over https with a self-signed certificate (a phone's camera and sensors need it) |
| `--no-open` | don't open the browser |

```
lecodes web dev
lecodes web dev --port 8080 --no-open
lecodes web dev --https
```

#### `lecodes web build`

Build the project into a folder any static hosting serves

Writes the page, the bundle, the page's own files and the assets the bundle names into one folder (dist/web). Every url in it is relative: the folder works at a domain's root and inside a folder of one.

app.json `web` sets the page: `title`, `themeColor`, `lang`, and `urls` — "hash" (default: a route is `#/product/42`, which any hosting serves) or "path" (`/product/42` under `base`: the hosting has to answer every path with the page; a 404.html is written for the ones that do it that way).

| Option | Meaning |
|---|---|
| `-o, --out <dir>` | the folder to write (cleaned first) (default `dist/web`) |
| `--entry <path>` | compile from this entry instead of the detected one |
| `--sourcemap` | inline the source map (errors name the app's own lines; the bundle grows by its sources) |
| `--no-minify` | keep the bundle readable (no compress / mangle) |

```
lecodes web build
lecodes web build -o public
```

#### `lecodes web preview [dir]`

Serve a built folder, as a static hosting would

| Option | Meaning |
|---|---|
| `--port <n>` | server port (default `4489`) |
| `--no-open` | don't open the browser |

```
lecodes web preview
lecodes web preview public --port 8080
```

### `lecodes compile`

Compile the local project to one JS bundle (stdout, or -o <file>)

Works on a cloned / created project (binary asset URLs resolved from the server) or a local `init` project (no login — asset() resources resolve on the host via fetchLocal, so the bundle is self-contained for LeCodes Desktop). Diagnostics go to stderr; the bundle is the only thing on stdout.

| Option | Meaning |
|---|---|
| `-o, --out <file>` | write to this file instead of stdout |
| `--entry <path>` | compile from this entry instead of the detected one |
| `--public-url <url>` | resolve assets from this URL prefix (default: the API origin / the working tree) |
| `--remote-assets` | desktop hosts: load asset() resources from the server instead of the working tree |
| `--no-header` | omit the metadata header |
| `--no-minify` | keep the bundle readable (no compress / mangle) |
| `--no-sourcemap` | don't generate / inline the source map |

```
lecodes compile -o app.js
lecodes compile --no-minify > app.js
```

### `lecodes render [file]`

Render the local project headless (JSON / PNG), or through the desktop host

Needs the optional lecodes-headless package (installed on first use). [file] renders ONE screen module instead of the app: a file that default-exports a UIScreen (or a (state) => UIScreen) — every `lecodes design` screen is written that way. A bare id ("login") resolves in design/screens/, and a design screen renders at its meta.json frame.

asset() resources load from the working tree by default; --remote-assets / --public-url use the server's copies (then data needs --wait-network).

| Option | Meaning |
|---|---|
| `--state <name>` | ([file]) the state to pass a (state) => UIScreen screen |
| `--dir <folder>` | ([file]) the design folder a bare screen id resolves in (default design/) |
| `-o, --out <file>` | write to this file instead of stdout |
| `--png [file]` | rasterize a PNG (default screenshot.png; needs @napi-rs/canvas) |
| `--map [file]` | draw the 3D scene's annotated top-down map (grid + XZ footprints + named markers) as <file>.png + <file>.json world-space data (default map.png; also a { "map": "f.png" } script step) |
| `--label-all` | (--map) label every object, not only the named markers |
| `--max-px <n>` | (--map) longest edge of the map image, px |
| `--script <file>` | drive the app through a step list (tap/type/fill/scroll/hover/wait/expect/screenshot/json steps; games: frames/drag/hold/key/keyDown/keyUp, expect.log, screenshot clip). With --desktop the same steps, targets (a name or { x, y }) and expects run on the native host — only expect.audio and the json output are headless-only; per-step screenshots + final --png |
| `--out-dir <dir>` | where --script step outputs land (default `.`) |
| `--clip <name|x,y,w,h>` | (--png) crop to one node's rect or a logical-px rect — a component / region render |
| `--fixtures <file>` | canned fetch responses (URL pattern → { status?, json?, text? }) — make backend-gated screens reachable headless; merged over the scenario file's own |
| `--scale <n>` | PNG device-pixel scale (default `2`) |
| `--time <ms>` | render at this animation time (0 = the initial frame-0 state) |
| `--no-after-animations` | keep frame 0 instead of advancing animations to their resting state |
| `--timeout <ms>` | hard cap on the whole render before aborting (default 15000, + 3000 per script step; --desktop 60000, + 5000 per step) |
| `--width <n>` | viewport width, logical px (default 390 — a desktop-only project renders at its window, a design screen at its board frame) |
| `--height <n>` | viewport height, logical px (default 844, same fallbacks) |
| `--settle <ms>` | max wait for promises / timers / fetch / image decode before the snapshot (ends early when idle) |
| `--wait-network` | let data fetches hit the real network (off by default → empty responses) |
| `--device <preset>` | safe-area preset: none \| iphone \| android (default: orientation-based) |
| `--safe-area <t,r,b,l>` | override the safe-area insets (px) the safe-* keywords resolve to |
| `--logs` | forward the app's console output (to stderr) |
| `--native-view <name=label,…>` | (headless) draw these native views as a placeholder — the label in their box — instead of having none: map=Map,camera (a bare name is its own label); calls to them reject |
| `--entry <path>` | compile from this entry instead of the detected one |
| `--public-url <url>` | resolve assets from this URL prefix (default: the API origin / the working tree) |
| `--remote-assets` | desktop hosts: load asset() resources from the server instead of the working tree |
| `--no-header` | omit the metadata header |
| `--no-minify` | keep the bundle readable (no compress / mangle) |
| `--no-sourcemap` | don't generate / inline the source map |
| `--desktop` | render through the NATIVE desktop host instead: real Filament 3D + tgfx UI + 2D, composited. PNG only (--png). Downloads the LeCodes Desktop renderer on first use into ~/.lecodes/renderer (Windows/Linux; LECODES_DESKTOP_EXE overrides) |
| `--sim` | run a --script scenario through the WINDOWLESS native host (lecodes-server, beside the desktop host; LECODES_SERVER_EXE overrides): the real engine — Jolt, animators, navmesh — with no GPU and no pixels, so it runs on a CI box or a container and in parallel with an open desktop window. expect.log / expect.paint only; input is scene input (key/look/stick/drag/tap by { x, y } straight to the scene — no UI, so no names, hover or type); no screenshots, no --png |
| `--frames <n>` | (--desktop) frames to run before the shot — headroom for async loads (default `60`) |
| `--fixed-dt <ms>` | (--desktop) virtual app clock, ms per frame — deterministic game time (scripted runs default to 16; plain renders to wall clock) |
| `--renderer <gl|vulkan>` | (--desktop) host + shader variant: gl \| vulkan (default: app.json desktop.renderer, else gl) |
| `--view <name>` | (--desktop) inspection camera auto-fitted to the whole 3D scene: iso \| top \| front \| side \| back [--fov N]; the app's camera is untouched |
| `--camera <x,y,z>` | (--desktop) inspection camera at a fixed eye point (world units, Y up) — goes with --look-at [--fov N]. In a --script run use { "view" } / { "camera" } steps |
| `--look-at <x,y,z>` | (--desktop) the point --camera looks at |
| `--fov <deg>` | (--desktop) vertical field of view for --view / --camera, degrees |

```
lecodes render -o ui.json
lecodes render --png shots/home.png --device iphone
lecodes render login --state empty --png
lecodes render --script tests/hero.flow.json --out-dir tests/.shots --logs
lecodes render --desktop --png --view iso
```

### `lecodes test [name…]`

Run the flow tests (tests/*.flow.json + design-derived flows)

Headless flow tests: tests/*.flow.json scenarios, plus flows derived from the design graph (design/meta.json activator edges — checks the built app against the designed navigation; needs named elements + screen roots). Failing steps dump the frame (JSON + PNG) under tests/.artifacts/<flow>/. A project with *.server.ts files gets its backend run for the tests, on a database of its own (.lecodes/test-db) that is emptied before every case; a step waits for the calls it started.

Design flows read tests/fixtures.json automatically (canned fetch responses) so backend-gated transitions run headless; file scenarios declare their own via the "fixtures" envelope. [name…] runs only those files (a path, or a name under the tests folder).

| Option | Meaning |
|---|---|
| `--flows` | run ONLY the design-derived flows |
| `--no-flows` | run ONLY the tests/*.flow.json files |
| `--fixtures <file>` | fixture pack for the design flows (overrides the tests/fixtures.json default) |
| `--json` | machine-readable report on stdout |
| `--dir <folder>` | tests folder (default `tests`) |
| `--design-dir <dir>` | design folder (default `design`) |
| `--artifacts <dir>` | failure-artifact folder override (default <tests>/.artifacts) |
| `--timeout <ms>` | hard cap on the whole run (default 20000 + 10000 per case; --desktop 60000 + 60000 per case) |
| `--width <n>` | viewport width, logical px (default 390 — a desktop-only project renders at its window, a design screen at its board frame) |
| `--height <n>` | viewport height, logical px (default 844, same fallbacks) |
| `--settle <ms>` | max wait for promises / timers / fetch / image decode before the snapshot (ends early when idle) |
| `--wait-network` | let data fetches hit the real network (off by default → empty responses) |
| `--device <preset>` | safe-area preset: none \| iphone \| android (default: orientation-based) |
| `--safe-area <t,r,b,l>` | override the safe-area insets (px) the safe-* keywords resolve to |
| `--logs` | forward the app's console output (to stderr) |
| `--native-view <name=label,…>` | (headless) draw these native views as a placeholder — the label in their box — instead of having none: map=Map,camera (a bare name is its own label); calls to them reject |
| `--entry <path>` | compile from this entry instead of the detected one |
| `--public-url <url>` | resolve assets from this URL prefix (default: the API origin / the working tree) |
| `--remote-assets` | desktop hosts: load asset() resources from the server instead of the working tree |
| `--no-header` | omit the metadata header |
| `--no-minify` | keep the bundle readable (no compress / mangle) |
| `--no-sourcemap` | don't generate / inline the source map |
| `--desktop` | run the file scenarios through the NATIVE host: the same steps, targets and expects as headless (expect.audio and the json output excepted); design flows stay headless. --frames / --fixed-dt as in render --desktop |
| `--sim` | run a --script scenario through the WINDOWLESS native host (lecodes-server, beside the desktop host; LECODES_SERVER_EXE overrides): the real engine — Jolt, animators, navmesh — with no GPU and no pixels, so it runs on a CI box or a container and in parallel with an open desktop window. expect.log / expect.paint only; input is scene input (key/look/stick/drag/tap by { x, y } straight to the scene — no UI, so no names, hover or type); no screenshots, no --png |
| `--frames <n>` | (--desktop) frames to run before the shot — headroom for async loads (default `60`) |
| `--fixed-dt <ms>` | (--desktop) virtual app clock, ms per frame — deterministic game time (scripted runs default to 16; plain renders to wall clock) |
| `--renderer <gl|vulkan>` | (--desktop) host + shader variant: gl \| vulkan (default: app.json desktop.renderer, else gl) |
| `--lite` | run the file scenarios on Web Lite, in a browser (needs Playwright): the same steps, targets and expects; the clock is the browser's, so a step that expects a log line waits for it; design flows stay headless |
| `--web` | run the file scenarios on the web player, in a browser (needs Playwright): as --lite, on the runtime as wasm; no `fill` step (tap a field and `type`) |

```
lecodes test
lecodes test hero --logs
lecodes test --flows --json
lecodes test --desktop
```

### `lecodes desktop`

The desktop host: status, run the project in a window, build a portable app, update

Bare `lecodes desktop` prints the version status: the host renders use, what is cached in ~/.lecodes/renderer, the newest published one. LECODES_DESKTOP_EXE points every desktop command at a local host build instead.

#### `lecodes desktop run`

Compile the current project and open it in a desktop window

Window size / min size / title default from app.json's desktop block; asset() resources load from the working tree. A project with *.server.ts files gets its backend run on this machine for as long as the window is open (the database of `lecodes dev`: .lecodes/db). Multiplayer: --server starts the host as a dedicated server, --connect joins one — both only set Net.launch (the app decides).

| Option | Meaning |
|---|---|
| `--width <n>` | window width (logical px) |
| `--height <n>` | window height (logical px) |
| `--title <s>` | window title |
| `--fullscreen` | force fullscreen (over app.json) |
| `--windowed` | force windowed (over app.json) |
| `--scale <n|fit>` | display scale: 0.5..4, "fit" (size a 1080p-designed app to the monitor), or "system" |
| `--render-scale <n>` | 3D render resolution 0.25..1 (over app.json desktop.renderScale) |
| `--renderer <gl|vulkan>` | host + shader variant: gl \| vulkan (default: app.json desktop.renderer, else gl) |
| `--fps` | draw the host's frame counter in the corner (composited fps + the 3D fps when they diverge) |
| `--perf` | the host's profiler: a [perf] line a second (GPU frame time, CPU split, what the camera sees) + a second overlay row; implies --fps |
| `--server` | launch as a multiplayer server |
| `--port <n>` | (--server) listen port |
| `--max-clients <n>` | (--server) client cap |
| `--connect <ip:port>` | join a server as a client |
| `--entry <path>` | compile from this entry instead of the detected one |
| `--public-url <url>` | resolve assets from this URL prefix (default: the API origin / the working tree) |
| `--remote-assets` | desktop hosts: load asset() resources from the server instead of the working tree |
| `--no-header` | omit the metadata header |
| `--no-minify` | keep the bundle readable (no compress / mangle) |
| `--no-sourcemap` | don't generate / inline the source map |

```
lecodes desktop run
lecodes desktop run --scale fit --fps
lecodes desktop run --perf
lecodes desktop run --server --port 7777
```

#### `lecodes desktop build`

Stage a portable desktop app folder (dist/<name>/): host exe + app.js + assets + runtime

The cached host renamed to <Name>.exe + app.js + assets/ (the project's files) + runtime/ (the engine's) + app.json (the host reads title/size/fullscreen from it) + app.ico from app.json "icon". Duplicate asset basenames are a build error (they silently alias in the bundle). Also spelled `lecodes app build desktop`.

On macOS: a signed .app instead (the downloaded LeCodes Desktop.app + the project inside it); app.json desktop.macos = bundleId / buildNumber / category / signing.identity / notarize.profile (a notarytool keychain profile — notarized + stapled, so it opens on any Mac without "Open Anyway"). --sign / --notarize / --no-notarize override it per build; LECODES_MACOS_SIGN_ID / LECODES_NOTARY_PROFILE sit between the flags and app.json.

| Option | Meaning |
|---|---|
| `-o, --out <dir>` | output folder (default dist/<name>) |
| `--slug <name>` | folder / zip name override (Latin-only) |
| `--renderer <gl|vulkan>` | host + shader variant: gl \| vulkan (default: app.json desktop.renderer, else gl) |
| `--probe` | headless-render the staged folder to a PNG next to it (macOS: a brief windowed run + snapshot) |
| `--frames <n>` | (--probe) frames to render before the snapshot (default `90`) |
| `--after <n>` | (--probe, macOS) seconds the app runs before the snapshot (default `8`) |
| `--zip` | also pack the result for sending: <out>.zip with UTF-8 / forward-slash entries (macOS: a ditto archive of the .app) |
| `--dmg` | macOS: also build a drag-to-Applications disk image (signed + notarized too) |
| `--sign <identity|adhoc>` | macOS codesign identity — a "Developer ID Application: …" cert, or adhoc (local only) |
| `--notarize <profile>` | macOS: notarize + staple with this `xcrun notarytool store-credentials` keychain profile |
| `--no-notarize` | macOS: skip notarization even when app.json / the environment names a profile |
| `--entry <path>` | compile from this entry instead of the detected one |
| `--public-url <url>` | resolve assets from this URL prefix (default: the API origin / the working tree) |
| `--remote-assets` | desktop hosts: load asset() resources from the server instead of the working tree |
| `--no-header` | omit the metadata header |
| `--no-minify` | keep the bundle readable (no compress / mangle) |
| `--no-sourcemap` | don't generate / inline the source map |

```
lecodes desktop build
lecodes app build desktop --zip --probe
lecodes desktop build -o out/kiosk --renderer vulkan
lecodes app build desktop --dmg --notarize MyProfile
```

#### `lecodes desktop update`

Move the host to the newest published release

### `lecodes serve`

Run the current project as a dedicated multiplayer server (windowless host: no window, no GPU)

Compiles the project like `desktop run` and starts it in the windowless host with --server, so the app's Net.launch.role is "server" and its Net.listen() opens the port for clients (`desktop run --connect <ip:port>` on the same LAN, or a published game). Prints the LAN addresses clients can dial. The host runs the real engine without a GPU, so it also runs on a CI box or a container.

--connect turns the same host into a headless client (a bot / a scripted test player) instead of a server.

| Option | Meaning |
|---|---|
| `--port <n>` | listen port (default `40000`) |
| `--max-clients <n>` | client slots (default `16`) |
| `--connect <ip:port>` | join a server as a headless client instead of serving |
| `--entry <path>` | compile from this entry instead of the detected one |
| `--public-url <url>` | resolve assets from this URL prefix (default: the API origin / the working tree) |
| `--remote-assets` | desktop hosts: load asset() resources from the server instead of the working tree |
| `--no-header` | omit the metadata header |
| `--no-minify` | keep the bundle readable (no compress / mangle) |
| `--no-sourcemap` | don't generate / inline the source map |

```
lecodes serve
lecodes serve --port 7777 --max-clients 32
lecodes serve --connect 192.168.1.10:40000
```

## Editors

### `lecodes design`

LeCodes Design — a live canvas of screens for AI-driven prototyping

Bare `lecodes design` starts the canvas (= `design serve`). Needs the optional lecodes-design package (installed on first use); works inside a cloned project or standalone in any folder.

The design folder (default design/) holds one file per screen (screens/<id>.ts default-exports a UIScreen), shared tokens and components (shared/), and meta.json — the human-owned graph of positions, descriptions and edges. Screens compile per file through the normal project pipeline and render live in the browser and headless for AI feedback.

| Option | Meaning |
|---|---|
| `--dir <folder>` | design folder (default `design`) |
| `--port <n>` | dev-server port (default `4477`) |
| `--no-open` | don't open the browser |
| `--no-sync` | don't reconcile comments with the platform while serving |

```
lecodes design init && lecodes design
lecodes design --port 4480 --no-open
lecodes design snapshot --png
```

#### `lecodes design serve`

Start the dev server + browser canvas (what bare `lecodes design` does)

| Option | Meaning |
|---|---|
| `--dir <folder>` | design folder (default `design`) |
| `--port <n>` | dev-server port (default `4477`) |
| `--no-open` | don't open the browser |
| `--no-sync` | don't reconcile comments with the platform while serving |

```
lecodes design serve --port 4480 --no-open
```

#### `lecodes design init`

Scaffold the design folder (screens/, shared/, meta.json, spec.md, CLAUDE.md)

Existing files are kept. Also ignores .snapshots/ and comments/ in .lecodesignore, registers the board's MCP server in .mcp.json (so Claude Code reaches it), and writes the IDE types + a design-scoped tsconfig so screens typecheck standalone or in a project.

| Option | Meaning |
|---|---|
| `--dir <folder>` | design folder (default `design`) |
| `--port <n>` | dev-server port to register in .mcp.json (default `4477`) |
| `--desktop` | desktop-frame board (meta.json defaultSize [1280, 800] instead of the phone frame) |

```
lecodes design init
lecodes design init --desktop
```

#### `lecodes design snapshot [screen…]`

Render design screens headless to JSON / PNG (needs lecodes-headless)

One screen → its JSON on stdout; otherwise one file per screen in <dir>/.snapshots/ (<id>.json, <id>@<state>.json, PNGs with --png). --state and --all-states need lecodes-design to discover a screen's states from its source. A stuck screen (runaway loop) is killed after 120 s.

| Option | Meaning |
|---|---|
| `--dir <folder>` | design folder (default `design`) |
| `--state <name>` | render this state of every (state) => UIScreen screen |
| `--all-states` | render every discovered state of each screen |
| `--png [file]` | also rasterize PNGs at 2× (needs @napi-rs/canvas); a value is accepted and ignored |
| `--out-dir <dir>` | output folder (default <dir>/.snapshots) |
| `--logs` | forward the screens' console output (to stderr) |

```
lecodes design snapshot
lecodes design snapshot login
lecodes design snapshot --all-states --png
lecodes design snapshot login --state error
```

#### `lecodes design arrange`

Auto-layout the flow graph in meta.json

Re-lays-out every screen and always applies (the board offers Undo; the layout score delta is printed, lower is better). A running dev server picks the change up and refreshes.

| Option | Meaning |
|---|---|
| `--dir <folder>` | design folder (default `design`) |
| `--new` | only place screens without a pos yet; never move the rest |

```
lecodes design arrange
lecodes design arrange --new
```

#### `lecodes design check`

Static coherence check: flow warnings, spec.md links, states, tab bars, open comments

The terminal / CI sibling of the board's check_concept tool. Hard warnings (broken edges, missing screens, undeclared states, dangling [[spec]] links) exit non-zero; hints and open comments never fail it. No compile, no render — needs only lecodes-design.

| Option | Meaning |
|---|---|
| `--dir <folder>` | design folder (default `design`) |

```
lecodes design check
```

#### `lecodes design share`

Turn the public share link on (or --off); --comments on|off gates visitors' comments

Needs a cloned project whose design is already pushed — this only flips the link, nothing is uploaded. Anyone with the link can view the mockups; the app code stays private.

| Option | Meaning |
|---|---|
| `--dir <folder>` | design folder (default `design`) |
| `--off` | revoke the share link |
| `--comments <on|off>` | let link visitors leave comments (existing ones stay readable either way) |

```
lecodes design share
lecodes design share --comments off
lecodes design share --off
```

#### `lecodes design comments`

Review comments pinned to the board, grouped by screen (open first)

Reads <dir>/comments/ directly, no server needed. Inside a cloned project those files mirror the platform's open threads (`comments pull` refreshes them; the running board keeps them in sync); standalone they are the store. Writes (add / reply / resolve) queue locally and go up with the next `lecodes push`, after the design they describe.

| Option | Meaning |
|---|---|
| `--dir <folder>` | design folder (default `design`) |
| `--all` | include resolved threads |

```
lecodes design comments
lecodes design comments --all
```

##### `lecodes design comments add "<text>"`

Start a thread from the terminal, pinned to a screen

| Option | Meaning |
|---|---|
| `--dir <folder>` | design folder (default `design`) |
| `--screen <id>` | pin it to this screen (checked against the design) |
| `--state <s>` | …in this state of the screen |
| `--element <name>` | …on this named element |
| `--ai` | sign the write as Claude (automatic inside a Claude Code session) |

```
lecodes design comments add "Is the CTA meant to be this small?" --screen login --element cta
```

##### `lecodes design comments reply <number> "<text>"`

Answer a thread

| Option | Meaning |
|---|---|
| `--dir <folder>` | design folder (default `design`) |
| `--ai` | sign the write as Claude (automatic inside a Claude Code session) |

```
lecodes design comments reply 3 "Bumped it to 48px — see the new login state."
```

##### `lecodes design comments resolve <number>`

Close a thread (or reopen it), optionally replying first

| Option | Meaning |
|---|---|
| `--dir <folder>` | design folder (default `design`) |
| `--reply <text>` | post this reply before closing |
| `--reopen` | reopen the thread instead of closing it |
| `--ai` | sign the write as Claude (automatic inside a Claude Code session) |

```
lecodes design comments resolve 3 --reply "Done."
lecodes design comments resolve 3 --reopen
```

##### `lecodes design comments pull`

Mirror the platform's open threads into <dir>/comments/

Inbound only — writes go the other way with `lecodes push`, never on their own. Needs a cloned project; a standalone folder's comments/ is already the only store.

| Option | Meaning |
|---|---|
| `--dir <folder>` | design folder (default `design`) |

```
lecodes design comments pull
```

### `lecodes scene [path]`

The scene editor: edit a .scene.ts visually (tree, inspector, gizmo)

Needs the optional lecodes-3d-editor package (installed on first use). Works inside a cloned project or standalone in any folder; `path` is a folder or a .scene.ts to host. Registers an MCP server in .mcp.json so Claude Code can call scene_state / check_scene.

| Option | Meaning |
|---|---|
| `--port <n>` | editor-server port (default `4488`) |
| `--no-open` | don't open the browser |
| `--no-mcp` | don't register the MCP server in .mcp.json |

```
lecodes scene
lecodes scene src/forest.scene.ts --no-open
```

### `lecodes thumbs [path…]`

Asset thumbnails for the scene editor, without a browser

Decodes the project's images (KTX2 too, via KTX-Software's ktx tool) into the editor's preview cache. Incremental: the cached name carries the asset's content hash, so unchanged files are skipped. Models, materials and prefabs are rendered by the editor, not here.

| Option | Meaning |
|---|---|
| `--force` | redo even the up-to-date ones |
| `--size <n>` | longest edge, px (default `256`) |

## Content

### `lecodes assets <command> [args…]`

The asset pipeline: convert / probe / doctor / unwrap / retarget / unpack / scene / sky / terrain-pack …

```
lecodes assets convert hero.fbx --clips anims/*.fbx -o hero.glb
lecodes assets doctor hero.glb --fix --ktx2
lecodes assets --help
```

### `lecodes shaders`

The project's custom shaders (.mat): list, compile, update matc, new

Bare `lecodes shaders` lists the project's shaders with their compile state and the matc status. matc (Filament's material compiler) caches in ~/.lecodes/matc; LECODES_MATC points at a local build.

```
lecodes shaders
lecodes shaders compile
lecodes shaders new hologram --ref lit
```

#### `lecodes shaders compile`

Compile every shader into .lecodes/shaders (the pass `lecodes dev` runs on the fly)

Downloads matc on first use (~/.lecodes/matc). Every target is compiled by default (opengl, metal, desktop, desktop-vulkan, webgl-lite) because one bundle serves every host; --platform narrows it when you know the target. Incremental per artifact.

| Option | Meaning |
|---|---|
| `--platform <a,b>` | only these targets, comma-separated (opengl, metal, desktop, desktop-vulkan, webgl-lite) |

```
lecodes shaders compile
lecodes shaders compile --platform desktop
```

#### `lecodes shaders update`

Download the newest published matc (run after an engine upgrade)

#### `lecodes shaders new <name>`

Write a fresh .mat + .material.ts: the starter, or a copy of an engine material

The starter is an unlit material with an annotated parameter of each kind. `--ref lit|unlit|particles|…` copies the engine's own source instead, renamed after the file — the way to get vertex colours, an emissive term or a second map onto an otherwise standard material without touching the engine. A <name>.material.ts asset (defineMaterial with the declared parameters) goes next to it.

| Option | Meaning |
|---|---|
| `--ref <name>` | start from the engine's material of that name (lit, unlit, particles, …) |
| `--dir <folder>` | write into this folder instead of the current dir |
| `--force` | overwrite existing files |

```
lecodes shaders new hologram
lecodes shaders new rim-light --ref lit --dir assets/shaders
```

### `lecodes lightmap`

Baked lighting: bake the level's statics, read a bake's numbers, inspect its layers

#### `lecodes lightmap bake`

Bake the sun, the sky and the lamps into a lux atlas for the level's statics

Runs the app in bake mode in lecodes-bake — the windowless host with the bake built in (no physics, 2D, audio, net or navmesh), downloaded on first use like the desktop host; LECODES_BAKE_EXE points at a local build (hosts/desktop build.ps1 -Bake). The statics are what the app registers with Lightmap.add (a scene file does it for every node no Physics aspect moves), the bake fires at its Lightmap.load call. Writes <out>/<name>.bake + <name>-light[_n].ktx2 (irradiance, BC6H) + <name>-aux[_n].ktx2 (sky visibility, the directional share and direction of the light) + <name>-probes.ktx2 (the reflection probes: a cubemap of the level's radiance per probe, prefiltered per roughness; --probe-spacing 0 = none). Needs a GPU with hardware ray tracing (--tracer).

Lightmap UVs: the GLBs the app references that have none (TEXCOORD_1) are unwrapped first, IN PLACE — the same as lecodes assets unwrap, run on just those files; a file that has them is left alone, --no-unwrap skips the step. A whole level in one GLB that wants --groups / --zone is unwrapped by hand first (lecodes assets unwrap --help).

| Option | Meaning |
|---|---|
| `--out <dir>` | where the atlas lands (default `assets/lightmap`) |
| `--name <stem>` | file stem — one per baked level (default `lightmap`) |
| `--entry <file>` | the file that builds the scene and calls Lightmap.load (default `main.ts`) |
| `--public-url <url>` | resolve assets from this URL prefix (default: the API origin / the working tree) |
| `--remote-assets` | desktop hosts: load asset() resources from the server instead of the working tree |
| `--no-header` | omit the metadata header |
| `--no-minify` | keep the bundle readable (no compress / mangle) |
| `--no-sourcemap` | don't generate / inline the source map |
| `--size <n>` | atlas page edge (texels) (default `4096`) |
| `--texel <n>` | texel size (m); the bake coarsens only when the pages are full (default `0.05`) |
| `--pages <n>` | atlas pages to fill before the bake coarsens the texel (default `4`) |
| `--sun-rays <n>` | shadow rays into the sun's disc per texel (default `64`) |
| `--sky-rays <n>` | hemisphere rays per texel (the sky's irradiance and visibility). 2048 (2026-09-16): shadow rays are cheap (the school's direct pass 2 s a page) and a texel deep in a corridor sees the sky through a far window with 1 ray in 256 — every hit was 410 lux (default `2048`) |
| `--light-rays <n>` | shadow rays per lamp per texel (64: at 32 the school's lamp layer read 43 % noise between two seeds, the ceilings and doors next to a lamp 48 %) (default `64`) |
| `--bounce-rays <n>` | bounce paths per texel; 0 = direct light only. 128 (2026-09-16): a dark room's floor in the school reads 240 % noise at 64, 160 % at 128, 130 % at 256 — the last doubling costs 2x the bake time for little (default `128`) |
| `--bounces <n>` | path vertices at most (Russian roulette from the third); 0 = direct light only (default `3`) |
| `--patch-samples <n>` | next-event samples per texel over the BRIGHT PATCHES of a first direct pass (sunlit floors, the ceiling around a lamp) — what keeps the bounce in dark rooms from arriving as impulses (every path vertex takes one more); 0 = the cosine paths alone (default `64`) |
| `--split` | also write <name>-direct[_n].ktx2 / <name>-indirect[_n].ktx2 — the pages the "direct" / "indirect" views bind (list them as env.lightmap.direct / .indirect) |
| `--denoise <none|indirect|no-sun|all>` | the AI denoiser (--denoiser) over: indirect = the bounce alone (the range 2026-09-16: bounce noise 57 % -> 23 % of the mean, per-instance means unchanged), no-sun = + the sky and the lamps (the sky through a small opening: 0..3 hits of 256 rays, a texel is one of four values), all = + the sun (its shadow edges too: a sunlit pillar's mean moved 6 %), none. School, two seeds: no-sun took the sky 1.8 % -> 0.5 %, the lamps 43 % -> 21 % (default `no-sun`) |
| `--tracer <auto|optix|vulkan>` | which GPU API traces: auto = OptiX where an NVIDIA driver with OptiX opens, else Vulkan ray query — any card with hardware ray tracing (RTX, Radeon RX 6000+, Arc) with its driver alone; optix / vulkan by name (the bake fails when it cannot open) (default `auto`) |
| `--denoiser <auto|optix|oidn>` | which AI denoiser --denoise runs: auto = OptiX's on an NVIDIA card, else Intel Open Image Denoise (its CUDA / HIP / CPU device, the DLLs next to the host); optix / oidn by name. Independent of --tracer; one that cannot open is a warning and the pages stay as traced (default `auto`) |
| `--light-radius <n>` | lamps: the emitter's radius (m) — soft shadows, no 1/d² singularity (default `0.05`) |
| `--sun-angle <n>` | sun angular diameter (deg) (default `0.53`) |
| `--bias <n>` | ray start offset from the surface (m) (default `0.01`) |
| `--filter <n>` | the texel's reconstruction filter radius, in texels: every ray starts at a tent-distributed point of the texel's footprint, so a shadow's edge is a ramp of this width instead of a step at the texel grid (1 = ±1 texel, the classic; 0.5 = within the texel; 0 = the centre alone) (default `1`) |
| `--seed <n>` | the RNG seed: two bakes that differ only here differ by their noise (`lightmap diff`) (default `1`) |
| `--probe-spacing <n>` | reflection probes: the grid spacing in metres over the receivers (one probe per cell a static touches, minus the points inside geometry; every static reflects the nearest it can see); 0 = no probes (default `4`) |
| `--probe-size <n>` | a probe cubemap face's edge in texels (a power of two, 16..128) (default `32`) |
| `--probe-rays <n>` | paths per probe texel; the first hit of each takes four times the direct-light samples (the picture), so noise on a mirror is the one thing this buys (default `128`) |
| `--probe-indoor` | keep only the probes under a roof — those whose open share of the sphere (rays from the point that meet nothing) is at most 15 %, holes in the geometry or not: outdoor statics reflect the sky through their baked sky visibility instead — an environment map without the ground, but no outdoor probe can reach indoors and the atlas shrinks |
| `--probe-layout <rooms|grid>` | rooms = every indoor point of the grid slides to the middle of its room and the points of one room merge: one probe per room, far from the walls, its box the room (stations every ~8 m along a room over 12 m), outdoor points stay on the grid; grid = everything stays on the grid. Either way the pages carry which probes each block of texels sees (rays at bake time) (default `rooms`) |
| `--min-texels <n>` | instances smaller than this many ATLAS TEXELS along every axis (x --texel: 12 at 5 cm = 0.6 m) get no place in the atlas: they still cast their baked shadow and bounce, their own light is baked where they stand (six sides, no atlas texels) — cartridges, boxes, pipe elbows, a fire extinguisher: a baked shadow does not read on them and their tight charts are where the seams are. 0 = everything bakes; --dry-run lists what falls under it (default `12`) |
| `--thin-texels <n>` | ROUND AND NARROW objects leave the atlas like --min-texels: an instance none of whose lightmap charts is wider than this many texels across its short side (at --texel) — a railing's tubes, a cable, a pipe run, blinds, a gun barrel. A strip of 1-3 texels around a tube cannot hold the light's change around it and its uv seam shows as a step (the range's railing: two texels 5 mm apart on the seam, one lit, one shadowed); the ambient cube baked where it stands is smooth. A flat object has a wide chart somewhere and stays. 0 = off; --dry-run lists what falls under it (default `4`) |
| `--cube <regex>` | BAKE AS CUBES BY NAME: a regular expression (ECMAScript, case-sensitive) searched in every record's key — <node path>@<ordinal>, one record per mesh of a model, so SM_pipe takes every pipe node and @1$ the second mesh of each. A record that matches leaves the atlas like a --min-texels / --thin-texels one, whatever its size or charts: an occluder whose own light is baked where it stands as ambient cubes (in slabs along a long one, --object-slab) — a pipe run, a fence, a prop whose lightmap seams show. Repeat the flag (--cube SM_pipe --cube SM_fence) or join with \| in one (--cube "SM_pipe\|SM_fence", quoted for the shell); a pattern that starts with - goes as --cube=<regex>. The scene's env.lightmap.cube adds its own patterns, a node's lightmap: 'cube' marks one node by hand; --dry-run lists what matches |
| `--object-slab <m>` | A LONG object the atlas left out (--min-texels, --thin-texels, --cube) takes its light in SLABS of this many metres along the longest axis of its bounds — round(length / this) ambient cubes, 16 at most — a 6 m pipe run, a railing along a wall: one end under a lamp, the other in the dark, where one cube gave all of it the mean. The shader blends the two slabs around each pixel along that axis. 0 = one cube an object (default `1`) |
| `--emissive-nits <n>` | EMISSIVE SURFACES LIGHT THE LEVEL: cd/m² of emission 1.0 (emissive factor x strength x map) — every lit region of an emissive surface (a panel drawn in a ceiling's texture, a screen, a sign) bakes as a rectangle lamp. Default: what the level's camera makes of 1.0, 1.2 x 2^EV100, so a panel lights as brightly as it looks. 0 = emissive surfaces light nothing |
| `--detail-texels <n>` | A SMALL SURFACE GETS A FINER TEXEL: an instance whose surface is under this many atlas texels (area / texel²) is laid out denser by sqrt(this / its texels), 3x at most — a pipe of 1 m is ~300 texels at 5 cm, its charts a few texels wide and every chart edge a step; at 2000 it bakes at ~2 cm. 0 = off (default `2000`) |
| `--volume <n>` | THE LIGHT GRID for movers: the cell size in metres (0.75 of it tall) — per cell in free space the baked ambient light (sky, lamps, every bounce; not the direct sun, which stays real-time on movers), links between cells a ray connects so nothing blends through a wall; written as <name>.lgrid, list it as env.lightmap.volume. 0 = none (default `1`) |
| `--probe-box <x0,y0,z0,x1,y1,z1>` | only grid cells whose centre lies inside this world box get a probe: x0,y0,z0,x1,y1,z1 in metres — keeps probes off a roof top the player never sees (the range: 79 probes above a ceiling with holes where the fixtures were bled the sky onto the walls). Default: the receivers' bounds |
| `--debug-dir <dir>` | keep the float layers per page (PFM) here for `lightmap inspect` — gigabytes per level; a `.lightmap/` dir inside the project is skipped by the compile scan, anywhere else must be outside the project |
| `--dry-run` | only the atlas LAYOUT: pages, texel size, the largest rects — no tracing, nothing written (seconds, not minutes; the host still loads the level) |
| `--frames <n>` | host frames to run before giving up — load headroom (default `900`) |
| `--timeout <n>` | host time cap (ms; default: none) |
| `--width <n>` | the bake host's virtual viewport width — it has no window; nothing in the bake depends on it (default: app.json desktop.window, else 1280) |
| `--height <n>` | the bake host's virtual viewport height (default: app.json desktop.window, else 720) |
| `--renderer <gl|vulkan>` | ignored: the bake host is one binary (lecodes-bake, GL shaders on Filament's NOOP backend); kept so older scripts still parse |
| `--logs` | forward the host's output |
| `--no-unwrap` | leave the referenced GLBs as they are: no lightmap unwrap of those without TEXCOORD_1 (they bake without lightmap uvs) |

```
lecodes lightmap bake
lecodes lightmap bake --entry school.ts --name school --texel 0.04 --pages 4 --debug-dir .lightmap/school --logs
```

#### `lecodes lightmap stats`

The per-instance numbers a .bake carries: mean lux, sun / sky visibility, texels, rect

Reads the .bake alone (no debug layers needed). One row per receiver, sorted by mean lux unless --sort says otherwise; --key filters by a regular expression over the keys.

| Option | Meaning |
|---|---|
| `--bake <file>` | the .bake file (default `assets/lightmap/lightmap.bake`) |
| `--key <regex>` | only instances whose key matches this regular expression |
| `--sort <lux|key|texels|sun|sky>` | row order (default `lux`) |
| `--limit <n>` | rows to print (0 = all) (default `60`) |

```
lecodes lightmap stats --bake assets/school/lightmap/school.bake --key Wall
lecodes lightmap stats --sort sky --limit 0
```

#### `lecodes lightmap diff`

The noise of a bake, per instance: two bakes that differ only by --seed, compared texel by texel

Bake the level twice with different --seed (and --out / --debug-dir, so the second does not overwrite the first — a `.lightmap/` dir inside the project is skipped by the compile scan). Both must share the layout (same instances, rects and pages — a seed changes nothing else). Per instance: the mean of the layer, the noise as the RMS of (a − b) / √2 over the mean, in %, and the share of texels that differ by more than half their mean. Sorted by noise unless --sort says otherwise.

| Option | Meaning |
|---|---|
| `--bake <file>` | the first .bake (default `assets/lightmap/lightmap.bake`) |
| `--debug-dir <dir>` | where the first bake kept its PFM layers (default `.lightmap`) |
| `--bake2 <file>` | the second .bake (another --seed) |
| `--debug-dir2 <dir>` | where the second bake kept its PFM layers |
| `--layer <irradiance|bounce|sun|sky|lamps>` | which layer to compare (default `irradiance`) |
| `--key <regex>` | only instances whose key matches this regular expression |
| `--sort <noise|lux|key|texels>` | row order (default `noise`) |
| `--min-lux <n>` | instances whose mean is under this are 'dark': no noise figure, sorted last (a relative noise over nothing is not a number) (default `1`) |
| `--limit <n>` | rows to print (0 = all) (default `40`) |

```
lecodes lightmap bake --entry range.ts --name range --seed 2 --out .lightmap/range-seed2/bake --debug-dir .lightmap/range-seed2 --logs
lecodes lightmap diff --bake assets/range/lightmap/range.bake --debug-dir .lightmap/range --bake2 .lightmap/range-seed2/bake/range.bake --debug-dir2 .lightmap/range-seed2 --layer bounce
```

#### `lecodes lightmap inspect`

PNGs from a bake's debug layers: whole pages, or one instance's rect enlarged

Reads the PFM float layers `bake --debug-dir` kept next to the .bake's rects. Without --key: one PNG per page and layer with every rect outlined. With --key: the matching instances' rects (plus a margin) enlarged by --scale, one PNG per instance and layer. Lux layers are shown as log10 over --lux-range (or linear over --white); vectors as x0.5+0.5.

| Option | Meaning |
|---|---|
| `--bake <file>` | the .bake file (default `assets/lightmap/lightmap.bake`) |
| `--debug-dir <dir>` | where the bake kept its PFM layers (default `.lightmap`) |
| `--out <dir>` | where the PNGs go (default `.lightmap/png`) |
| `--key <regex>` | instances whose key matches this regular expression (default: whole pages) |
| `--layer <list>` | which layers: a comma list of irradiance, sun, sky, lamps, bounce, bounce-raw, visibility, direction, share, normal, position, ids, probes or all (default `irradiance,visibility,direction`) |
| `--scale <n>` | enlargement of a rect crop (integer) (default `8`) |
| `--margin <n>` | texels of gutter around a rect crop (default `4`) |
| `--lux-range <lo,hi>` | log10 lux mapped to black..white (default `0,5`) |
| `--white <n>` | linear mode: the lux shown as white (0 = log mode) (default `0`) |
| `--limit <n>` | instances to render with --key (0 = all) (default `24`) |

```
lecodes lightmap inspect --bake assets/school/lightmap/school.bake --debug-dir .lightmap/school
lecodes lightmap inspect --key 'Wall_05' --layer irradiance,sun,sky,lamps --scale 6
```

### `lecodes navmesh`

Build the level's navigation mesh (bake)

#### `lecodes navmesh bake`

Build the level's navigation mesh for NavAgent / NavMesh

The desktop host dumps every STATIC body's collision triangles at the scene's env.navmesh (or NavMesh.load) point, then Recast builds the tiled mesh here → <out>/<name>.navmesh. The agent size defaults to what the scene declares. Needs a desktop host with the geometry dump (LECODES_DESKTOP_EXE for a local build).

| Option | Meaning |
|---|---|
| `--out <dir>` | where it lands (default `assets/nav`) |
| `--name <n>` | file name (default: the scene file that declares navmesh:, else scene) |
| `--entry <file>` | the file that builds the level: a scene file with env.navmesh, or code calling NavMesh.load (default `main.ts`) |
| `--public-url <url>` | resolve assets from this URL prefix (default: the API origin / the working tree) |
| `--remote-assets` | desktop hosts: load asset() resources from the server instead of the working tree |
| `--no-header` | omit the metadata header |
| `--no-minify` | keep the bundle readable (no compress / mangle) |
| `--no-sourcemap` | don't generate / inline the source map |
| `--radius <n>` | agent radius (m; default: the scene's) |
| `--height <n>` | agent height (m; default: the scene's) |
| `--climb <n>` | agent max step height (m; default: the scene's) |
| `--slope <n>` | agent max walkable slope (deg; default: the scene's) |
| `--cell <n>` | voxel size (m) (default `0.25`) |
| `--cell-height <n>` | voxel height (m) (default `0.1`) |
| `--tile <n>` | tile size (cells; 0 = one tile) (default `48`) |
| `--region-min <n>` | min region area (cells²) (default `8`) |
| `--region-merge <n>` | regions smaller than this merge (cells²) (default `20`) |
| `--edge-max <n>` | max polygon edge length (voxels) (default `12`) |
| `--edge-error <n>` | max edge simplification error (voxels) (default `1.3`) |
| `--detail <n>` | detail mesh sample distance (voxels) (default `6`) |
| `--detail-error <n>` | detail mesh max error (voxels) (default `1`) |
| `--obj` | also write <name>.navmesh.obj to inspect the polygons |
| `--keep-geometry` | keep the host's <name>.navgeo dump |
| `--frames <n>` | host frames to run before giving up — load headroom (default `900`) |
| `--timeout <n>` | host time cap (ms) (default `600000`) |
| `--width <n>` | host window width (default: app.json desktop.window, else 1280) |
| `--renderer <gl|vulkan>` | desktop host + shader variant (default: app.json desktop.renderer, else gl) |
| `--logs` | forward the host's output |

```
lecodes navmesh bake
lecodes navmesh bake --name yard --cell 0.2 --obj --logs
```

## Native apps

### `lecodes app`

Native apps: iOS / Android shells and the portable desktop build (init, sync, update, build, upload)

A shell is a generated Xcode / Gradle project in <project>/ios/ or android/ that runs the compiled bundle offline; `sync` keeps it matching the project, `build` and `upload` ship it. The desktop target needs no shell — `build desktop` stages the host + bundle into a portable folder (a signed .app on macOS). app.json carries the identity, versions, plugins, icon and per-platform settings.

#### `lecodes app init <ios|android|desktop>`

Generate a native shell in <project>/ios/ or <project>/android/, or declare the desktop target

The shell is offline-first (it runs the bundled app.js) and never pushed to LeCodes (the dir goes to .lecodesignore). The native SDK starts on the newest published release, pinned in app.json ios.sdk / android.sdk so builds stay reproducible — `lecodes app update` moves the pin; --sdk latest floats it instead (every sync re-resolves).

desktop: no shell needed — writes app.json's desktop block (window size; --fullscreen for kiosks). Re-running keeps existing app.json values.

| Option | Meaning |
|---|---|
| `--name <s>` | display name (default: app.json / the folder name) |
| `--bundle-id <id>` | iOS bundle identifier (default com.example.<slug>) |
| `--app-id <id>` | Android applicationId (default com.example.<slug>) |
| `--team <id>` | Apple development team id |
| `--variant <v>` | SDK variant: auto (default — sync picks the smallest of core/2d/3d/full covering what the bundle uses), or one of those to pin; Android also vulkan (full on Vulkan, arm64 only) |
| `--sdk <v|latest>` | native SDK release (default: the newest one, pinned in app.json); 'latest' floats it — every sync re-resolves |
| `--fullscreen` | (desktop) borderless fullscreen on the primary monitor — kiosks |

```
lecodes app init ios --team ABCDE12345
lecodes app init android --app-id com.acme.game
lecodes app init desktop --fullscreen
```

#### `lecodes app sync [ios|android]`

Make every existing shell match the project: bundle + assets + SDK variant + plugins + CLI-owned files

Compiles the bundle and embeds the assets it references (ios/App/Resources/ resp. the android assets/; offline by default, also for server projects — app.json "resources": {"remote": [globs]} keeps heavy assets streaming), picks the SDK variant from the bundle's engine use, vendors the app.json plugins (pinned in plugins.lock.json) and regenerates the CLI-owned shell files. User files (ios/App/*.swift, android/app/) are never touched.

Native OTA JS updates at launch (both shells): the first launch of an installed build waits (≤ 5 s, the splash held) for the check and runs the newest bundle (a source not newer than the build answers 304 to the build's compile date: nothing downloaded, the embedded bundle runs); every later launch runs the newest LOCAL bundle and a download applies on the next. On by default for a pushed project (its bundle on the platform, the launcher's own URL), app.json "update": {"url": …} for custom hosting, "update": false turns it off. A download runs only when it is of the shell's SDK major and not older than the bundle running; a store update keeps a downloaded bundle only when it is newer code than the one it ships. app.json "icon" (a square ≥1024px image) renders the app icons every sync ("iconBackground": the adaptive / flatten backdrop). app.json "splash" ({"background": "#RRGGBB", "image": "./splash.png", "size": 160}, all optional) renders the launch screen of both shells every sync — the colour with the image fitted centred into a size × size pt / dp square, 160 by default (iOS LaunchScreen.storyboard, Android the system splash of Theme.LeCodesRuntime, which the app's theme inherits). app.json "urlScheme" ("myshop") is the scheme the shells open on: iOS CFBundleURLTypes, Android a BROWSABLE intent-filter — a link arrives as app.launchUrl / the "url" event (the return of Browser.open with a returnUrl). app.json "appLinks" (["https://shop.example/pay/"]) are the verified https links they open on — iOS universal links (associated-domains in App.entitlements), Android App Links (autoVerify intent-filters in the manifest); sync prints the apple-app-site-association / assetlinks.json each site must serve. No platform = every shell that exists; a dual-shell project compiles once.

```
lecodes app sync
lecodes app sync android
```

#### `lecodes app update [ios|android]`

Move the shell's pins forward (SDK release + plugin tags), then re-sync

app.json ios.sdk / android.sdk → the newest published SDK release, plugins.lock.json → each plugin's newest tag (a github:owner/repo@tag spec is a deliberate pin and is left alone), then `lecodes app sync` so the shell carries the new versions. `lecodes update` runs this as its shells step.

| Option | Meaning |
|---|---|
| `--sdk <v|latest>` | pin that release instead of the newest one; 'latest' floats the pin |
| `--check` | report available updates, write nothing |
| `--no-plugins` | only move the SDK pin |
| `--no-sync` | update the pins without re-syncing |

```
lecodes app update
lecodes app update --check
lecodes app update ios --sdk 1.6.0
```

#### `lecodes app plugin`

Manage native plugins in app.json: list | add | remove (then run `lecodes app sync`)

Bare `lecodes app plugin` lists them with their plugins.lock.json pins. `add` takes a first-party id (camera, qr-scanner, map, geolocation, push, browser), github:owner/repo[@tag] for a third-party plugin (native code — review it before shipping), or the folder of a plugin of this project (./plugins/<id> — written with `lecodes plugin gen`, nothing to publish). Nothing reaches the shell until the next sync.

##### `lecodes app plugin list`

The plugins in app.json and their lock pins

##### `lecodes app plugin add <id | github:owner/repo[@tag]>`

Add a plugin: a known id, or github:owner/repo[@tag]

```
lecodes app plugin add camera
lecodes app plugin add github:acme/lecodes-plugin-thing@v1.2.0
lecodes app plugin add ./plugins/acme-dial
```

##### `lecodes app plugin remove <id>`

Remove a plugin entry

#### `lecodes app open [ios|android]`

Open the native shell in its IDE (Xcode / Android Studio)

No platform: opens whichever single shell exists. Xcode needs macOS; Android Studio is looked up in its default install locations (or `studio` on the PATH).

```
lecodes app open
lecodes app open android
```

#### `lecodes app doctor`

Preflight checks for building / uploading the shells

Toolchain (Xcode / JDK + Android SDK), the synced bundle, SDK variant + version vs app.json, plugin lock, signing (team id / release keystore), icons, the splash theme (Android), OTA wiring and the App Store Connect key. Fatal checks fail the command (exit 1); soft ones print a `!` and a hint.

```
lecodes app doctor
```

#### `lecodes app build`

Build a shippable artifact: ios (.ipa) | android (.apk/.aab) | desktop (portable folder / .app)

##### `lecodes app build ios`

Sync, archive and export the .ipa (automatic signing)

xcodebuild archive of ios/App.xcodeproj (generic iOS device, -allowProvisioningUpdates) into ios/build/App.xcarchive, then an export with the method given — the .ipa lands in ios/build/export/. `lecodes app upload ios` sends the archive to App Store Connect.

| Option | Meaning |
|---|---|
| `--configuration <c>` | build configuration (default `Release`) |
| `--method <m>` | export method (default `app-store-connect`) |
| `--no-export` | stop after the archive |
| `--no-sync` | skip the `lecodes app sync` step |

```
lecodes app build ios
lecodes app build ios --method ad-hoc
```

##### `lecodes app build android`

Sync, gradlew assembleRelease + bundleRelease, copy the results to dist/<name>-<version>-<versionCode>.apk|.aab

Signing comes from the project .env keys `lecodes app keystore` writes (LECODES_ANDROID_KEYSTORE_FILE / _KEYSTORE_PASSWORD / _KEY_ALIAS / _KEY_PASSWORD) or android/keystore.properties; a release build without a key is refused — --allow-unsigned builds one anyway (throwaway builds only). Release builds run R8 (app.json android.minify: false turns it off). Gradle needs the Android SDK location: android/local.properties, or written from ANDROID_HOME on the first build.

Both artifacts are built by default; --apk / --aab pick one. The shell's own build/ tree stays where Android Studio expects it — dist/ gets a copy named by the app.

| Option | Meaning |
|---|---|
| `--configuration <c>` | Release (default) or Debug (default `Release`) |
| `--apk` | only the .apk |
| `--aab` | only the .aab |
| `--no-aab` | the old spelling of --apk |
| `--allow-unsigned` | build a release without a signing key anyway |
| `-o, --out <dir>` | output dir (default dist/) |
| `--slug <name>` | file name stem (default: the app name) |
| `--no-sync` | skip the `lecodes app sync` step |

```
lecodes app build android
lecodes app build android --configuration Debug --apk
lecodes app build android -o out/
```

##### `lecodes app build desktop`

Stage a portable desktop app folder (dist/<name>/): host exe + app.js + assets + runtime

The cached host renamed to <Name>.exe + app.js + assets/ (the project's files) + runtime/ (the engine's) + app.json (the host reads title/size/fullscreen from it) + app.ico from app.json "icon". Duplicate asset basenames are a build error (they silently alias in the bundle). Also spelled `lecodes app build desktop`.

On macOS: a signed .app instead (the downloaded LeCodes Desktop.app + the project inside it); app.json desktop.macos = bundleId / buildNumber / category / signing.identity / notarize.profile (a notarytool keychain profile — notarized + stapled, so it opens on any Mac without "Open Anyway"). --sign / --notarize / --no-notarize override it per build; LECODES_MACOS_SIGN_ID / LECODES_NOTARY_PROFILE sit between the flags and app.json.

| Option | Meaning |
|---|---|
| `-o, --out <dir>` | output folder (default dist/<name>) |
| `--slug <name>` | folder / zip name override (Latin-only) |
| `--renderer <gl|vulkan>` | host + shader variant: gl \| vulkan (default: app.json desktop.renderer, else gl) |
| `--probe` | headless-render the staged folder to a PNG next to it (macOS: a brief windowed run + snapshot) |
| `--frames <n>` | (--probe) frames to render before the snapshot (default `90`) |
| `--after <n>` | (--probe, macOS) seconds the app runs before the snapshot (default `8`) |
| `--zip` | also pack the result for sending: <out>.zip with UTF-8 / forward-slash entries (macOS: a ditto archive of the .app) |
| `--dmg` | macOS: also build a drag-to-Applications disk image (signed + notarized too) |
| `--sign <identity|adhoc>` | macOS codesign identity — a "Developer ID Application: …" cert, or adhoc (local only) |
| `--notarize <profile>` | macOS: notarize + staple with this `xcrun notarytool store-credentials` keychain profile |
| `--no-notarize` | macOS: skip notarization even when app.json / the environment names a profile |
| `--entry <path>` | compile from this entry instead of the detected one |
| `--public-url <url>` | resolve assets from this URL prefix (default: the API origin / the working tree) |
| `--remote-assets` | desktop hosts: load asset() resources from the server instead of the working tree |
| `--no-header` | omit the metadata header |
| `--no-minify` | keep the bundle readable (no compress / mangle) |
| `--no-sourcemap` | don't generate / inline the source map |

```
lecodes desktop build
lecodes app build desktop --zip --probe
lecodes desktop build -o out/kiosk --renderer vulkan
lecodes app build desktop --dmg --notarize MyProfile
```

#### `lecodes app keystore`

Android release signing key: create | use <file> | info

The key's path + passwords live in the project .env as LECODES_ANDROID_* (or in android/keystore.properties for Studio-only builds), never in app.json — it only carries the metadata (android.signing). `lecodes app build android` refuses a release without one.

##### `lecodes app keystore create`

Generate an Android release signing key (keytool), banked in the project .env

The keystore path + passwords go to the project .env (LECODES_ANDROID_KEYSTORE_FILE / _KEYSTORE_PASSWORD / _KEY_ALIAS / _KEY_PASSWORD — loaded by every lecodes run, never pushed), the metadata to app.json android.signing. Refuses to overwrite an existing key or an already configured signing setup unless --force.

With Play App Signing (default for new Play apps) this is the UPLOAD key — Google keeps the app signing key, and a lost upload key can be reset through Play support. Without it, losing this file means the app can never be updated. Back it up either way.

| Option | Meaning |
|---|---|
| `--alias <a>` | key alias (default `upload`) |
| `-o, --out <file>` | keystore path (default android/release.keystore) |
| `--password <pw>` | keystore password (else asked on the terminal) |
| `--generate` | a random password instead of asking |
| `--validity <days>` | certificate validity in days (default `10000`) |
| `--dname <X.500>` | X.500 distinguished name (default CN=<app name>) |
| `--force` | overwrite an existing keystore / signing config |

```
lecodes app keystore create
lecodes app keystore create --generate --alias upload
```

##### `lecodes app keystore use <file>`

Attach an existing keystore (any path; --copy puts it in android/)

Reads the keystore's private-key entries with keytool, picks the alias (asked for with --alias when there are several), then banks the path + passwords in the project .env and the metadata in app.json android.signing — exactly what `keystore create` writes. `~/…` paths resolve against the home dir.

With Play App Signing (default for new Play apps) this is the UPLOAD key — Google keeps the app signing key, and a lost upload key can be reset through Play support. Without it, losing this file means the app can never be updated. Back it up either way.

| Option | Meaning |
|---|---|
| `--alias <a>` | key alias (needed when the keystore holds several keys) |
| `--password <pw>` | keystore password (else asked on the terminal) |
| `--key-password <pw>` | the key's own password when it differs from the store's (JKS) |
| `--copy` | copy the file into android/ and point the config at the copy |
| `--force` | (--copy) replace a same-named file in android/ |

```
lecodes app keystore use ~/keys/upload.keystore
lecodes app keystore use release.jks --alias upload --copy
```

##### `lecodes app keystore info`

Which key the build signs with + its SHA-1 / SHA-256 fingerprints

Resolves signing exactly like the build does (android/keystore.properties first, then the LECODES_ANDROID_* environment the project .env fills) and prints the keystore, the alias and the certificate lines Play / Firebase ask for.

| Option | Meaning |
|---|---|
| `--password <pw>` | keystore password (else asked on the terminal) |

```
lecodes app keystore info
```

#### `lecodes app upload`

Upload a build to the store: ios (App Store Connect) | android (Google Play)

##### `lecodes app upload ios`

Upload the archive to App Store Connect (then bumps ios.buildNumber)

Exports ios/build/App.xcarchive with destination `upload` through xcodebuild, authenticated with an App Store Connect API key: the three flags on first use, then ~/.lecodes/asc.json remembers them. A successful upload consumes the build number, so app.json ios.buildNumber is bumped for the next build.

| Option | Meaning |
|---|---|
| `--key-path <p8>` | App Store Connect API key (AuthKey_XXXX.p8) |
| `--key-id <id>` | the key's id |
| `--issuer-id <id>` | the key's issuer id |

```
lecodes app upload ios --key-path ~/AuthKey_ABC123.p8 --key-id ABC123 --issuer-id 1234-…
lecodes app upload ios
```

##### `lecodes app upload android`

Upload the .aab to a Google Play track (then bumps android.versionCode)

One edit of the Google Play Developer API — the bundle onto the track as one release, then the commit; Play changes only when every step went through. Authenticated with a Cloud service account's JSON key: --key on first use, then the project .env remembers it (LECODES_PLAY_SERVICE_ACCOUNT; in CI set the variable to the key's path, e.g. a GitLab File variable). The app must already exist in Play Console under app.json android.applicationId (the API cannot create it), the account be invited there with "Release to testing tracks". A successful upload spends the versionCode, so app.json android.versionCode moves past it for the next build.

| Option | Meaning |
|---|---|
| `--aab <file>` | the bundle (default: the dist/<name>-<version>-<versionCode>.aab `app build android` wrote) |
| `--track <t>` | internal \| alpha \| beta \| production \| a closed track's name (default `internal`) |
| `--draft` | a draft release, rolled out in Play Console (the only kind a never-published app takes) |
| `--key <json>` | the service account's JSON key (remembered in the project .env) |
| `--slug <name>` | file name stem of the default bundle, as given to `app build android --slug` |

```
lecodes app upload android --key ~/keys/play.json
lecodes app upload android --track internal
lecodes app upload android --draft
```

### `lecodes plugin`

Write a native plugin: gen

For the author of a plugin — a capability an app reaches the same way on iOS, Android and the web. `lecodes app` is the other side: it installs plugins into an app's shell.

#### `lecodes plugin gen [path]`

Generate a plugin's files from its contract.d.ts (--check: fail on a stale one)

The path is one plugin (a directory with a contract.d.ts) or a repo that keeps its plugins in plugins/<id>/; the current directory when omitted.

Writes sdk/<id>.gen.ts — the wrapper an app calls: the wire call by call, the class with the wire's methods as its own, and the public export when the plugin has no hand-written sdk/<id>.ts. Where the plugin has an ios/ folder, writes ios/<Name>.gen.swift — the contract's types, the protocol the plugin implements and the glue that reads a call off the wire. Where it has an android/ folder, writes the same as <Name>.gen.kt into the folder of the plugin's Kotlin package (android/ is laid out as a Gradle module: the package is the folder the path under android/src/main/java narrows down to). Where it has a web/ folder, writes web/<id>.gen.ts — the same for the web half, in a file that imports nothing; a web half that imports a LeCodes package is refused, because it would run on that host only. A repo of plugins also gets its Package.swift from the manifests: the iOS halves as one SwiftPM package, a target and a product per plugin, the SDK as a package on disk (LECODES_IOS_SDK_PATH, else the repo's package.json lecodes.iosSdkPath, else ../lecodes) — what an Xcode project links the halves as (a shell vendors the sources instead). A contract the generator does not understand is an error with its line, and every problem of a file is reported at once.

A HALF FOLDER of a project — a manifest with `implements: "<first-party id>"`, no contract.d.ts and no sdk/ of its own — is generated from the contract of the plugin it implements (the one the SDK carries): `lecodes plugin gen plugins/ar-8thwall` writes web/ar.gen.ts, the half registers the service "ar", and app.json names the folder for that platform (`"ar": { "web": "./plugins/ar-8thwall" }`).

Generated files are committed. Reading a contract needs TypeScript in the plugin's repo.

| Option | Meaning |
|---|---|
| `--check` | write nothing; exit 1 when a generated file is missing or stale |

```
lecodes plugin gen
lecodes plugin gen plugins/camera
lecodes plugin gen plugins/ar-8thwall
lecodes plugin gen --check
```

### `lecodes pn`

Push notifications: send | devices | keys

Push notifications for the project in .lecodes/manifest.json. `send` uses your login (no push key needed to test); `devices` is the registration ledger; `keys` mints the `lecodes_pk_…` keys a production server deploys.

#### `lecodes pn send [title]`

Send a notification (uses your login — no key needed)

Targets are what a production server sends to: --user ids from Push.register({ user }) and/or --to device addresses (`lecodes pn devices` lists both). The title may also be given as the first positional.

| Option | Meaning |
|---|---|
| `-t, --title <s>` | the notification title |
| `-b, --body <s>` | second line |
| `--url <s>` | deep link (also arrives as app.launchUrl) |
| `-u, --user <ids>` | target Push.register({ user }) ids, comma-separated |
| `--to <pa_…>` | target addresses, comma-separated |
| `--badge <n>` | app icon badge |
| `--data <json>` | your JSON payload (≤ 2 KB) |

```
lecodes pn send --title "Order shipped" --user u42
lecodes pn send --title "Hi" --to pa_abc --body "second line"
```

#### `lecodes pn devices`

List the devices that registered for this project (the 'why didn't it arrive?' command)

A device appears here after the app calls Push.register(). `lecodes dev` bundles are unattributed — register() rejects there; open a PUBLISHED project via QR.

| Option | Meaning |
|---|---|
| `-u, --user <id>` | only that user's devices |
| `--forget <pa_…>` | drop a dead registration |
| `-y, --yes` | skip the confirmation |

```
lecodes pn devices
lecodes pn devices --user u42
lecodes pn devices --forget pa_abc -y
```

#### `lecodes pn keys`

Push keys for YOUR server: list (bare), mint, revoke

Bare `lecodes pn keys` lists this project's keys. You only need one to send from your OWN server — `lecodes pn send` uses your login.

```
lecodes pn keys
lecodes pn keys new "prod server" --env
lecodes pn keys revoke 3
```

##### `lecodes pn keys new [name]`

Mint a push key for YOUR server (owner only; shown once)

A push key is generated BY the server — you can't choose or recover one. It only sends for this project, which is why deploying it beats deploying a personal access token.

| Option | Meaning |
|---|---|
| `--name <s>` | the key's name (or the first positional) |
| `--env` | write it to .env as LECODES_PUSH_KEY (+ gitignore) |

```
lecodes pn keys new "prod server" --env
```

##### `lecodes pn keys revoke <id>`

Revoke a key — any server still using it stops sending immediately

| Option | Meaning |
|---|---|
| `-y, --yes` | skip the confirmation |

```
lecodes pn keys revoke 3 -y
```

## Maintenance

### `lecodes update`

Move everything the project depends on to its newest published state

In dependency order: the desktop host (`desktop update`), matc (`shaders update`), the project's shader compiles, the IDE types (.lecodes/types/ + tsconfig.json, from the server — the former `lecodes types`), the ios/android shell pins (`app update`), then the GLOBAL installs: every installed optional peer (lecodes-headless, lecodes-design, lecodes-3d-editor, lecodes-assets) and this CLI that is behind npm's latest is installed again, with the package manager the CLI came by (`npm install -g`, or `bun add -g` for a CLI installed with bun).

Pinned tools are skipped, not overridden: LECODES_DESKTOP_EXE / LECODES_MATC mean a local build. Keeps going when a step fails; the summary says which.

| Option | Meaning |
|---|---|
| `--local` | types from the SDK bundled in this CLI |
| `--no-shells` | leave ios/android pins alone (no re-sync) |
| `--check` | npm step: report what's behind, install nothing |
| `--no-cli` | skip the npm step (offline) |

```
lecodes update
lecodes update --check
lecodes update --local --no-shells
```

### `lecodes install`

Install this standalone build so `lecodes` is on your PATH

Copies the bundle (lecodes + chisel + runtime/) to a library folder and exposes the binary: a symlink in ~/.local/bin on Linux/macOS, the install folder itself on PATH on Windows. PATH is only edited with --modify-path; otherwise the command prints how to finish. Only works from the packaged release archive.

| Option | Meaning |
|---|---|
| `--modify-path` | also add it to PATH (edits your shell rc / user PATH) |
| `--system` | install for all users (Linux/macOS; needs sudo) |
| `--dir <path>` | override the install/library folder |
| `--bin <path>` | override the symlink folder (Linux/macOS) |

```
lecodes install --modify-path
sudo lecodes install --system
```

