lecodes-cli 0.20.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -57
- package/dist/index.js +11052 -7592
- package/package.json +14 -8
- package/runtime/materials/decal-relief.mat +12 -1
- package/runtime/materials/decal.mat +17 -3
- package/runtime/materials/lightmap.mat +117 -138
- package/runtime/materials/particles-quad.mat +6 -2
- package/runtime/materials/terrain-lightmap.mat +123 -0
- package/runtime/materials/terrain.mat +36 -55
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/VERSION.json +5 -0
- package/runtime/web/assets/camera-5XOM8eHl.js +1 -0
- package/runtime/web/assets/creator-2d-D9WKYM6h.js +1 -0
- package/runtime/web/assets/creator-2d-ukvEmNEp.wasm +0 -0
- package/runtime/web/assets/creator-full-BBXzQlhq.js +1 -0
- package/runtime/web/assets/creator-full-D2dSDdDj.wasm +0 -0
- package/runtime/web/assets/creator-full.js-BYswGsqR.symbols +8124 -0
- package/runtime/web/assets/creator-ui-DIc92ion.wasm +0 -0
- package/runtime/web/assets/creator-ui-_1Z_AX2M.js +1 -0
- package/runtime/web/assets/geolocation-BjTKN9_P.js +1 -0
- package/runtime/web/assets/inspect-C7x78PNs.js +2 -0
- package/runtime/web/assets/map-CyBxOSV0.js +1 -0
- package/runtime/web/assets/map-XzoOFt0E.js +2 -0
- package/runtime/web/assets/mapImpl-bxvQ2h1Z.js +823 -0
- package/runtime/web/assets/maplibre-gl-worker-B7LXXUkR.js +8 -0
- package/runtime/web/assets/materials-uri4FvPE.bin +0 -0
- package/runtime/web/assets/materials-world-CQWt8TAm.bin +0 -0
- package/runtime/web/assets/qr-scanner-CK_cYoJR.js +1 -0
- package/runtime/web/assets/uberarchive-DLH-kBhM.bin +0 -0
- package/runtime/web/index.html +36 -0
- package/runtime/web/player.js +21 -0
- package/runtime/web-page/index.html +41 -0
- package/runtime/web-page/page.css +1 -0
- package/runtime/web-page/page.js +31 -0
- package/src/api.ts +1 -302
- package/src/cli/args.ts +37 -0
- package/src/cli/command.ts +113 -0
- package/src/cli/errors.ts +13 -0
- package/src/cli/help.ts +158 -0
- package/src/cli/index.ts +7 -0
- package/src/cli/output.ts +102 -0
- package/src/cli/run.ts +106 -0
- package/src/commands/{appAndroid.ts → app/android.ts} +454 -43
- package/src/commands/{appDesktop.ts → app/desktop.ts} +64 -33
- package/src/commands/{appDesktopMac.ts → app/desktopMac.ts} +34 -27
- package/src/commands/{appIcon.ts → app/icon.ts} +11 -8
- package/src/commands/{app.ts → app/index.ts} +400 -290
- package/src/commands/{appShared.ts → app/shared.ts} +43 -44
- package/src/commands/{appTemplatesAndroid.ts → app/templates/android.ts} +72 -39
- package/src/commands/{appTemplatesGradlew.ts → app/templates/gradlew.ts} +1 -1
- package/src/commands/{appTemplates.ts → app/templates/ios.ts} +2 -2
- package/src/commands/assets.ts +23 -21
- package/src/commands/clone.ts +48 -51
- package/src/commands/compile.ts +76 -89
- package/src/commands/create.ts +46 -47
- package/src/commands/design/comments.ts +361 -0
- package/src/commands/design/context.ts +101 -0
- package/src/commands/design/index.ts +391 -0
- package/src/commands/design/snapshot.ts +134 -0
- package/src/commands/{designTemplates.ts → design/templates.ts} +4 -3
- package/src/commands/desktop.ts +94 -82
- package/src/commands/dev.ts +260 -182
- package/src/commands/diff.ts +39 -51
- package/src/commands/index.ts +64 -0
- package/src/commands/{init.ts → init/index.ts} +195 -191
- package/src/commands/{projectTemplates.ts → init/templates.ts} +64 -21
- package/src/commands/install.ts +80 -67
- package/src/commands/lightmap.ts +543 -252
- package/src/commands/link.ts +87 -94
- package/src/commands/login.ts +25 -23
- package/src/commands/navmesh.ts +169 -139
- package/src/commands/plugin.ts +46 -0
- package/src/commands/pn.ts +201 -244
- package/src/commands/pull.ts +49 -56
- package/src/commands/push.ts +48 -54
- package/src/commands/render.ts +176 -111
- package/src/commands/scene.ts +63 -55
- package/src/commands/serve.ts +58 -0
- package/src/commands/{shaders.ts → shaders/index.ts} +61 -42
- package/src/commands/{shadersNew.ts → shaders/new.ts} +3 -3
- package/src/commands/shared.ts +79 -0
- package/src/commands/status.ts +22 -26
- package/src/commands/test.ts +94 -63
- package/src/commands/thumbs.ts +185 -175
- package/src/commands/update/index.ts +226 -0
- package/src/commands/{types.ts → update/types.ts} +31 -22
- package/src/compile/collect.ts +5 -5
- package/src/compile/collectLocal.ts +3 -3
- package/src/compile/designCompile.ts +4 -4
- package/src/compile/headlessBundle.ts +16 -12
- package/src/compile/nativeStack.ts +1 -1
- package/src/compile/projectCompile.ts +12 -4
- package/src/compile/sceneCompile.ts +14 -26
- package/src/compile/screenEntry.ts +123 -127
- package/src/compile/shaders.ts +3 -3
- package/src/{lecodes-3d-editor.d.ts → declarations/lecodes-3d-editor.d.ts} +4 -0
- package/src/{lecodes-assets.d.ts → declarations/lecodes-assets.d.ts} +1 -1
- package/src/{lecodes-renderer.d.ts → declarations/lecodes-headless.d.ts} +3 -3
- package/src/dev/androidDev.ts +1 -1
- package/src/dev/clientTemplates.ts +18 -14
- package/src/dev/devServer.ts +78 -41
- package/src/dev/iosDev.ts +180 -0
- package/src/dev/webRunner.ts +83 -15
- package/src/{cmgenTool.ts → hosts/cmgenTool.ts} +2 -2
- package/src/{desktopRenderer.ts → hosts/desktopRenderer.ts} +104 -32
- package/src/{desktopScript.ts → hosts/desktopScript.ts} +151 -36
- package/src/{distRoot.ts → hosts/distRoot.ts} +9 -5
- package/src/{matcTool.ts → hosts/matcTool.ts} +2 -2
- package/src/{peerInstall.ts → hosts/peerInstall.ts} +7 -7
- package/src/{peers.ts → hosts/peers.ts} +2 -2
- package/src/{releases.ts → hosts/releases.ts} +1 -1
- package/src/index.ts +35 -480
- package/src/platform/api.ts +302 -0
- package/src/{browserAuth.ts → platform/browserAuth.ts} +1 -1
- package/src/{config.ts → platform/config.ts} +1 -1
- package/src/{serverDiff.ts → platform/serverDiff.ts} +3 -3
- package/src/plugin/contract.ts +438 -0
- package/src/plugin/emitKotlin.ts +357 -0
- package/src/plugin/emitSdk.ts +259 -0
- package/src/plugin/emitSwift.ts +336 -0
- package/src/plugin/emitWeb.ts +383 -0
- package/src/plugin/generate.ts +157 -0
- package/src/plugin/lines.ts +28 -0
- package/src/plugin/lower.ts +231 -0
- package/src/plugin/manifest.ts +192 -0
- package/src/{projectEnv.ts → project/env.ts} +33 -2
- package/src/{ignore.ts → project/ignore.ts} +164 -163
- package/src/{localFiles.ts → project/localFiles.ts} +1 -1
- package/src/{manifest.ts → project/manifest.ts} +1 -1
- package/src/{project.ts → project/materialize.ts} +2 -2
- package/src/project/paths.ts +20 -0
- package/src/{textDiff.ts → project/textDiff.ts} +1 -1
- package/src/{types.ts → project/types.ts} +0 -0
- package/runtime/materials/lightmap-baked-lite.mat +0 -175
- package/runtime/materials/lightmap-baked.mat +0 -176
- package/runtime/scene-harness.json +0 -1
- package/runtime/web/assets/__vite-browser-external-BIHI7g3E.js +0 -1
- package/runtime/web/assets/basis-C64VHDVD.js +0 -1
- package/runtime/web/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
- package/runtime/web/assets/createViewerLite-Ct_PZdof.js +0 -852
- package/runtime/web/assets/draco-BiISTFcR.js +0 -118
- package/runtime/web/assets/draco_decoder-DsQ12WqX.wasm +0 -0
- package/runtime/web/assets/index-BMt7AnC5.js +0 -2
- package/runtime/web/assets/mapViewImpl-B2JcES8l.js +0 -810
- package/runtime/web/assets/maplibre-gl-worker-CJfwIrte.js +0 -8
- package/runtime/web/assets/worker-Caf-yYEI.js +0 -2
- package/runtime/web/embed.html +0 -32
- package/runtime/web/embed.js +0 -171
- package/src/commands/design.ts +0 -846
- package/src/commands/update.ts +0 -211
- package/src/util.ts +0 -146
- /package/src/{lecodes-design.d.ts → declarations/lecodes-design.d.ts} +0 -0
- /package/src/{qrcode-terminal.d.ts → declarations/qrcode-terminal.d.ts} +0 -0
- /package/src/{designMeta.ts → project/designMeta.ts} +0 -0
package/src/dev/webRunner.ts
CHANGED
|
@@ -1,22 +1,90 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync } from "node:child_process"
|
|
2
|
+
import { existsSync, readdirSync, statSync } from "node:fs"
|
|
2
3
|
import { createRequire } from "node:module"
|
|
3
4
|
import { dirname, join } from "node:path"
|
|
4
|
-
import { distRoot } from "../distRoot"
|
|
5
|
+
import { distRoot } from "../hosts/distRoot"
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
-
* (
|
|
9
|
-
*
|
|
10
|
-
* the
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* What `lecodes dev --web` serves to a browser, two built folders:
|
|
9
|
+
* player the web player (lecodes-web-player's dist: index.html + player.js + assets/ — the
|
|
10
|
+
* runtime module, the archives, the plugins' web halves): the app, filling its window
|
|
11
|
+
* page the page around it (lecodes-web-frame's dist-page: the app in a device, the buttons,
|
|
12
|
+
* the console), which holds the player in an <iframe>; absent = the player alone
|
|
13
|
+
*
|
|
14
|
+
* Each comes in one of two shapes:
|
|
15
|
+
* - its package's own build, in a CHECKOUT of the core (the package has its sources beside it):
|
|
16
|
+
* built here when it is older than what it is built from — a stale build is yesterday's host
|
|
17
|
+
* under today's SDK, and it fails in ways that name nothing;
|
|
18
|
+
* - vendored next to the CLI as runtime/web and runtime/web-page (scripts/vendor-runtime.ts —
|
|
19
|
+
* the npm package and the standalone binary).
|
|
20
|
+
* `null` = no player available (the dev server then serves everything but /web/).
|
|
13
21
|
*/
|
|
14
|
-
export
|
|
15
|
-
|
|
22
|
+
export type WebRunner = { player: string, page: string | null }
|
|
23
|
+
|
|
24
|
+
type Built = {
|
|
25
|
+
/** The package, as npm names it. */
|
|
26
|
+
pkg: string
|
|
27
|
+
/** Its build, a folder of the package. */
|
|
28
|
+
dist: string
|
|
29
|
+
/** The file a build has. */
|
|
30
|
+
entry: string
|
|
31
|
+
/** The folder it is vendored as, beside the CLI. */
|
|
32
|
+
vendored: string
|
|
33
|
+
/** What it is built from, relative to the repo. */
|
|
34
|
+
sources: string[]
|
|
35
|
+
/** The arguments of the `vite build` that makes it. */
|
|
36
|
+
build: string[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const PLAYER: Built = {
|
|
40
|
+
pkg: "lecodes-web-player", dist: "dist", entry: "player.js", vendored: "web", build: [],
|
|
41
|
+
sources: ["hosts/web-player/src", "hosts/web-player/build", "hosts/web-player/index.html", "hosts/web-host/src", "renderers/web-canvas/src", "renderers/web-shared/src", "runtime/web/src", "runtime/web/native/dist", "hosts/plugins/plugins"],
|
|
42
|
+
}
|
|
43
|
+
const PAGE: Built = {
|
|
44
|
+
// (the package's own build is the element, a library; the page is another)
|
|
45
|
+
pkg: "lecodes-web-frame", dist: "dist-page", entry: "page.js", vendored: "web-page", build: ["-c", "vite.page.config.ts"],
|
|
46
|
+
sources: ["hosts/web-frame/src", "hosts/web-frame/page", "hosts/web-player/src/protocol.ts"],
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const resolveWebRunner = (log: (message: string) => void = () => {}): WebRunner | null => {
|
|
50
|
+
const player = resolve(PLAYER, log)
|
|
51
|
+
return player ? { player, page: resolve(PAGE, log) } : null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const resolve = (built: Built, log: (message: string) => void): string | null => {
|
|
55
|
+
const has = (dir: string) => existsSync(join(dir, "index.html")) && existsSync(join(dir, built.entry))
|
|
56
|
+
const checkout = checkoutOf(built.pkg)
|
|
57
|
+
if (checkout) {
|
|
58
|
+
const dist = join(checkout, built.dist)
|
|
59
|
+
const repo = join(checkout, "..", "..")
|
|
60
|
+
if (!has(dist) || newest(built.sources.map((s) => join(repo, s))) > statSync(join(dist, built.entry)).mtimeMs) {
|
|
61
|
+
log(`Building ${built.pkg} (its sources are newer than its build)…`)
|
|
62
|
+
const done = spawnSync("bunx", ["vite", "build", ...built.build], { cwd: checkout, stdio: "pipe", encoding: "utf8", shell: process.platform === "win32" })
|
|
63
|
+
if (done.status !== 0) log(`${built.pkg} did not build — the browser gets the build that is there:\n${done.stderr || done.stdout}`)
|
|
64
|
+
}
|
|
65
|
+
if (has(dist)) return dist
|
|
66
|
+
}
|
|
67
|
+
const vendored = join(distRoot, "runtime", built.vendored)
|
|
68
|
+
return has(vendored) ? vendored : null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A package when it is a checkout (its sources are there), else null. */
|
|
72
|
+
const checkoutOf = (pkg: string): string | null => {
|
|
16
73
|
try {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
} catch {
|
|
20
|
-
|
|
21
|
-
|
|
74
|
+
const dir = dirname(createRequire(import.meta.url).resolve(`${pkg}/package.json`))
|
|
75
|
+
return existsSync(join(dir, "vite.config.ts")) && existsSync(join(dir, "src")) ? dir : null
|
|
76
|
+
} catch { return null }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The latest modification under the paths (0 when none exists). */
|
|
80
|
+
const newest = (paths: string[]): number => {
|
|
81
|
+
let latest = 0
|
|
82
|
+
const walk = (path: string) => {
|
|
83
|
+
let stat
|
|
84
|
+
try { stat = statSync(path) } catch { return }
|
|
85
|
+
if (!stat.isDirectory()) { latest = Math.max(latest, stat.mtimeMs); return }
|
|
86
|
+
for (const name of readdirSync(path)) if (name !== "node_modules" && name !== "ios" && name !== "android") walk(join(path, name))
|
|
87
|
+
}
|
|
88
|
+
for (const path of paths) walk(path)
|
|
89
|
+
return latest
|
|
22
90
|
}
|
|
@@ -2,7 +2,7 @@ import { existsSync, readdirSync } from "node:fs"
|
|
|
2
2
|
import { homedir } from "node:os"
|
|
3
3
|
import { join, resolve } from "node:path"
|
|
4
4
|
import { downloadReleaseArchive, fetchReleaseProduct, newestForPlatform, semverNewer, type ManifestFile, type ManifestVersion } from "./releases"
|
|
5
|
-
import { CliError, note } from "
|
|
5
|
+
import { CliError, note } from "../cli"
|
|
6
6
|
|
|
7
7
|
/*
|
|
8
8
|
* Filament's cmgen (environment tool) — the tool behind `lecodes assets sky`, which turns an
|
|
@@ -14,7 +14,7 @@ import { CliError, note } from "./util"
|
|
|
14
14
|
* releases/versions.json — published by scripts/release/release-windows.ps1 from the desktop
|
|
15
15
|
* build's in-tree filament host tools, so the KTX it writes always matches the engine the hosts
|
|
16
16
|
* ship). Cache: ~/.lecodes/cmgen/cmgen-<version>/. LECODES_CMGEN overrides resolution entirely
|
|
17
|
-
* (local dev:
|
|
17
|
+
* (local dev: hosts/desktop/build/creator-gl/filament/tools/cmgen/cmgen.exe).
|
|
18
18
|
*
|
|
19
19
|
* The CLI resolves it and hands the path to lecodes-assets through LECODES_CMGEN, so that package
|
|
20
20
|
* stays network-free and still works standalone against a local build.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { execFileSync, spawn, spawnSync } from "node:child_process"
|
|
2
|
-
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, linkSync } from "node:fs"
|
|
3
3
|
import { homedir, tmpdir } from "node:os"
|
|
4
4
|
import { delimiter, join, resolve } from "node:path"
|
|
5
5
|
import { cachedArtifactSha, downloadReleaseArchive, fetchReleaseProduct, semverNewer, type ManifestVersion } from "./releases"
|
|
6
|
-
import { CliError, note, warnErr } from "
|
|
7
|
-
import { makeStackDemangler } from "
|
|
6
|
+
import { CliError, note, warnErr } from "../cli"
|
|
7
|
+
import { makeStackDemangler } from "../compile/nativeStack"
|
|
8
8
|
|
|
9
9
|
/*
|
|
10
10
|
* The NATIVE renderer behind `lecodes render --desktop`: the LeCodes Desktop host (tgfx UI + Filament
|
|
@@ -15,7 +15,7 @@ import { makeStackDemangler } from "./compile/nativeStack"
|
|
|
15
15
|
* natively). It downloads on first use from the same S3 release bucket the desktop distribution
|
|
16
16
|
* publishes to (scripts/release/* → releases/versions.json), sha256-verified, into
|
|
17
17
|
* ~/.lecodes/renderer/desktop-<version>/. LECODES_DESKTOP_EXE overrides resolution entirely
|
|
18
|
-
* (local dev: point it at
|
|
18
|
+
* (local dev: point it at hosts/desktop/build/lecodes-desktop.exe).
|
|
19
19
|
*
|
|
20
20
|
* Platforms: windows-x64 + linux (the ubuntu build) + macos-arm64. The macOS host is built and
|
|
21
21
|
* published separately (the Apple repo's LeCodes Desktop.app, via scripts/release/release-macos.sh)
|
|
@@ -38,9 +38,10 @@ export const DESKTOP_RENDERER_VERSION = "1.1.0"
|
|
|
38
38
|
export { semverNewer }
|
|
39
39
|
|
|
40
40
|
/** Which desktop host to run. "gl" is the default everywhere; "vulkan" is a separate Windows binary
|
|
41
|
-
* (Filament + tgfx on one VkDevice), NOT a runtime switch — it has its own artifact, its
|
|
42
|
-
* shaders, and a smaller feature set (no
|
|
43
|
-
* app.json `desktop.renderer`, or per run via `--renderer` /
|
|
41
|
+
* (Filament + tgfx + creator-2d on one VkDevice), NOT a runtime switch — it has its own artifact, its
|
|
42
|
+
* own shaders, and a smaller feature set (no native-view plugins; the 2D engine joined it 2026-09-08,
|
|
43
|
+
* i.e. desktop releases after 1.4.1). Chosen per project via app.json `desktop.renderer`, or per run via `--renderer` /
|
|
44
|
+
* LECODES_DESKTOP_RENDERER. */
|
|
44
45
|
export type DesktopRenderer = "gl" | "vulkan"
|
|
45
46
|
|
|
46
47
|
export const DESKTOP_RENDERER_MIN_VERSION: Record<DesktopRenderer, string> = { gl: "0.0.0", vulkan: "1.4.0" }
|
|
@@ -57,8 +58,9 @@ const platformKey = (renderer: DesktopRenderer = "gl"): string => {
|
|
|
57
58
|
|
|
58
59
|
const isMac = () => process.platform === "darwin"
|
|
59
60
|
|
|
60
|
-
/** The host binary's file name
|
|
61
|
-
|
|
61
|
+
/** The host binary's file name — the same on every OS; on macOS it sits inside LeCodes Desktop.app
|
|
62
|
+
* (Contents/MacOS/lecodes-desktop, hosts/desktop/package-macos.sh). */
|
|
63
|
+
const exeName = () => (process.platform === "win32" ? "lecodes-desktop.exe" : "lecodes-desktop")
|
|
62
64
|
|
|
63
65
|
/** Recursively locate the host binary inside an extracted archive (the zip nests a
|
|
64
66
|
* `lecodes-desktop/` top-level folder; the tarballs may differ — search, don't assume). */
|
|
@@ -166,7 +168,7 @@ export const findMacHostExe = (): string | null => {
|
|
|
166
168
|
const candidates = [
|
|
167
169
|
...["/Applications", join(homedir(), "Applications")]
|
|
168
170
|
.flatMap((root) => ["LeCodes Desktop.app", "LeCodes.app"].map((app) => join(root, app))),
|
|
169
|
-
].map((app) => join(app, "Contents", "MacOS", "lecodes-
|
|
171
|
+
].map((app) => join(app, "Contents", "MacOS", "lecodes-desktop"))
|
|
170
172
|
return candidates.find((exe) => existsSync(exe)) ?? null
|
|
171
173
|
}
|
|
172
174
|
|
|
@@ -204,7 +206,7 @@ const resolveMacExe = async (selected: string): Promise<string> => {
|
|
|
204
206
|
throw new CliError(
|
|
205
207
|
`${e instanceof Error ? e.message : String(e)}\n` +
|
|
206
208
|
" No LeCodes Desktop.app is installed either — retry online (`lecodes desktop update`), install the DMG into\n" +
|
|
207
|
-
" /Applications, or point LECODES_DESKTOP_EXE at a build (
|
|
209
|
+
" /Applications, or point LECODES_DESKTOP_EXE at a build (hosts/desktop/build-macos/lecodes-desktop, or an .app's Contents/MacOS/lecodes-desktop).",
|
|
208
210
|
)
|
|
209
211
|
}
|
|
210
212
|
}
|
|
@@ -234,6 +236,38 @@ export const resolveDesktopExe = async (renderer: DesktopRenderer = "gl"): Promi
|
|
|
234
236
|
return exe
|
|
235
237
|
}
|
|
236
238
|
|
|
239
|
+
/** Which native host runs a bundle: the desktop (a window, a GPU, pixels) or the windowless server
|
|
240
|
+
* (hosts/desktop lecodes-server: the same engine on Filament's NOOP backend — the dedicated
|
|
241
|
+
* multiplayer server, a headless client, an offline simulation run; no UI hit-test, no pixels). */
|
|
242
|
+
export type HostKind = "desktop" | "server"
|
|
243
|
+
|
|
244
|
+
const serverExeName = () => (process.platform === "win32" ? "lecodes-server.exe" : "lecodes-server")
|
|
245
|
+
|
|
246
|
+
/** The windowless host: LECODES_SERVER_EXE override → the binary beside the resolved desktop host
|
|
247
|
+
* (one product, two executables — a release archive and a repo build both keep them side by side,
|
|
248
|
+
* sharing runtime/). A host older than the server's introduction has none beside it. */
|
|
249
|
+
export const resolveServerExe = async (renderer: DesktopRenderer = "gl"): Promise<string> => {
|
|
250
|
+
const override = process.env.LECODES_SERVER_EXE
|
|
251
|
+
if (override) {
|
|
252
|
+
if (!existsSync(override)) throw new CliError(`LECODES_SERVER_EXE points at "${override}" which doesn't exist.`)
|
|
253
|
+
return resolve(override)
|
|
254
|
+
}
|
|
255
|
+
// macOS: the server ships inside the .app beside the window host (Contents/MacOS/) since the
|
|
256
|
+
// in-repo Metal host (hosts/desktop/build-macos.sh); the "beside the desktop exe" rule holds.
|
|
257
|
+
const desktopExe = await resolveDesktopExe(renderer)
|
|
258
|
+
const exe = join(desktopExe, "..", serverExeName())
|
|
259
|
+
if (!existsSync(exe)) {
|
|
260
|
+
throw new CliError(
|
|
261
|
+
`No ${serverExeName()} beside ${desktopExe} — this desktop host predates the windowless server. ` +
|
|
262
|
+
"Update it (lecodes desktop update), or point LECODES_SERVER_EXE at a build (hosts/desktop/build[-linux|-macos]/lecodes-server).",
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
return exe
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const resolveHostExe = (host: HostKind | undefined, renderer: DesktopRenderer | undefined): Promise<string> =>
|
|
269
|
+
host === "server" ? resolveServerExe(renderer) : resolveDesktopExe(renderer)
|
|
270
|
+
|
|
237
271
|
/** `lecodes desktop update`: move the render host to the newest published release (never below the
|
|
238
272
|
* CLI's pin — a manifest that somehow regressed under it is an error, not a downgrade). Saves the
|
|
239
273
|
* selection so later `render --desktop` runs use it; the download is skipped when already cached. */
|
|
@@ -278,6 +312,8 @@ export type DesktopWindowOpts = {
|
|
|
278
312
|
/** Which host binary to run (see DesktopRenderer). Must match the renderer the bundle's shaders
|
|
279
313
|
* were staged for — resolve it once per command and pass the same value to both. */
|
|
280
314
|
renderer?: DesktopRenderer
|
|
315
|
+
/** The desktop window (default) or the windowless server host (see HostKind). */
|
|
316
|
+
host?: HostKind
|
|
281
317
|
width?: number
|
|
282
318
|
height?: number
|
|
283
319
|
minWidth?: number
|
|
@@ -304,8 +340,16 @@ export type DesktopWindowOpts = {
|
|
|
304
340
|
* the block, forwarded verbatim as CREATOR_VIRTUAL_KEYBOARD JSON. A set env wins, like the mouse knob. */
|
|
305
341
|
virtualKeyboard?: boolean | object
|
|
306
342
|
/** Draw the host's frame counter in the corner (host ≥1.5.0; older hosts ignore the env). Costs
|
|
307
|
-
* nothing to leave off and next to nothing on — the digits are rectangles, no UI behind them.
|
|
343
|
+
* nothing to leave off and next to nothing on — the digits are rectangles, no UI behind them.
|
|
344
|
+
* On macOS too (host 2026-09-09; the CLI sets the same env there). */
|
|
308
345
|
fps?: boolean
|
|
346
|
+
/** The host's profiler (CREATOR_PERF=1, host 2026-09-26): a `[perf]` line a second — GPU frame time from
|
|
347
|
+
* Filament's timer query, the CPU split, visible / total renderables, the view's switches — and the
|
|
348
|
+
* fps overlay's second row. docs/profiling.md in the core repo. */
|
|
349
|
+
perf?: boolean
|
|
350
|
+
/** app.json desktop.frameRate — a CAP on the host's render loop, "display" or a number; forwarded
|
|
351
|
+
* verbatim as CREATOR_FRAME_RATE (a set env wins). macOS host 2026-09-09; Windows ignores it. */
|
|
352
|
+
frameRate?: number | "display"
|
|
309
353
|
/** Multiplayer launch facts for the host (`--server --port N --max-clients N` / `--connect ip:port`);
|
|
310
354
|
* the SDK reads them through `Net.launch` and decides (docs/multiplayer-plan.md). */
|
|
311
355
|
net?: { role: "server" | "client"; address?: string; port?: number; maxClients?: number }
|
|
@@ -339,6 +383,10 @@ const windowEnv = (opts: DesktopWindowOpts): Record<string, string> => {
|
|
|
339
383
|
env.CREATOR_RENDER_MAX = `${Math.round(opts.maxRenderSize[0])}x${Math.round(opts.maxRenderSize[1])}`
|
|
340
384
|
}
|
|
341
385
|
if (opts.fps) env.CREATOR_FPS = "1"
|
|
386
|
+
if (opts.perf) env.CREATOR_PERF = "1"
|
|
387
|
+
if (opts.frameRate !== undefined && process.env.CREATOR_FRAME_RATE === undefined) {
|
|
388
|
+
env.CREATOR_FRAME_RATE = String(opts.frameRate)
|
|
389
|
+
}
|
|
342
390
|
if (opts.mouseEmulateTouch !== undefined && process.env.CREATOR_MOUSE_TOUCH === undefined) {
|
|
343
391
|
env.CREATOR_MOUSE_TOUCH = opts.mouseEmulateTouch ? "1" : "0"
|
|
344
392
|
}
|
|
@@ -366,6 +414,9 @@ export type DesktopRenderOptions = {
|
|
|
366
414
|
/** Which host binary to run (see DesktopRenderer). Must match the renderer the bundle's shaders
|
|
367
415
|
* were staged for — resolve it once per command and pass the same value to both. */
|
|
368
416
|
renderer?: DesktopRenderer
|
|
417
|
+
/** The hidden desktop host (default: pixels, a PNG) or the windowless server host (no pixels — the
|
|
418
|
+
* run is judged by its output alone; `outPng` is ignored). */
|
|
419
|
+
host?: HostKind
|
|
369
420
|
width: number
|
|
370
421
|
height: number
|
|
371
422
|
/** Frames to run before the dump — headroom for async loads (GLB, images, fonts). */
|
|
@@ -375,7 +426,7 @@ export type DesktopRenderOptions = {
|
|
|
375
426
|
/** Forward the host's stdout/stderr (app console.log included) to stderr. */
|
|
376
427
|
logs: boolean
|
|
377
428
|
timeoutMs: number
|
|
378
|
-
/** CREATOR_SCRIPT contents (the line format in
|
|
429
|
+
/** CREATOR_SCRIPT contents (the line format in hosts/desktop/src/main.cpp `scriptStep`): the
|
|
379
430
|
* host drives these commands and self-terminates when done; `frames` is then only a safety cap. */
|
|
380
431
|
script?: string
|
|
381
432
|
/** CREATOR_FIXED_DT: virtual app clock, ms per frame (0/undefined = wall clock). */
|
|
@@ -395,13 +446,29 @@ export type DesktopRenderOutcome = {
|
|
|
395
446
|
|
|
396
447
|
/** Stage a compiled bundle for the host: app.js + the local-asset resources copied next to it
|
|
397
448
|
* (the host's fetchLocal resolves them by basename relative to its cwd). Caller removes the dir. */
|
|
449
|
+
/** The staging dir the host runs from: app.js + every resource the bundle names, next to it (fetchLocal's
|
|
450
|
+
* fallback). A resource is HARD-LINKED when the temp dir shares the project's volume (the usual case:
|
|
451
|
+
* both on C:) and copied otherwise — a yard-sized project names hundreds of MB of models and textures,
|
|
452
|
+
* and copying them was seconds of "the window has not opened yet" on every run. The host only reads
|
|
453
|
+
* them, and the dir is removed after the run, so a link is as safe as a copy. */
|
|
398
454
|
const stageBundle = (js: string, resources: { name: string, absPath?: string }[]): string => {
|
|
455
|
+
const t0 = performance.now()
|
|
399
456
|
const runDir = mkdtempSync(join(tmpdir(), "lecodes-render-"))
|
|
400
457
|
writeFileSync(join(runDir, "app.js"), js)
|
|
458
|
+
let files = 0, bytes = 0, copied = 0
|
|
401
459
|
for (const r of resources) {
|
|
402
|
-
if (r.absPath
|
|
403
|
-
|
|
404
|
-
|
|
460
|
+
if (!r.absPath || !existsSync(r.absPath)) continue
|
|
461
|
+
const st = statSync(r.absPath)
|
|
462
|
+
if (!st.isFile()) continue
|
|
463
|
+
const dst = join(runDir, r.name)
|
|
464
|
+
try { linkSync(r.absPath, dst) }
|
|
465
|
+
catch { copyFileSync(r.absPath, dst); copied++ }
|
|
466
|
+
files++
|
|
467
|
+
bytes += st.size
|
|
468
|
+
}
|
|
469
|
+
const ms = performance.now() - t0
|
|
470
|
+
if (files > 0 && (ms > 250 || copied > 0)) {
|
|
471
|
+
note(`Staged ${files} resources (${(bytes / 1048576).toFixed(0)} MB${copied > 0 ? `, ${copied} copied` : ", linked"}) in ${(ms / 1000).toFixed(1)} s`)
|
|
405
472
|
}
|
|
406
473
|
return runDir
|
|
407
474
|
}
|
|
@@ -428,7 +495,8 @@ export const runDesktopApp = async (
|
|
|
428
495
|
resources: { name: string, absPath?: string }[],
|
|
429
496
|
opts: DesktopWindowOpts = {},
|
|
430
497
|
): Promise<void> => {
|
|
431
|
-
const exe = await
|
|
498
|
+
const exe = await resolveHostExe(opts.host, opts.renderer)
|
|
499
|
+
const what = opts.host === "server" ? "server" : "desktop"
|
|
432
500
|
const runDir = stageBundle(js, resources)
|
|
433
501
|
try {
|
|
434
502
|
const env = { ...process.env, ...windowEnv(opts) }
|
|
@@ -444,10 +512,15 @@ export const runDesktopApp = async (
|
|
|
444
512
|
pipeThroughDemangler(child.stdout!, process.stdout, demangle)
|
|
445
513
|
pipeThroughDemangler(child.stderr!, process.stderr, demangle)
|
|
446
514
|
}
|
|
447
|
-
child
|
|
515
|
+
// Ctrl+C in the terminal: the child gets it too on a console (Windows) or via SIGINT (POSIX),
|
|
516
|
+
// and the server host tears its endpoint down on it; make sure a detached one is not left behind.
|
|
517
|
+
const onSigint = () => { child.kill("SIGINT") }
|
|
518
|
+
process.once("SIGINT", onSigint)
|
|
519
|
+
child.on("error", (e) => { process.off("SIGINT", onSigint); fail(new CliError(`Can't run the ${what} host: ${e.message}`)) })
|
|
448
520
|
child.on("exit", (code) => {
|
|
521
|
+
process.off("SIGINT", onSigint)
|
|
449
522
|
if (code === 0 || code === null) done() // null: killed (e.g. Ctrl+C reached the child)
|
|
450
|
-
else fail(new CliError(`The
|
|
523
|
+
else fail(new CliError(`The ${what} host exited with code ${code}.`))
|
|
451
524
|
})
|
|
452
525
|
})
|
|
453
526
|
} finally {
|
|
@@ -479,30 +552,29 @@ export const runDesktopDev = async (url: string, opts: DesktopWindowOpts = {}):
|
|
|
479
552
|
})
|
|
480
553
|
}
|
|
481
554
|
|
|
482
|
-
/** Run a compiled bundle in the hidden desktop host and dump the final frame as a PNG
|
|
555
|
+
/** Run a compiled bundle in the hidden desktop host and dump the final frame as a PNG — or, with
|
|
556
|
+
* `host: "server"`, in the windowless host, which paints nothing: the outcome is its output. */
|
|
483
557
|
export const runDesktopRender = async (
|
|
484
558
|
js: string,
|
|
485
559
|
resources: { name: string, absPath?: string }[],
|
|
486
560
|
opts: DesktopRenderOptions,
|
|
487
561
|
): Promise<DesktopRenderOutcome> => {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}
|
|
494
|
-
const exe = await resolveDesktopExe(opts.renderer)
|
|
562
|
+
const server = opts.host === "server"
|
|
563
|
+
// macOS: the in-repo Metal host (hosts/desktop, phase 11) carries the headless render contract like
|
|
564
|
+
// the other two platforms (CREATOR_HIDDEN / CREATOR_DUMP_PNG / CREATOR_SCRIPT); the old Swift shell
|
|
565
|
+
// did not — a cached one is refused by its missing lecodes-server, not here.
|
|
566
|
+
const exe = await resolveHostExe(opts.host, opts.renderer)
|
|
495
567
|
const runDir = stageBundle(js, resources)
|
|
496
568
|
try {
|
|
497
569
|
const appJs = join(runDir, "app.js")
|
|
498
570
|
const outAbs = resolve(opts.outPng)
|
|
499
571
|
const env: Record<string, string | undefined> = {
|
|
500
572
|
...process.env,
|
|
501
|
-
CREATOR_DUMP_PNG: outAbs,
|
|
502
573
|
CREATOR_MAX_FRAMES: String(opts.frames),
|
|
503
574
|
CREATOR_WINDOW_SIZE: `${opts.width}x${opts.height}`,
|
|
504
|
-
CREATOR_HIDDEN: "1",
|
|
575
|
+
CREATOR_HIDDEN: "1", // a clean storage every run (and, on the desktop host, no window)
|
|
505
576
|
}
|
|
577
|
+
if (!server) env.CREATOR_DUMP_PNG = outAbs
|
|
506
578
|
if (opts.script !== undefined) {
|
|
507
579
|
const scriptPath = join(runDir, "script.txt")
|
|
508
580
|
writeFileSync(scriptPath, opts.script)
|
|
@@ -534,17 +606,17 @@ export const runDesktopRender = async (
|
|
|
534
606
|
child.kill()
|
|
535
607
|
fail(new CliError(`The native render timed out after ${opts.timeoutMs}ms (raise --timeout, or lower --frames).`))
|
|
536
608
|
}, opts.timeoutMs)
|
|
537
|
-
child.on("error", (e) => { clearTimeout(killer); fail(new CliError(`Can't run the desktop
|
|
609
|
+
child.on("error", (e) => { clearTimeout(killer); fail(new CliError(`Can't run the ${server ? "server" : "desktop"} host: ${e.message}`)) })
|
|
538
610
|
child.on("exit", (code) => {
|
|
539
611
|
clearTimeout(killer)
|
|
540
612
|
const output = demangle(chunks.join(""))
|
|
541
613
|
if (stopped) return done({ output, exitCode: 0 })
|
|
542
|
-
if (code === 0 && existsSync(outAbs)) return done({ output, exitCode: code })
|
|
614
|
+
if (code === 0 && (server || existsSync(outAbs))) return done({ output, exitCode: code })
|
|
543
615
|
const tail = demangle(chunks.slice(-80).join("")).trimEnd()
|
|
544
616
|
const detail = tail.length > 0 ? `\n${tail}` : ""
|
|
545
617
|
fail(new CliError(code === 0
|
|
546
618
|
? `The renderer exited cleanly but produced no PNG — did the app open a screen?${detail}`
|
|
547
|
-
: `The renderer exited with code ${code}.${detail}`))
|
|
619
|
+
: `The ${server ? "server host" : "renderer"} exited with code ${code}.${detail}`))
|
|
548
620
|
})
|
|
549
621
|
})
|
|
550
622
|
} finally {
|