lecodes-cli 0.19.0 → 0.20.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 +1 -0
- package/dist/index.js +1069 -396
- package/package.json +14 -13
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/assets/Roboto-Variable-DHm9jpd-.woff2 +0 -0
- package/runtime/web/assets/__vite-browser-external-BIHI7g3E.js +1 -0
- package/runtime/web/assets/basis-C64VHDVD.js +1 -0
- package/runtime/web/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
- package/runtime/web/assets/createViewerLite-BvHobVhp.js +852 -0
- package/runtime/web/assets/draco-BiISTFcR.js +118 -0
- package/runtime/web/assets/draco_decoder-DsQ12WqX.wasm +0 -0
- package/runtime/web/assets/index-CfNCCBjN.js +2 -0
- package/runtime/web/assets/mapViewImpl-B2JcES8l.js +810 -0
- package/runtime/web/assets/maplibre-gl-worker-CJfwIrte.js +8 -0
- package/runtime/web/assets/neutral_ibl128-D-lzdkLN.ktx +0 -0
- package/runtime/web/assets/worker-Caf-yYEI.js +2 -0
- package/runtime/web/embed.html +32 -0
- package/runtime/web/embed.js +171 -0
- package/src/api.ts +302 -0
- package/src/browserAuth.ts +87 -0
- package/src/cmgenTool.ts +104 -0
- package/src/commands/app.ts +896 -0
- package/src/commands/appAndroid.ts +603 -0
- package/src/commands/appDesktop.ts +339 -0
- package/src/commands/appDesktopMac.ts +470 -0
- package/src/commands/appIcon.ts +187 -0
- package/src/commands/appShared.ts +542 -0
- package/src/commands/appTemplates.ts +907 -0
- package/src/commands/appTemplatesAndroid.ts +599 -0
- package/src/commands/appTemplatesGradlew.ts +9 -0
- package/src/commands/assets.ts +28 -0
- package/src/commands/clone.ts +60 -0
- package/src/commands/compile.ts +253 -0
- package/src/commands/create.ts +59 -0
- package/src/commands/design.ts +863 -0
- package/src/commands/designTemplates.ts +11 -0
- package/src/commands/desktop.ts +127 -0
- package/src/commands/dev.ts +236 -0
- package/src/commands/diff.ts +59 -0
- package/src/commands/init.ts +191 -0
- package/src/commands/install.ts +198 -0
- package/src/commands/lightmap.ts +290 -0
- package/src/commands/link.ts +147 -0
- package/src/commands/login.ts +24 -0
- package/src/commands/navmesh.ts +225 -0
- package/src/commands/pn.ts +305 -0
- package/src/commands/projectTemplates.ts +130 -0
- package/src/commands/pull.ts +109 -0
- package/src/commands/push.ts +173 -0
- package/src/commands/render.ts +416 -0
- package/src/commands/scene.ts +148 -0
- package/src/commands/shaders.ts +157 -0
- package/src/commands/status.ts +29 -0
- package/src/commands/test.ts +371 -0
- package/src/commands/thumbs.ts +178 -0
- package/src/commands/types.ts +87 -0
- package/src/commands/update.ts +190 -0
- package/src/compile/assetIcons.ts +214 -0
- package/src/compile/collect.ts +46 -0
- package/src/compile/collectLocal.ts +37 -0
- package/src/compile/designCompile.ts +104 -0
- package/src/compile/fonts.ts +156 -0
- package/src/compile/headlessBundle.ts +129 -0
- package/src/compile/nativeStack.ts +21 -0
- package/src/compile/projectCompile.ts +109 -0
- package/src/compile/sceneCompile.ts +113 -0
- package/src/compile/screenEntry.ts +127 -0
- package/src/compile/shaders.ts +245 -0
- package/src/config.ts +42 -0
- package/src/designMeta.ts +35 -0
- package/src/desktopRenderer.ts +545 -0
- package/src/desktopScript.ts +276 -0
- package/src/dev/clientTemplates.ts +160 -0
- package/src/dev/devServer.ts +333 -0
- package/src/dev/webRunner.ts +22 -0
- package/src/dev/wsServer.ts +144 -0
- package/src/distRoot.ts +20 -0
- package/src/ignore.ts +163 -0
- package/src/index.ts +494 -0
- package/src/lecodes-3d-editor.d.ts +41 -0
- package/src/lecodes-assets.d.ts +7 -0
- package/src/lecodes-design.d.ts +191 -0
- package/src/lecodes-renderer.d.ts +131 -0
- package/src/localFiles.ts +144 -0
- package/src/manifest.ts +46 -0
- package/src/matcTool.ts +137 -0
- package/src/peers.ts +40 -0
- package/src/project.ts +61 -0
- package/src/projectEnv.ts +94 -0
- package/src/qrcode-terminal.d.ts +9 -0
- package/src/releases.ts +125 -0
- package/src/serverDiff.ts +78 -0
- package/src/textDiff.ts +103 -0
- package/src/types.ts +0 -0
- package/src/util.ts +146 -0
- package/runtime/sdk/animate/animate.ts +0 -238
- package/runtime/sdk/animate/bezier.ts +0 -138
- package/runtime/sdk/animate/easings.ts +0 -126
- package/runtime/sdk/canvas/Canvas.ts +0 -305
- package/runtime/sdk/compile/aspectMacro.ts +0 -86
- package/runtime/sdk/compile/assetIconMacro.ts +0 -384
- package/runtime/sdk/compile/assetMacro.ts +0 -146
- package/runtime/sdk/compile/assetName.ts +0 -50
- package/runtime/sdk/compile/bundler.ts +0 -287
- package/runtime/sdk/compile/compileProject.ts +0 -129
- package/runtime/sdk/compile/detectEntry.ts +0 -128
- package/runtime/sdk/compile/fontMacro.ts +0 -459
- package/runtime/sdk/compile/fontRegistry.ts +0 -78
- package/runtime/sdk/compile/header.ts +0 -67
- package/runtime/sdk/compile/index.ts +0 -100
- package/runtime/sdk/compile/libraryImports.ts +0 -52
- package/runtime/sdk/compile/liteMaterial.ts +0 -247
- package/runtime/sdk/compile/sceneEditor.ts +0 -88
- package/runtime/sdk/compile/serverSplit.ts +0 -233
- package/runtime/sdk/compile/serverTypes.ts +0 -227
- package/runtime/sdk/compile/sfnt.ts +0 -98
- package/runtime/sdk/compile/shaderSchema.ts +0 -202
- package/runtime/sdk/compile/shaderTargets.ts +0 -81
- package/runtime/sdk/compile/sourcemap.ts +0 -25
- package/runtime/sdk/core/Aspect.ts +0 -568
- package/runtime/sdk/core/InspectorUI.ts +0 -212
- package/runtime/sdk/core/StateMachine.ts +0 -308
- package/runtime/sdk/core/color.ts +0 -66
- package/runtime/sdk/core/compWrite.ts +0 -42
- package/runtime/sdk/core/events.ts +0 -38
- package/runtime/sdk/core/fields.ts +0 -120
- package/runtime/sdk/core/registry.ts +0 -23
- package/runtime/sdk/core/signals.ts +0 -277
- package/runtime/sdk/core/time.ts +0 -84
- package/runtime/sdk/g2/Camera2D.ts +0 -40
- package/runtime/sdk/g2/CharacterController2D.ts +0 -276
- package/runtime/sdk/g2/Node2D.ts +0 -267
- package/runtime/sdk/g2/OneWay2D.ts +0 -66
- package/runtime/sdk/g2/Physics2D.ts +0 -346
- package/runtime/sdk/g2/Scene2D.ts +0 -209
- package/runtime/sdk/g2/Shape2D.ts +0 -259
- package/runtime/sdk/g2/Sprite.ts +0 -89
- package/runtime/sdk/g2/SpriteAnimation.ts +0 -171
- package/runtime/sdk/g2/SpriteSheet.ts +0 -166
- package/runtime/sdk/g2/Texture2D.ts +0 -47
- package/runtime/sdk/g2/Tilemap.ts +0 -41
- package/runtime/sdk/g2/Tileset.ts +0 -71
- package/runtime/sdk/g2/Trigger2D.ts +0 -77
- package/runtime/sdk/g2/autotile.ts +0 -433
- package/runtime/sdk/g2/cells.ts +0 -91
- package/runtime/sdk/g2/defineScene2d.ts +0 -381
- package/runtime/sdk/g2/groups2d.ts +0 -106
- package/runtime/sdk/g2/loop.ts +0 -50
- package/runtime/sdk/g2/scenarios2d.ts +0 -69
- package/runtime/sdk/g2/touch.ts +0 -83
- package/runtime/sdk/gl/Camera.ts +0 -160
- package/runtime/sdk/gl/CameraPlace.ts +0 -52
- package/runtime/sdk/gl/CharacterController.ts +0 -238
- package/runtime/sdk/gl/Gearbox.ts +0 -212
- package/runtime/sdk/gl/Geometry.ts +0 -279
- package/runtime/sdk/gl/IK.ts +0 -193
- package/runtime/sdk/gl/InstancedMesh.ts +0 -132
- package/runtime/sdk/gl/Light.ts +0 -107
- package/runtime/sdk/gl/Lightmap.ts +0 -256
- package/runtime/sdk/gl/Material.ts +0 -260
- package/runtime/sdk/gl/Mesh.ts +0 -83
- package/runtime/sdk/gl/Model.ts +0 -124
- package/runtime/sdk/gl/NavAgent.ts +0 -337
- package/runtime/sdk/gl/NavMesh.ts +0 -397
- package/runtime/sdk/gl/Node.ts +0 -350
- package/runtime/sdk/gl/Noise.ts +0 -30
- package/runtime/sdk/gl/Particles.ts +0 -676
- package/runtime/sdk/gl/Physics.ts +0 -222
- package/runtime/sdk/gl/Plane.ts +0 -53
- package/runtime/sdk/gl/Ragdoll.ts +0 -270
- package/runtime/sdk/gl/Ray.ts +0 -16
- package/runtime/sdk/gl/Scene.ts +0 -486
- package/runtime/sdk/gl/Shape.ts +0 -409
- package/runtime/sdk/gl/Terrain.ts +0 -1092
- package/runtime/sdk/gl/Texture.ts +0 -63
- package/runtime/sdk/gl/Trigger.ts +0 -45
- package/runtime/sdk/gl/Vehicle.ts +0 -473
- package/runtime/sdk/gl/Wheel.ts +0 -240
- package/runtime/sdk/gl/animation/AnimationClip.ts +0 -204
- package/runtime/sdk/gl/animation/Animator.ts +0 -120
- package/runtime/sdk/gl/animation/Layer.ts +0 -31
- package/runtime/sdk/gl/animation/Loop.ts +0 -30
- package/runtime/sdk/gl/animation/Playback.ts +0 -44
- package/runtime/sdk/gl/animation/core.ts +0 -336
- package/runtime/sdk/gl/controls.ts +0 -95
- package/runtime/sdk/gl/physicsEvents.ts +0 -20
- package/runtime/sdk/gl/scenarios.ts +0 -291
- package/runtime/sdk/gl/state.ts +0 -6
- package/runtime/sdk/gl/terrainMesh.ts +0 -219
- package/runtime/sdk/gl/touch.ts +0 -68
- package/runtime/sdk/inject.ts +0 -197
- package/runtime/sdk/math/Mathf.ts +0 -118
- package/runtime/sdk/math/mat4.ts +0 -278
- package/runtime/sdk/math/quat.ts +0 -232
- package/runtime/sdk/math/vec.ts +0 -255
- package/runtime/sdk/net/codec.ts +0 -119
- package/runtime/sdk/net/core.ts +0 -384
- package/runtime/sdk/net/index.ts +0 -181
- package/runtime/sdk/net/replication.ts +0 -622
- package/runtime/sdk/plugins/camera.ts +0 -81
- package/runtime/sdk/plugins/geolocation.ts +0 -123
- package/runtime/sdk/plugins/oauth.ts +0 -61
- package/runtime/sdk/plugins/permission.ts +0 -7
- package/runtime/sdk/plugins/push.ts +0 -132
- package/runtime/sdk/plugins/qr.ts +0 -73
- package/runtime/sdk/plugins/service.ts +0 -47
- package/runtime/sdk/runtime/app.ts +0 -101
- package/runtime/sdk/runtime/appEvents.ts +0 -54
- package/runtime/sdk/runtime/channel.ts +0 -61
- package/runtime/sdk/runtime/clipboard.ts +0 -20
- package/runtime/sdk/runtime/datetime.ts +0 -329
- package/runtime/sdk/runtime/device.ts +0 -293
- package/runtime/sdk/runtime/fetch.ts +0 -77
- package/runtime/sdk/runtime/files.ts +0 -23
- package/runtime/sdk/runtime/input.ts +0 -175
- package/runtime/sdk/runtime/media.ts +0 -111
- package/runtime/sdk/runtime/misc.ts +0 -16
- package/runtime/sdk/runtime/net.ts +0 -36
- package/runtime/sdk/runtime/rpc.ts +0 -218
- package/runtime/sdk/runtime/service.ts +0 -83
- package/runtime/sdk/runtime/share.ts +0 -9
- package/runtime/sdk/runtime/storage.ts +0 -13
- package/runtime/sdk/runtime/touch.ts +0 -76
- package/runtime/sdk/scene/defineScene.ts +0 -1353
- package/runtime/sdk/scene/editorPlugins.ts +0 -110
- package/runtime/sdk/scene/gizmos.ts +0 -148
- package/runtime/sdk/scene/grammar.ts +0 -120
- package/runtime/sdk/scene/material.ts +0 -188
- package/runtime/sdk/server/auth/appConfig.ts +0 -12
- package/runtime/sdk/server/auth/global.ts +0 -80
- package/runtime/sdk/server/auth/host.ts +0 -318
- package/runtime/sdk/server/auth/models.ts +0 -83
- package/runtime/sdk/server/auth/types.ts +0 -50
- package/runtime/sdk/server/channel.ts +0 -56
- package/runtime/sdk/server/context.ts +0 -36
- package/runtime/sdk/server/db/defineDb.ts +0 -237
- package/runtime/sdk/server/db/fields.ts +0 -132
- package/runtime/sdk/server/db/httpTransport.ts +0 -93
- package/runtime/sdk/server/db/index.ts +0 -7
- package/runtime/sdk/server/db/marci/query.ts +0 -412
- package/runtime/sdk/server/db/types.ts +0 -202
- package/runtime/sdk/server/errors.ts +0 -12
- package/runtime/sdk/server/host.ts +0 -74
- package/runtime/sdk/server/inject.ts +0 -13
- package/runtime/sdk/server/runtime.ts +0 -133
- package/runtime/sdk/server/validate.ts +0 -87
- package/runtime/sdk/ui/NativeView.ts +0 -142
- package/runtime/sdk/ui/UI.ts +0 -39
- package/runtime/sdk/ui/UIBottomSheet.ts +0 -139
- package/runtime/sdk/ui/UIButton.ts +0 -101
- package/runtime/sdk/ui/UIContainer.ts +0 -60
- package/runtime/sdk/ui/UIImage.ts +0 -83
- package/runtime/sdk/ui/UIInput.ts +0 -185
- package/runtime/sdk/ui/UIModal.ts +0 -139
- package/runtime/sdk/ui/UINode.ts +0 -830
- package/runtime/sdk/ui/UIPager.ts +0 -362
- package/runtime/sdk/ui/UIPopover.ts +0 -100
- package/runtime/sdk/ui/UIScreen.ts +0 -123
- package/runtime/sdk/ui/UIScrollable.ts +0 -87
- package/runtime/sdk/ui/UISpacer.ts +0 -14
- package/runtime/sdk/ui/UITabs.ts +0 -236
- package/runtime/sdk/ui/UIText.ts +0 -51
- package/runtime/sdk/ui/UIVideo.ts +0 -88
- package/runtime/sdk/ui/UIVirtualizedList.ts +0 -241
- package/runtime/sdk/ui/UIWidget.ts +0 -127
- package/runtime/sdk/ui/fonts.ts +0 -13
- package/runtime/sdk/ui/presentable.ts +0 -117
- package/runtime/sdk/ui/router.ts +0 -132
- package/runtime/sdk/ui/theme.ts +0 -84
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import { execFileSync, spawn, spawnSync } from "node:child_process"
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { homedir, tmpdir } from "node:os"
|
|
4
|
+
import { delimiter, join, resolve } from "node:path"
|
|
5
|
+
import { cachedArtifactSha, downloadReleaseArchive, fetchReleaseProduct, semverNewer, type ManifestVersion } from "./releases"
|
|
6
|
+
import { CliError, note, warnErr } from "./util"
|
|
7
|
+
import { makeStackDemangler } from "./compile/nativeStack"
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* The NATIVE renderer behind `lecodes render --desktop`: the LeCodes Desktop host (tgfx UI + Filament
|
|
11
|
+
* 3D + creator-2d, composited) run headless — CREATOR_HIDDEN + CREATOR_WINDOW_SIZE +
|
|
12
|
+
* CREATOR_MAX_FRAMES + CREATOR_DUMP_PNG (the host dumps only the final frame of a bounded run).
|
|
13
|
+
*
|
|
14
|
+
* The binary is NOT shipped with the CLI (it's ~23 MB per platform and most users never render
|
|
15
|
+
* natively). It downloads on first use from the same S3 release bucket the desktop distribution
|
|
16
|
+
* publishes to (scripts/release/* → releases/versions.json), sha256-verified, into
|
|
17
|
+
* ~/.lecodes/renderer/desktop-<version>/. LECODES_DESKTOP_EXE overrides resolution entirely
|
|
18
|
+
* (local dev: point it at packages/desktop/build/lecodes-desktop.exe).
|
|
19
|
+
*
|
|
20
|
+
* Platforms: windows-x64 + linux (the ubuntu build) + macos-arm64. The macOS host is built and
|
|
21
|
+
* published separately (the Apple repo's LeCodes Desktop.app, via scripts/release/release-macos.sh)
|
|
22
|
+
* as a notarized DMG under the same "desktop" product — so its version line has gaps: a release
|
|
23
|
+
* may ship for Windows only. On darwin the cache therefore holds the newest published version
|
|
24
|
+
* that HAS a macOS artifact (never below the pin), and an app installed in /Applications is only
|
|
25
|
+
* the fallback for when the manifest can't be reached. The headless render contract
|
|
26
|
+
* (CREATOR_HIDDEN & co) is still Windows/Linux-only — the macOS host has no hidden mode.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** The MINIMUM desktop version this CLI drives. The headless-render envs (CREATOR_HIDDEN,
|
|
30
|
+
* CREATOR_WINDOW_SIZE, final-frame-only CREATOR_DUMP_PNG) shipped in 1.1.0 — an older host would
|
|
31
|
+
* ignore the size, flash a window and dump every frame, so we require the pin rather than degrade.
|
|
32
|
+
* Bump together with desktop releases that change the render contract. `lecodes desktop update`
|
|
33
|
+
* can move FORWARD past it (selection saved in ~/.lecodes/renderer/current.json); it never goes below. */
|
|
34
|
+
export const DESKTOP_RENDERER_VERSION = "1.1.0"
|
|
35
|
+
|
|
36
|
+
// The manifest fetch + sha256-verified archive download live in releases.ts (shared with the
|
|
37
|
+
// matc tool). semverNewer is re-exported for the existing importers (commands/desktop.ts).
|
|
38
|
+
export { semverNewer }
|
|
39
|
+
|
|
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 own
|
|
42
|
+
* shaders, and a smaller feature set (no 2D engine, no native-view plugins). Chosen per project via
|
|
43
|
+
* app.json `desktop.renderer`, or per run via `--renderer` / LECODES_DESKTOP_RENDERER. */
|
|
44
|
+
export type DesktopRenderer = "gl" | "vulkan"
|
|
45
|
+
|
|
46
|
+
export const DESKTOP_RENDERER_MIN_VERSION: Record<DesktopRenderer, string> = { gl: "0.0.0", vulkan: "1.4.0" }
|
|
47
|
+
|
|
48
|
+
const platformKey = (renderer: DesktopRenderer = "gl"): string => {
|
|
49
|
+
if (process.platform === "win32") return renderer === "vulkan" ? "windows-x64-vulkan" : "windows-x64"
|
|
50
|
+
if (renderer === "vulkan") {
|
|
51
|
+
throw new CliError(`The Vulkan desktop host is Windows-only (this is ${process.platform}). Use the default renderer.`)
|
|
52
|
+
}
|
|
53
|
+
if (process.platform === "linux") return "ubuntu-linux-x64"
|
|
54
|
+
if (process.platform === "darwin") return process.arch === "arm64" ? "macos-arm64" : "macos-x64"
|
|
55
|
+
throw new CliError(`The LeCodes Desktop host isn't available on ${process.platform}.`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const isMac = () => process.platform === "darwin"
|
|
59
|
+
|
|
60
|
+
/** The host binary's file name. macOS: the executable inside LeCodes Desktop.app. */
|
|
61
|
+
const exeName = () => (process.platform === "win32" ? "lecodes-desktop.exe" : isMac() ? "lecodes-app" : "lecodes-desktop")
|
|
62
|
+
|
|
63
|
+
/** Recursively locate the host binary inside an extracted archive (the zip nests a
|
|
64
|
+
* `lecodes-desktop/` top-level folder; the tarballs may differ — search, don't assume). */
|
|
65
|
+
const findExe = (dir: string): string | null => {
|
|
66
|
+
const name = exeName()
|
|
67
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
68
|
+
const p = join(dir, entry.name)
|
|
69
|
+
if (entry.isFile() && entry.name === name) return p
|
|
70
|
+
if (entry.isDirectory()) { const found = findExe(p); if (found) return found }
|
|
71
|
+
}
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const cacheRoot = () => join(homedir(), ".lecodes", "renderer")
|
|
76
|
+
const currentFile = () => join(cacheRoot(), "current.json")
|
|
77
|
+
|
|
78
|
+
/** Versions with a working host binary in the cache, newest first. */
|
|
79
|
+
export const installedDesktopVersions = (): string[] => {
|
|
80
|
+
try {
|
|
81
|
+
return readdirSync(cacheRoot(), { withFileTypes: true })
|
|
82
|
+
.filter((e) => e.isDirectory() && e.name.startsWith("desktop-"))
|
|
83
|
+
.map((e) => e.name.slice("desktop-".length))
|
|
84
|
+
.filter((v) => findExe(join(cacheRoot(), `desktop-${v}`)) !== null)
|
|
85
|
+
.sort(semverNewer)
|
|
86
|
+
} catch {
|
|
87
|
+
return []
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The version `--desktop` renders with: the `lecodes desktop update` selection when it's ahead of
|
|
92
|
+
* the CLI's pin, the pin otherwise (a CLI upgrade that raises the contract wins over a stale save). */
|
|
93
|
+
export const selectedDesktopVersion = (): string => {
|
|
94
|
+
try {
|
|
95
|
+
const saved = (JSON.parse(readFileSync(currentFile(), "utf8")) as { version?: string }).version
|
|
96
|
+
if (saved && /^\d+\.\d+\.\d+/.test(saved) && semverNewer(DESKTOP_RENDERER_VERSION, saved) > 0) return saved
|
|
97
|
+
} catch { /* no saved selection */ }
|
|
98
|
+
return DESKTOP_RENDERER_VERSION
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The desktop product from the release manifest (always fetched fresh — see releases.ts). */
|
|
102
|
+
export const fetchDesktopManifest = (): Promise<{ latest: string, versions: ManifestVersion[] }> =>
|
|
103
|
+
fetchReleaseProduct("desktop", "desktop")
|
|
104
|
+
|
|
105
|
+
/** The version a Mac actually gets for a wanted one: the same version when it has a macOS
|
|
106
|
+
* artifact, else the newest published one that does (never below the CLI's pin — the render
|
|
107
|
+
* contract) — null when nothing qualifies. Pure; see the platform note in the header. */
|
|
108
|
+
export const pickMacVersion = (versions: ManifestVersion[], wanted: string, key = "macos-arm64"): string | null => {
|
|
109
|
+
if (versions.find((v) => v.version === wanted)?.files[key]) return wanted
|
|
110
|
+
const eligible = versions
|
|
111
|
+
.filter((v) => v.files[key] && semverNewer(DESKTOP_RENDERER_VERSION, v.version) >= 0)
|
|
112
|
+
.map((v) => v.version)
|
|
113
|
+
.sort(semverNewer)
|
|
114
|
+
return eligible[0] ?? null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Unpack a DMG: mount read-only, copy the .app out, unmount — the copy is a plain folder in the
|
|
118
|
+
* cache with no quarantine flag (it never went through a browser), like every other host. */
|
|
119
|
+
const extractDmg = (archive: string, stage: string) => {
|
|
120
|
+
const mount = join(stage, "mnt")
|
|
121
|
+
mkdirSync(mount, { recursive: true })
|
|
122
|
+
execFileSync("hdiutil", ["attach", "-nobrowse", "-readonly", "-quiet", "-mountpoint", mount, archive])
|
|
123
|
+
try {
|
|
124
|
+
const app = readdirSync(mount).find((n) => n.endsWith(".app"))
|
|
125
|
+
if (!app) throw new CliError("The downloaded DMG holds no .app bundle.")
|
|
126
|
+
execFileSync("cp", ["-R", join(mount, app), join(stage, app)])
|
|
127
|
+
} finally {
|
|
128
|
+
spawnSync("hdiutil", ["detach", "-quiet", "-force", mount])
|
|
129
|
+
rmSync(mount, { recursive: true, force: true })
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const downloadDesktop = async (version: string, dest: string, renderer: DesktopRenderer = "gl"): Promise<void> => {
|
|
134
|
+
const key = platformKey(renderer)
|
|
135
|
+
const entry = (await fetchDesktopManifest()).versions.find((v) => v.version === version)
|
|
136
|
+
const file = entry?.files[key]
|
|
137
|
+
if (!file) {
|
|
138
|
+
// Never silently fall back to the GL host: a project asking for Vulkan and quietly getting
|
|
139
|
+
// OpenGL makes "why is it slow / why does the shader look wrong" unanswerable.
|
|
140
|
+
if (renderer === "vulkan") {
|
|
141
|
+
throw new CliError(
|
|
142
|
+
`The Vulkan desktop host isn't published for LeCodes Desktop ${version} [${key}].\n` +
|
|
143
|
+
` Vulkan needs desktop >= ${DESKTOP_RENDERER_MIN_VERSION.vulkan}; run \`lecodes desktop update\`, ` +
|
|
144
|
+
`set LECODES_DESKTOP_EXE to a locally built host, or drop desktop.renderer from app.json.`,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
throw new CliError(
|
|
148
|
+
`LeCodes Desktop ${version} [${key}] isn't published yet — \`--desktop\` needs it for the ` +
|
|
149
|
+
`headless render contract.\nSet LECODES_DESKTOP_EXE to a locally built host, or update lecodes-cli.`,
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
note(`Downloading LeCodes Desktop ${version} [${key}] (${(file.size / 1024 / 1024).toFixed(1)} MB)…`)
|
|
154
|
+
await downloadReleaseArchive(file, {
|
|
155
|
+
cacheRoot: cacheRoot(),
|
|
156
|
+
dest,
|
|
157
|
+
validate: (dir) => (findExe(dir) ? null : "The downloaded archive doesn't contain the desktop host binary."),
|
|
158
|
+
extract: file.url.toLowerCase().endsWith(".dmg") ? extractDmg : undefined,
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** A macOS host installed by hand (/Applications or ~/Applications) — the OFFLINE fallback when
|
|
163
|
+
* nothing is cached and the manifest can't be fetched; the cache is preferred because its
|
|
164
|
+
* version is known and chosen by the CLI, an installed app is whatever the user last dragged in. */
|
|
165
|
+
export const findMacHostExe = (): string | null => {
|
|
166
|
+
const candidates = [
|
|
167
|
+
...["/Applications", join(homedir(), "Applications")]
|
|
168
|
+
.flatMap((root) => ["LeCodes Desktop.app", "LeCodes.app"].map((app) => join(root, app))),
|
|
169
|
+
].map((app) => join(app, "Contents", "MacOS", "lecodes-app"))
|
|
170
|
+
return candidates.find((exe) => existsSync(exe)) ?? null
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** macOS: the cached host for the selected version, else the newest published version with a
|
|
174
|
+
* macOS artifact (downloaded + remembered as the selection, so later runs are offline-stable),
|
|
175
|
+
* else the installed app. */
|
|
176
|
+
const resolveMacExe = async (selected: string): Promise<string> => {
|
|
177
|
+
const cached = findExe(join(cacheRoot(), `desktop-${selected}`))
|
|
178
|
+
if (cached) return cached
|
|
179
|
+
try {
|
|
180
|
+
const { versions } = await fetchDesktopManifest()
|
|
181
|
+
const version = pickMacVersion(versions, selected, platformKey())
|
|
182
|
+
if (!version) {
|
|
183
|
+
throw new CliError(`No LeCodes Desktop release for ${platformKey()} is published at or above ${DESKTOP_RENDERER_VERSION}.`)
|
|
184
|
+
}
|
|
185
|
+
const dir = join(cacheRoot(), `desktop-${version}`)
|
|
186
|
+
let exe = findExe(dir)
|
|
187
|
+
if (!exe) {
|
|
188
|
+
await downloadDesktop(version, dir)
|
|
189
|
+
exe = findExe(dir)
|
|
190
|
+
if (!exe) throw new CliError(`Renderer cache at ${dir} is broken — delete the folder and retry.`)
|
|
191
|
+
}
|
|
192
|
+
if (version !== selected) {
|
|
193
|
+
note(`LeCodes Desktop ${selected} has no macOS build — using ${version}.`)
|
|
194
|
+
mkdirSync(cacheRoot(), { recursive: true })
|
|
195
|
+
writeFileSync(currentFile(), JSON.stringify({ version }, null, 2))
|
|
196
|
+
}
|
|
197
|
+
return exe
|
|
198
|
+
} catch (e) {
|
|
199
|
+
const installed = findMacHostExe()
|
|
200
|
+
if (installed) {
|
|
201
|
+
warnErr(`${e instanceof Error ? e.message : String(e)}\n Falling back to the installed host: ${installed}`)
|
|
202
|
+
return installed
|
|
203
|
+
}
|
|
204
|
+
throw new CliError(
|
|
205
|
+
`${e instanceof Error ? e.message : String(e)}\n` +
|
|
206
|
+
" 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 (…/LeCodes Desktop.app/Contents/MacOS/lecodes-app).",
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** The desktop host binary: LECODES_DESKTOP_EXE override → the selected version's cache → download.
|
|
213
|
+
* The two renderers cache side by side (`desktop-<version>` / `desktop-<version>-vulkan`) so
|
|
214
|
+
* switching costs nothing after the first download. LECODES_DESKTOP_EXE wins over the renderer —
|
|
215
|
+
* it names an exact binary, and which backend that binary uses is its own business. */
|
|
216
|
+
export const resolveDesktopExe = async (renderer: DesktopRenderer = "gl"): Promise<string> => {
|
|
217
|
+
const override = process.env.LECODES_DESKTOP_EXE
|
|
218
|
+
if (override) {
|
|
219
|
+
if (!existsSync(override)) throw new CliError(`LECODES_DESKTOP_EXE points at "${override}" which doesn't exist.`)
|
|
220
|
+
return resolve(override)
|
|
221
|
+
}
|
|
222
|
+
if (renderer === "vulkan") platformKey(renderer) // fail early + clearly on a non-Windows machine
|
|
223
|
+
const version = selectedDesktopVersion()
|
|
224
|
+
// macOS: the same cache, fed from the DMG the Apple repo publishes; the version may differ from
|
|
225
|
+
// the selection (gaps in the macOS line) and an installed app is the offline fallback.
|
|
226
|
+
if (isMac()) return resolveMacExe(version)
|
|
227
|
+
const dir = join(cacheRoot(), `desktop-${version}${renderer === "vulkan" ? "-vulkan" : ""}`)
|
|
228
|
+
let exe = existsSync(dir) ? findExe(dir) : null
|
|
229
|
+
if (!exe) {
|
|
230
|
+
await downloadDesktop(version, dir, renderer)
|
|
231
|
+
exe = findExe(dir)
|
|
232
|
+
if (!exe) throw new CliError(`Renderer cache at ${dir} is broken — delete the folder and retry.`)
|
|
233
|
+
}
|
|
234
|
+
return exe
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** `lecodes desktop update`: move the render host to the newest published release (never below the
|
|
238
|
+
* CLI's pin — a manifest that somehow regressed under it is an error, not a downgrade). Saves the
|
|
239
|
+
* selection so later `render --desktop` runs use it; the download is skipped when already cached. */
|
|
240
|
+
export const updateDesktop = async (): Promise<void> => {
|
|
241
|
+
const manifest = await fetchDesktopManifest()
|
|
242
|
+
// macOS: "latest" is the latest WITH a macOS artifact — a Windows-only release isn't an update here.
|
|
243
|
+
const latest = isMac() ? pickMacVersion(manifest.versions, manifest.latest, platformKey()) : manifest.latest
|
|
244
|
+
if (!latest) throw new CliError(`No LeCodes Desktop release for ${platformKey()} is published at or above ${DESKTOP_RENDERER_VERSION}.`)
|
|
245
|
+
if (semverNewer(latest, DESKTOP_RENDERER_VERSION) > 0) {
|
|
246
|
+
throw new CliError(
|
|
247
|
+
`The newest published LeCodes Desktop is ${latest}, but this CLI needs ≥ ${DESKTOP_RENDERER_VERSION} ` +
|
|
248
|
+
`for the render contract. Publish the desktop release first.`,
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
const before = selectedDesktopVersion()
|
|
252
|
+
const dir = join(cacheRoot(), `desktop-${latest}`)
|
|
253
|
+
// "Up to date" means the cache holds the artifact the manifest CURRENTLY names, not merely a dir
|
|
254
|
+
// with the right version in its name: a release can be republished under the same number (the
|
|
255
|
+
// macOS host was, as 1.3.2), and a stale cache then silently outlives it. Comparing the recorded
|
|
256
|
+
// sha256 catches that; a dir with no stamp predates the check, so its contents are unknown and it
|
|
257
|
+
// is replaced too. Only `update` pays for this — it already has the manifest in hand.
|
|
258
|
+
const published = (await fetchDesktopManifest()).versions.find((v) => v.version === latest)?.files[platformKey()]
|
|
259
|
+
const cached = existsSync(dir) && findExe(dir) !== null
|
|
260
|
+
const republished = cached && published !== undefined && cachedArtifactSha(dir) !== published.sha256
|
|
261
|
+
if (republished) {
|
|
262
|
+
note(`LeCodes Desktop ${latest} was republished since this machine cached it — downloading the current build.`)
|
|
263
|
+
rmSync(dir, { recursive: true, force: true })
|
|
264
|
+
}
|
|
265
|
+
const hadIt = cached && !republished
|
|
266
|
+
if (!hadIt) await downloadDesktop(latest, dir)
|
|
267
|
+
mkdirSync(cacheRoot(), { recursive: true })
|
|
268
|
+
writeFileSync(currentFile(), JSON.stringify({ version: latest }, null, 2))
|
|
269
|
+
if (latest === before && hadIt) note(`Already up to date: LeCodes Desktop ${latest}.`)
|
|
270
|
+
else if (latest === before) note(`Re-downloaded LeCodes Desktop ${latest}.`)
|
|
271
|
+
else note(`Desktop renderer set to ${latest} (was ${before}).`)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Window config forwarded to the host as CREATOR_* envs (unknown envs are ignored by older
|
|
275
|
+
* hosts, so none of these bump the version pin — they just don't apply there). Sizes are
|
|
276
|
+
* LOGICAL units; the host multiplies by the display scale. */
|
|
277
|
+
export type DesktopWindowOpts = {
|
|
278
|
+
/** Which host binary to run (see DesktopRenderer). Must match the renderer the bundle's shaders
|
|
279
|
+
* were staged for — resolve it once per command and pass the same value to both. */
|
|
280
|
+
renderer?: DesktopRenderer
|
|
281
|
+
width?: number
|
|
282
|
+
height?: number
|
|
283
|
+
minWidth?: number
|
|
284
|
+
minHeight?: number
|
|
285
|
+
resizable?: boolean
|
|
286
|
+
title?: string
|
|
287
|
+
/** Borderless fullscreen on the primary monitor (host ≥1.3.0; older hosts ignore the env). */
|
|
288
|
+
fullscreen?: boolean
|
|
289
|
+
/** Display scale: a fixed density (0.5–4) or "fit" (derive from design frame vs monitor/work
|
|
290
|
+
* area; host ≥1.3.0 — a 1.2.0 host warns and ignores "fit"). */
|
|
291
|
+
scale?: number | "fit"
|
|
292
|
+
/** 3D buffer scale vs the window, 0.25–1 (host ≥1.4.0); the UI stays at native pixels. */
|
|
293
|
+
renderScale?: number
|
|
294
|
+
/** 3D buffer cap [W, H] in px (host ≥1.4.0) — e.g. [1920, 1080] keeps a 4K fullscreen at 1080p 3D. */
|
|
295
|
+
maxRenderSize?: [number, number]
|
|
296
|
+
/** Draw the host's frame counter in the corner (host ≥1.5.0; older hosts ignore the env). Costs
|
|
297
|
+
* nothing to leave off and next to nothing on — the digits are rectangles, no UI behind them. */
|
|
298
|
+
fps?: boolean
|
|
299
|
+
/** Multiplayer launch facts for the host (`--server --port N --max-clients N` / `--connect ip:port`);
|
|
300
|
+
* the SDK reads them through `Net.launch` and decides (docs/multiplayer-plan.md). */
|
|
301
|
+
net?: { role: "server" | "client"; address?: string; port?: number; maxClients?: number }
|
|
302
|
+
/** Dirs the host scans for native plugin libraries (CREATOR_PLUGIN_DIRS; the loader was added
|
|
303
|
+
* after host 1.4.0 — an older host ignores the env). See appShared.desktopPluginLibraries. */
|
|
304
|
+
pluginDirs?: string[]
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** CREATOR_PLUGIN_DIRS: the project's plugin dirs first, then whatever the environment already
|
|
308
|
+
* carried (a `.env` CREATOR_PLUGIN_DIRS keeps working alongside the project's `plugins/`). */
|
|
309
|
+
const pluginEnv = (dirs: string[] | undefined): Record<string, string> => {
|
|
310
|
+
const all = [...(dirs ?? []), ...(process.env.CREATOR_PLUGIN_DIRS ? [process.env.CREATOR_PLUGIN_DIRS] : [])]
|
|
311
|
+
return all.length > 0 ? { CREATOR_PLUGIN_DIRS: all.join(delimiter) } : {}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const windowEnv = (opts: DesktopWindowOpts): Record<string, string> => {
|
|
315
|
+
const env: Record<string, string> = { ...pluginEnv(opts.pluginDirs) }
|
|
316
|
+
// A lone width/height fills the other side from the host default so it isn't silently ignored.
|
|
317
|
+
const width = opts.width ?? (opts.height !== undefined ? 1280 : undefined)
|
|
318
|
+
const height = opts.height ?? (opts.width !== undefined ? 720 : undefined)
|
|
319
|
+
if (width && height) env.CREATOR_WINDOW_SIZE = `${width}x${height}`
|
|
320
|
+
if (opts.minWidth && opts.minHeight) env.CREATOR_MIN_WINDOW_SIZE = `${opts.minWidth}x${opts.minHeight}`
|
|
321
|
+
if (opts.title) env.CREATOR_WINDOW_TITLE = opts.title
|
|
322
|
+
if (opts.resizable === false) env.CREATOR_WINDOW_FIXED = "1"
|
|
323
|
+
if (opts.fullscreen !== undefined) env.CREATOR_WINDOW_FULLSCREEN = opts.fullscreen ? "1" : "0"
|
|
324
|
+
if (opts.scale === "fit") env.CREATOR_UI_SCALE = "fit"
|
|
325
|
+
else if (typeof opts.scale === "number") env.CREATOR_UI_SCALE = String(opts.scale)
|
|
326
|
+
if (typeof opts.renderScale === "number") env.CREATOR_RENDER_SCALE = String(opts.renderScale)
|
|
327
|
+
if (opts.maxRenderSize && opts.maxRenderSize[0] > 0 && opts.maxRenderSize[1] > 0) {
|
|
328
|
+
env.CREATOR_RENDER_MAX = `${Math.round(opts.maxRenderSize[0])}x${Math.round(opts.maxRenderSize[1])}`
|
|
329
|
+
}
|
|
330
|
+
if (opts.fps) env.CREATOR_FPS = "1"
|
|
331
|
+
return env
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// The host's own multiplayer flags (main.cpp argv) — options, not env, so a shipped exe takes them too.
|
|
335
|
+
const netArgs = (net: DesktopWindowOpts["net"]): string[] => {
|
|
336
|
+
if (!net) return []
|
|
337
|
+
const args: string[] = []
|
|
338
|
+
if (net.role === "server") args.push("--server")
|
|
339
|
+
else if (net.address) args.push("--connect", net.address)
|
|
340
|
+
if (net.port) args.push("--port", String(net.port))
|
|
341
|
+
if (net.maxClients) args.push("--max-clients", String(net.maxClients))
|
|
342
|
+
return args
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export type DesktopRenderOptions = {
|
|
346
|
+
/** Called per output chunk; returning true ends the run early (the host is killed, the
|
|
347
|
+
* outcome is a success with the output so far — a bake that printed its done marker). */
|
|
348
|
+
stopOnLine?: (chunk: string) => boolean
|
|
349
|
+
/** Which host binary to run (see DesktopRenderer). Must match the renderer the bundle's shaders
|
|
350
|
+
* were staged for — resolve it once per command and pass the same value to both. */
|
|
351
|
+
renderer?: DesktopRenderer
|
|
352
|
+
width: number
|
|
353
|
+
height: number
|
|
354
|
+
/** Frames to run before the dump — headroom for async loads (GLB, images, fonts). */
|
|
355
|
+
frames: number
|
|
356
|
+
/** Absolute or cwd-relative path the PNG lands at. */
|
|
357
|
+
outPng: string
|
|
358
|
+
/** Forward the host's stdout/stderr (app console.log included) to stderr. */
|
|
359
|
+
logs: boolean
|
|
360
|
+
timeoutMs: number
|
|
361
|
+
/** CREATOR_SCRIPT contents (the line format in packages/desktop/src/main.cpp `scriptStep`): the
|
|
362
|
+
* host drives these commands and self-terminates when done; `frames` is then only a safety cap. */
|
|
363
|
+
script?: string
|
|
364
|
+
/** CREATOR_FIXED_DT: virtual app clock, ms per frame (0/undefined = wall clock). */
|
|
365
|
+
fixedDtMs?: number
|
|
366
|
+
/** Inspection camera: CREATOR_CAMERA ("ex,ey,ez;tx,ty,tz[;fov]") or CREATOR_VIEW ("iso[;fov]"). */
|
|
367
|
+
camera?: string
|
|
368
|
+
view?: string
|
|
369
|
+
/** Dirs the host scans for native plugin libraries (CREATOR_PLUGIN_DIRS). */
|
|
370
|
+
pluginDirs?: string[]
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export type DesktopRenderOutcome = {
|
|
374
|
+
/** Everything the host printed (stdout + stderr), for marker parsing / expect.log. */
|
|
375
|
+
output: string
|
|
376
|
+
exitCode: number | null
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Stage a compiled bundle for the host: app.js + the local-asset resources copied next to it
|
|
380
|
+
* (the host's fetchLocal resolves them by basename relative to its cwd). Caller removes the dir. */
|
|
381
|
+
const stageBundle = (js: string, resources: { name: string, absPath?: string }[]): string => {
|
|
382
|
+
const runDir = mkdtempSync(join(tmpdir(), "lecodes-render-"))
|
|
383
|
+
writeFileSync(join(runDir, "app.js"), js)
|
|
384
|
+
for (const r of resources) {
|
|
385
|
+
if (r.absPath && existsSync(r.absPath) && statSync(r.absPath).isFile()) {
|
|
386
|
+
copyFileSync(r.absPath, join(runDir, r.name))
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return runDir
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** Forward a child stream line-by-line through `map`. Line-buffered so a chunk boundary can never
|
|
393
|
+
* cut a `module.js:L:C` frame in half; the trailing partial line is flushed when the stream ends. */
|
|
394
|
+
const pipeThroughDemangler = (src: NodeJS.ReadableStream, dst: NodeJS.WritableStream, map: (s: string) => string) => {
|
|
395
|
+
let rest = ""
|
|
396
|
+
src.on("data", (d: Buffer | string) => {
|
|
397
|
+
rest += String(d)
|
|
398
|
+
const nl = rest.lastIndexOf("\n")
|
|
399
|
+
if (nl < 0) return
|
|
400
|
+
dst.write(map(rest.slice(0, nl + 1)))
|
|
401
|
+
rest = rest.slice(nl + 1)
|
|
402
|
+
})
|
|
403
|
+
src.on("end", () => { if (rest) dst.write(map(rest)) })
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** `lecodes desktop run`: open the compiled project in a normal (visible, interactive) desktop
|
|
407
|
+
* window and stream the host's output — the plain-run counterpart of `lecodes dev`, no reload.
|
|
408
|
+
* Resolves until the window is closed. Ambient CREATOR_* envs pass through (handy for testing). */
|
|
409
|
+
export const runDesktopApp = async (
|
|
410
|
+
js: string,
|
|
411
|
+
resources: { name: string, absPath?: string }[],
|
|
412
|
+
opts: DesktopWindowOpts = {},
|
|
413
|
+
): Promise<void> => {
|
|
414
|
+
const exe = await resolveDesktopExe(opts.renderer)
|
|
415
|
+
const runDir = stageBundle(js, resources)
|
|
416
|
+
try {
|
|
417
|
+
const env = { ...process.env, ...windowEnv(opts) }
|
|
418
|
+
const demangle = makeStackDemangler(js)
|
|
419
|
+
const argv = [join(runDir, "app.js"), ...netArgs(opts.net)]
|
|
420
|
+
await new Promise<void>((done, fail) => {
|
|
421
|
+
// Piped (not inherited) only when there's a map to apply — the host's frames name the minified
|
|
422
|
+
// bundle (`module.js:5:51631`) and we rewrite them to project positions on the way through.
|
|
423
|
+
const child = demangle
|
|
424
|
+
? spawn(exe, argv, { cwd: runDir, env, stdio: ["ignore", "pipe", "pipe"] })
|
|
425
|
+
: spawn(exe, argv, { cwd: runDir, env, stdio: ["ignore", "inherit", "inherit"] })
|
|
426
|
+
if (demangle) {
|
|
427
|
+
pipeThroughDemangler(child.stdout!, process.stdout, demangle)
|
|
428
|
+
pipeThroughDemangler(child.stderr!, process.stderr, demangle)
|
|
429
|
+
}
|
|
430
|
+
child.on("error", (e) => fail(new CliError(`Can't run the desktop host: ${e.message}`)))
|
|
431
|
+
child.on("exit", (code) => {
|
|
432
|
+
if (code === 0 || code === null) done() // null: killed (e.g. Ctrl+C reached the child)
|
|
433
|
+
else fail(new CliError(`The desktop host exited with code ${code}.`))
|
|
434
|
+
})
|
|
435
|
+
})
|
|
436
|
+
} finally {
|
|
437
|
+
// A host that was just killed (timeout / stopOnLine) can still hold the staging dir for a moment;
|
|
438
|
+
// an EPERM here must not replace the real error with "Permission denied" (seen on a 10-minute
|
|
439
|
+
// lightmap timeout, read as a crash for two hours). Retry briefly, then leave it to the OS temp.
|
|
440
|
+
for (let attempt = 0; ; attempt++) {
|
|
441
|
+
try { rmSync(runDir, { recursive: true, force: true }); break }
|
|
442
|
+
catch {
|
|
443
|
+
if (attempt >= 5) break
|
|
444
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** `lecodes dev --desktop`: open the dev-server URL in the host — its launcher fetches the
|
|
451
|
+
* bundle and the appended prelude hot-reloads over websocket. No staging dir: assets stream
|
|
452
|
+
* from the dev server. Resolves when the window closes (any exit code — a dev host getting
|
|
453
|
+
* killed mid-session is not an error); the caller's dev server keeps running. stdout is
|
|
454
|
+
* dropped: the prelude already mirrors app console output to the dev terminal. */
|
|
455
|
+
export const runDesktopDev = async (url: string, opts: DesktopWindowOpts = {}): Promise<void> => {
|
|
456
|
+
const exe = await resolveDesktopExe(opts.renderer)
|
|
457
|
+
const env = { ...process.env, ...windowEnv(opts) }
|
|
458
|
+
await new Promise<void>((done, fail) => {
|
|
459
|
+
const child = spawn(exe, [url], { env, stdio: ["ignore", "ignore", "inherit"] })
|
|
460
|
+
child.on("error", (e) => fail(new CliError(`Can't run the desktop host: ${e.message}`)))
|
|
461
|
+
child.on("exit", () => done())
|
|
462
|
+
})
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** Run a compiled bundle in the hidden desktop host and dump the final frame as a PNG. */
|
|
466
|
+
export const runDesktopRender = async (
|
|
467
|
+
js: string,
|
|
468
|
+
resources: { name: string, absPath?: string }[],
|
|
469
|
+
opts: DesktopRenderOptions,
|
|
470
|
+
): Promise<DesktopRenderOutcome> => {
|
|
471
|
+
if (isMac()) {
|
|
472
|
+
throw new CliError(
|
|
473
|
+
"`lecodes render --desktop` isn't available on macOS yet — the macOS host has no headless mode " +
|
|
474
|
+
"(CREATOR_HIDDEN / CREATOR_DUMP_PNG). `lecodes desktop run` and `lecodes desktop build` do work there.",
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
const exe = await resolveDesktopExe(opts.renderer)
|
|
478
|
+
const runDir = stageBundle(js, resources)
|
|
479
|
+
try {
|
|
480
|
+
const appJs = join(runDir, "app.js")
|
|
481
|
+
const outAbs = resolve(opts.outPng)
|
|
482
|
+
const env: Record<string, string | undefined> = {
|
|
483
|
+
...process.env,
|
|
484
|
+
CREATOR_DUMP_PNG: outAbs,
|
|
485
|
+
CREATOR_MAX_FRAMES: String(opts.frames),
|
|
486
|
+
CREATOR_WINDOW_SIZE: `${opts.width}x${opts.height}`,
|
|
487
|
+
CREATOR_HIDDEN: "1",
|
|
488
|
+
}
|
|
489
|
+
if (opts.script !== undefined) {
|
|
490
|
+
const scriptPath = join(runDir, "script.txt")
|
|
491
|
+
writeFileSync(scriptPath, opts.script)
|
|
492
|
+
env.CREATOR_SCRIPT = scriptPath
|
|
493
|
+
}
|
|
494
|
+
if (opts.fixedDtMs) env.CREATOR_FIXED_DT = String(opts.fixedDtMs)
|
|
495
|
+
if (opts.camera) env.CREATOR_CAMERA = opts.camera
|
|
496
|
+
if (opts.view) env.CREATOR_VIEW = opts.view
|
|
497
|
+
Object.assign(env, pluginEnv(opts.pluginDirs))
|
|
498
|
+
// Frames the host prints name the minified bundle (`module.js:5:51631`); map them back to
|
|
499
|
+
// project positions. Chunks stay raw so `[script]` marker parsing sees exactly what was sent —
|
|
500
|
+
// the rewrite happens on the joined text (and best-effort per chunk for live --logs).
|
|
501
|
+
const demangle = makeStackDemangler(js) ?? ((text: string) => text)
|
|
502
|
+
return await new Promise<DesktopRenderOutcome>((done, fail) => {
|
|
503
|
+
const child = spawn(exe, [appJs], { cwd: runDir, env, stdio: ["ignore", "pipe", "pipe"] })
|
|
504
|
+
// Keep the whole output (scripted runs parse its `[script]` markers + console lines); forward
|
|
505
|
+
// live only with --logs. The error tail is the last 80 chunks.
|
|
506
|
+
const chunks: string[] = []
|
|
507
|
+
let stopped = false
|
|
508
|
+
const sink = (d: unknown) => {
|
|
509
|
+
const text = String(d)
|
|
510
|
+
chunks.push(text)
|
|
511
|
+
if (opts.logs) process.stderr.write(demangle(text))
|
|
512
|
+
if (!stopped && opts.stopOnLine?.(text)) { stopped = true; child.kill() }
|
|
513
|
+
}
|
|
514
|
+
child.stdout.on("data", sink)
|
|
515
|
+
child.stderr.on("data", sink)
|
|
516
|
+
const killer = setTimeout(() => {
|
|
517
|
+
child.kill()
|
|
518
|
+
fail(new CliError(`The native render timed out after ${opts.timeoutMs}ms (raise --timeout, or lower --frames).`))
|
|
519
|
+
}, opts.timeoutMs)
|
|
520
|
+
child.on("error", (e) => { clearTimeout(killer); fail(new CliError(`Can't run the desktop renderer: ${e.message}`)) })
|
|
521
|
+
child.on("exit", (code) => {
|
|
522
|
+
clearTimeout(killer)
|
|
523
|
+
const output = demangle(chunks.join(""))
|
|
524
|
+
if (stopped) return done({ output, exitCode: 0 })
|
|
525
|
+
if (code === 0 && existsSync(outAbs)) return done({ output, exitCode: code })
|
|
526
|
+
const tail = demangle(chunks.slice(-80).join("")).trimEnd()
|
|
527
|
+
const detail = tail.length > 0 ? `\n${tail}` : ""
|
|
528
|
+
fail(new CliError(code === 0
|
|
529
|
+
? `The renderer exited cleanly but produced no PNG — did the app open a screen?${detail}`
|
|
530
|
+
: `The renderer exited with code ${code}.${detail}`))
|
|
531
|
+
})
|
|
532
|
+
})
|
|
533
|
+
} finally {
|
|
534
|
+
// A host that was just killed (timeout / stopOnLine) can still hold the staging dir for a moment;
|
|
535
|
+
// an EPERM here must not replace the real error with "Permission denied" (seen on a 10-minute
|
|
536
|
+
// lightmap timeout, read as a crash for two hours). Retry briefly, then leave it to the OS temp.
|
|
537
|
+
for (let attempt = 0; ; attempt++) {
|
|
538
|
+
try { rmSync(runDir, { recursive: true, force: true }); break }
|
|
539
|
+
catch {
|
|
540
|
+
if (attempt >= 5) break
|
|
541
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200)
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|