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.
Files changed (270) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +1069 -396
  3. package/package.json +14 -13
  4. package/runtime/scene-harness.json +1 -1
  5. package/runtime/sdk-types.json +1 -1
  6. package/runtime/web/assets/Roboto-Variable-DHm9jpd-.woff2 +0 -0
  7. package/runtime/web/assets/__vite-browser-external-BIHI7g3E.js +1 -0
  8. package/runtime/web/assets/basis-C64VHDVD.js +1 -0
  9. package/runtime/web/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
  10. package/runtime/web/assets/createViewerLite-BvHobVhp.js +852 -0
  11. package/runtime/web/assets/draco-BiISTFcR.js +118 -0
  12. package/runtime/web/assets/draco_decoder-DsQ12WqX.wasm +0 -0
  13. package/runtime/web/assets/index-CfNCCBjN.js +2 -0
  14. package/runtime/web/assets/mapViewImpl-B2JcES8l.js +810 -0
  15. package/runtime/web/assets/maplibre-gl-worker-CJfwIrte.js +8 -0
  16. package/runtime/web/assets/neutral_ibl128-D-lzdkLN.ktx +0 -0
  17. package/runtime/web/assets/worker-Caf-yYEI.js +2 -0
  18. package/runtime/web/embed.html +32 -0
  19. package/runtime/web/embed.js +171 -0
  20. package/src/api.ts +302 -0
  21. package/src/browserAuth.ts +87 -0
  22. package/src/cmgenTool.ts +104 -0
  23. package/src/commands/app.ts +896 -0
  24. package/src/commands/appAndroid.ts +603 -0
  25. package/src/commands/appDesktop.ts +339 -0
  26. package/src/commands/appDesktopMac.ts +470 -0
  27. package/src/commands/appIcon.ts +187 -0
  28. package/src/commands/appShared.ts +542 -0
  29. package/src/commands/appTemplates.ts +907 -0
  30. package/src/commands/appTemplatesAndroid.ts +599 -0
  31. package/src/commands/appTemplatesGradlew.ts +9 -0
  32. package/src/commands/assets.ts +28 -0
  33. package/src/commands/clone.ts +60 -0
  34. package/src/commands/compile.ts +253 -0
  35. package/src/commands/create.ts +59 -0
  36. package/src/commands/design.ts +863 -0
  37. package/src/commands/designTemplates.ts +11 -0
  38. package/src/commands/desktop.ts +127 -0
  39. package/src/commands/dev.ts +236 -0
  40. package/src/commands/diff.ts +59 -0
  41. package/src/commands/init.ts +191 -0
  42. package/src/commands/install.ts +198 -0
  43. package/src/commands/lightmap.ts +290 -0
  44. package/src/commands/link.ts +147 -0
  45. package/src/commands/login.ts +24 -0
  46. package/src/commands/navmesh.ts +225 -0
  47. package/src/commands/pn.ts +305 -0
  48. package/src/commands/projectTemplates.ts +130 -0
  49. package/src/commands/pull.ts +109 -0
  50. package/src/commands/push.ts +173 -0
  51. package/src/commands/render.ts +416 -0
  52. package/src/commands/scene.ts +148 -0
  53. package/src/commands/shaders.ts +157 -0
  54. package/src/commands/status.ts +29 -0
  55. package/src/commands/test.ts +371 -0
  56. package/src/commands/thumbs.ts +178 -0
  57. package/src/commands/types.ts +87 -0
  58. package/src/commands/update.ts +190 -0
  59. package/src/compile/assetIcons.ts +214 -0
  60. package/src/compile/collect.ts +46 -0
  61. package/src/compile/collectLocal.ts +37 -0
  62. package/src/compile/designCompile.ts +104 -0
  63. package/src/compile/fonts.ts +156 -0
  64. package/src/compile/headlessBundle.ts +129 -0
  65. package/src/compile/nativeStack.ts +21 -0
  66. package/src/compile/projectCompile.ts +109 -0
  67. package/src/compile/sceneCompile.ts +113 -0
  68. package/src/compile/screenEntry.ts +127 -0
  69. package/src/compile/shaders.ts +245 -0
  70. package/src/config.ts +42 -0
  71. package/src/designMeta.ts +35 -0
  72. package/src/desktopRenderer.ts +545 -0
  73. package/src/desktopScript.ts +276 -0
  74. package/src/dev/clientTemplates.ts +160 -0
  75. package/src/dev/devServer.ts +333 -0
  76. package/src/dev/webRunner.ts +22 -0
  77. package/src/dev/wsServer.ts +144 -0
  78. package/src/distRoot.ts +20 -0
  79. package/src/ignore.ts +163 -0
  80. package/src/index.ts +494 -0
  81. package/src/lecodes-3d-editor.d.ts +41 -0
  82. package/src/lecodes-assets.d.ts +7 -0
  83. package/src/lecodes-design.d.ts +191 -0
  84. package/src/lecodes-renderer.d.ts +131 -0
  85. package/src/localFiles.ts +144 -0
  86. package/src/manifest.ts +46 -0
  87. package/src/matcTool.ts +137 -0
  88. package/src/peers.ts +40 -0
  89. package/src/project.ts +61 -0
  90. package/src/projectEnv.ts +94 -0
  91. package/src/qrcode-terminal.d.ts +9 -0
  92. package/src/releases.ts +125 -0
  93. package/src/serverDiff.ts +78 -0
  94. package/src/textDiff.ts +103 -0
  95. package/src/types.ts +0 -0
  96. package/src/util.ts +146 -0
  97. package/runtime/sdk/animate/animate.ts +0 -238
  98. package/runtime/sdk/animate/bezier.ts +0 -138
  99. package/runtime/sdk/animate/easings.ts +0 -126
  100. package/runtime/sdk/canvas/Canvas.ts +0 -305
  101. package/runtime/sdk/compile/aspectMacro.ts +0 -86
  102. package/runtime/sdk/compile/assetIconMacro.ts +0 -384
  103. package/runtime/sdk/compile/assetMacro.ts +0 -146
  104. package/runtime/sdk/compile/assetName.ts +0 -50
  105. package/runtime/sdk/compile/bundler.ts +0 -287
  106. package/runtime/sdk/compile/compileProject.ts +0 -129
  107. package/runtime/sdk/compile/detectEntry.ts +0 -128
  108. package/runtime/sdk/compile/fontMacro.ts +0 -459
  109. package/runtime/sdk/compile/fontRegistry.ts +0 -78
  110. package/runtime/sdk/compile/header.ts +0 -67
  111. package/runtime/sdk/compile/index.ts +0 -100
  112. package/runtime/sdk/compile/libraryImports.ts +0 -52
  113. package/runtime/sdk/compile/liteMaterial.ts +0 -247
  114. package/runtime/sdk/compile/sceneEditor.ts +0 -88
  115. package/runtime/sdk/compile/serverSplit.ts +0 -233
  116. package/runtime/sdk/compile/serverTypes.ts +0 -227
  117. package/runtime/sdk/compile/sfnt.ts +0 -98
  118. package/runtime/sdk/compile/shaderSchema.ts +0 -202
  119. package/runtime/sdk/compile/shaderTargets.ts +0 -81
  120. package/runtime/sdk/compile/sourcemap.ts +0 -25
  121. package/runtime/sdk/core/Aspect.ts +0 -568
  122. package/runtime/sdk/core/InspectorUI.ts +0 -212
  123. package/runtime/sdk/core/StateMachine.ts +0 -308
  124. package/runtime/sdk/core/color.ts +0 -66
  125. package/runtime/sdk/core/compWrite.ts +0 -42
  126. package/runtime/sdk/core/events.ts +0 -38
  127. package/runtime/sdk/core/fields.ts +0 -120
  128. package/runtime/sdk/core/registry.ts +0 -23
  129. package/runtime/sdk/core/signals.ts +0 -277
  130. package/runtime/sdk/core/time.ts +0 -84
  131. package/runtime/sdk/g2/Camera2D.ts +0 -40
  132. package/runtime/sdk/g2/CharacterController2D.ts +0 -276
  133. package/runtime/sdk/g2/Node2D.ts +0 -267
  134. package/runtime/sdk/g2/OneWay2D.ts +0 -66
  135. package/runtime/sdk/g2/Physics2D.ts +0 -346
  136. package/runtime/sdk/g2/Scene2D.ts +0 -209
  137. package/runtime/sdk/g2/Shape2D.ts +0 -259
  138. package/runtime/sdk/g2/Sprite.ts +0 -89
  139. package/runtime/sdk/g2/SpriteAnimation.ts +0 -171
  140. package/runtime/sdk/g2/SpriteSheet.ts +0 -166
  141. package/runtime/sdk/g2/Texture2D.ts +0 -47
  142. package/runtime/sdk/g2/Tilemap.ts +0 -41
  143. package/runtime/sdk/g2/Tileset.ts +0 -71
  144. package/runtime/sdk/g2/Trigger2D.ts +0 -77
  145. package/runtime/sdk/g2/autotile.ts +0 -433
  146. package/runtime/sdk/g2/cells.ts +0 -91
  147. package/runtime/sdk/g2/defineScene2d.ts +0 -381
  148. package/runtime/sdk/g2/groups2d.ts +0 -106
  149. package/runtime/sdk/g2/loop.ts +0 -50
  150. package/runtime/sdk/g2/scenarios2d.ts +0 -69
  151. package/runtime/sdk/g2/touch.ts +0 -83
  152. package/runtime/sdk/gl/Camera.ts +0 -160
  153. package/runtime/sdk/gl/CameraPlace.ts +0 -52
  154. package/runtime/sdk/gl/CharacterController.ts +0 -238
  155. package/runtime/sdk/gl/Gearbox.ts +0 -212
  156. package/runtime/sdk/gl/Geometry.ts +0 -279
  157. package/runtime/sdk/gl/IK.ts +0 -193
  158. package/runtime/sdk/gl/InstancedMesh.ts +0 -132
  159. package/runtime/sdk/gl/Light.ts +0 -107
  160. package/runtime/sdk/gl/Lightmap.ts +0 -256
  161. package/runtime/sdk/gl/Material.ts +0 -260
  162. package/runtime/sdk/gl/Mesh.ts +0 -83
  163. package/runtime/sdk/gl/Model.ts +0 -124
  164. package/runtime/sdk/gl/NavAgent.ts +0 -337
  165. package/runtime/sdk/gl/NavMesh.ts +0 -397
  166. package/runtime/sdk/gl/Node.ts +0 -350
  167. package/runtime/sdk/gl/Noise.ts +0 -30
  168. package/runtime/sdk/gl/Particles.ts +0 -676
  169. package/runtime/sdk/gl/Physics.ts +0 -222
  170. package/runtime/sdk/gl/Plane.ts +0 -53
  171. package/runtime/sdk/gl/Ragdoll.ts +0 -270
  172. package/runtime/sdk/gl/Ray.ts +0 -16
  173. package/runtime/sdk/gl/Scene.ts +0 -486
  174. package/runtime/sdk/gl/Shape.ts +0 -409
  175. package/runtime/sdk/gl/Terrain.ts +0 -1092
  176. package/runtime/sdk/gl/Texture.ts +0 -63
  177. package/runtime/sdk/gl/Trigger.ts +0 -45
  178. package/runtime/sdk/gl/Vehicle.ts +0 -473
  179. package/runtime/sdk/gl/Wheel.ts +0 -240
  180. package/runtime/sdk/gl/animation/AnimationClip.ts +0 -204
  181. package/runtime/sdk/gl/animation/Animator.ts +0 -120
  182. package/runtime/sdk/gl/animation/Layer.ts +0 -31
  183. package/runtime/sdk/gl/animation/Loop.ts +0 -30
  184. package/runtime/sdk/gl/animation/Playback.ts +0 -44
  185. package/runtime/sdk/gl/animation/core.ts +0 -336
  186. package/runtime/sdk/gl/controls.ts +0 -95
  187. package/runtime/sdk/gl/physicsEvents.ts +0 -20
  188. package/runtime/sdk/gl/scenarios.ts +0 -291
  189. package/runtime/sdk/gl/state.ts +0 -6
  190. package/runtime/sdk/gl/terrainMesh.ts +0 -219
  191. package/runtime/sdk/gl/touch.ts +0 -68
  192. package/runtime/sdk/inject.ts +0 -197
  193. package/runtime/sdk/math/Mathf.ts +0 -118
  194. package/runtime/sdk/math/mat4.ts +0 -278
  195. package/runtime/sdk/math/quat.ts +0 -232
  196. package/runtime/sdk/math/vec.ts +0 -255
  197. package/runtime/sdk/net/codec.ts +0 -119
  198. package/runtime/sdk/net/core.ts +0 -384
  199. package/runtime/sdk/net/index.ts +0 -181
  200. package/runtime/sdk/net/replication.ts +0 -622
  201. package/runtime/sdk/plugins/camera.ts +0 -81
  202. package/runtime/sdk/plugins/geolocation.ts +0 -123
  203. package/runtime/sdk/plugins/oauth.ts +0 -61
  204. package/runtime/sdk/plugins/permission.ts +0 -7
  205. package/runtime/sdk/plugins/push.ts +0 -132
  206. package/runtime/sdk/plugins/qr.ts +0 -73
  207. package/runtime/sdk/plugins/service.ts +0 -47
  208. package/runtime/sdk/runtime/app.ts +0 -101
  209. package/runtime/sdk/runtime/appEvents.ts +0 -54
  210. package/runtime/sdk/runtime/channel.ts +0 -61
  211. package/runtime/sdk/runtime/clipboard.ts +0 -20
  212. package/runtime/sdk/runtime/datetime.ts +0 -329
  213. package/runtime/sdk/runtime/device.ts +0 -293
  214. package/runtime/sdk/runtime/fetch.ts +0 -77
  215. package/runtime/sdk/runtime/files.ts +0 -23
  216. package/runtime/sdk/runtime/input.ts +0 -175
  217. package/runtime/sdk/runtime/media.ts +0 -111
  218. package/runtime/sdk/runtime/misc.ts +0 -16
  219. package/runtime/sdk/runtime/net.ts +0 -36
  220. package/runtime/sdk/runtime/rpc.ts +0 -218
  221. package/runtime/sdk/runtime/service.ts +0 -83
  222. package/runtime/sdk/runtime/share.ts +0 -9
  223. package/runtime/sdk/runtime/storage.ts +0 -13
  224. package/runtime/sdk/runtime/touch.ts +0 -76
  225. package/runtime/sdk/scene/defineScene.ts +0 -1353
  226. package/runtime/sdk/scene/editorPlugins.ts +0 -110
  227. package/runtime/sdk/scene/gizmos.ts +0 -148
  228. package/runtime/sdk/scene/grammar.ts +0 -120
  229. package/runtime/sdk/scene/material.ts +0 -188
  230. package/runtime/sdk/server/auth/appConfig.ts +0 -12
  231. package/runtime/sdk/server/auth/global.ts +0 -80
  232. package/runtime/sdk/server/auth/host.ts +0 -318
  233. package/runtime/sdk/server/auth/models.ts +0 -83
  234. package/runtime/sdk/server/auth/types.ts +0 -50
  235. package/runtime/sdk/server/channel.ts +0 -56
  236. package/runtime/sdk/server/context.ts +0 -36
  237. package/runtime/sdk/server/db/defineDb.ts +0 -237
  238. package/runtime/sdk/server/db/fields.ts +0 -132
  239. package/runtime/sdk/server/db/httpTransport.ts +0 -93
  240. package/runtime/sdk/server/db/index.ts +0 -7
  241. package/runtime/sdk/server/db/marci/query.ts +0 -412
  242. package/runtime/sdk/server/db/types.ts +0 -202
  243. package/runtime/sdk/server/errors.ts +0 -12
  244. package/runtime/sdk/server/host.ts +0 -74
  245. package/runtime/sdk/server/inject.ts +0 -13
  246. package/runtime/sdk/server/runtime.ts +0 -133
  247. package/runtime/sdk/server/validate.ts +0 -87
  248. package/runtime/sdk/ui/NativeView.ts +0 -142
  249. package/runtime/sdk/ui/UI.ts +0 -39
  250. package/runtime/sdk/ui/UIBottomSheet.ts +0 -139
  251. package/runtime/sdk/ui/UIButton.ts +0 -101
  252. package/runtime/sdk/ui/UIContainer.ts +0 -60
  253. package/runtime/sdk/ui/UIImage.ts +0 -83
  254. package/runtime/sdk/ui/UIInput.ts +0 -185
  255. package/runtime/sdk/ui/UIModal.ts +0 -139
  256. package/runtime/sdk/ui/UINode.ts +0 -830
  257. package/runtime/sdk/ui/UIPager.ts +0 -362
  258. package/runtime/sdk/ui/UIPopover.ts +0 -100
  259. package/runtime/sdk/ui/UIScreen.ts +0 -123
  260. package/runtime/sdk/ui/UIScrollable.ts +0 -87
  261. package/runtime/sdk/ui/UISpacer.ts +0 -14
  262. package/runtime/sdk/ui/UITabs.ts +0 -236
  263. package/runtime/sdk/ui/UIText.ts +0 -51
  264. package/runtime/sdk/ui/UIVideo.ts +0 -88
  265. package/runtime/sdk/ui/UIVirtualizedList.ts +0 -241
  266. package/runtime/sdk/ui/UIWidget.ts +0 -127
  267. package/runtime/sdk/ui/fonts.ts +0 -13
  268. package/runtime/sdk/ui/presentable.ts +0 -117
  269. package/runtime/sdk/ui/router.ts +0 -132
  270. package/runtime/sdk/ui/theme.ts +0 -84
@@ -0,0 +1,276 @@
1
+ import { resolve } from "node:path"
2
+ import { CliError } from "./util"
3
+
4
+ /*
5
+ * `lecodes render --desktop --script`: the same JSON scenario a headless run takes, driven through
6
+ * the NATIVE desktop host. The host has no semantic UI tree, so it can't resolve names or read text —
7
+ * it takes a line-based command file (CREATOR_SCRIPT, see packages/desktop/src/main.cpp `scriptStep`)
8
+ * with COORDINATES, and reports `[script] #<line> done <op>` on stdout as each command completes.
9
+ * This module does both halves: translate the scenario into that file (refusing, with guidance, the
10
+ * steps the host can't honour) and turn the host's stdout back into per-step results — `expect.log`
11
+ * is evaluated here against the console lines seen up to each step's marker; the semantic expects
12
+ * (`screen`/`node`/`text`/`value`) are reported as skipped, never silently passed.
13
+ */
14
+
15
+ type Point = { x: number, y: number }
16
+ const isPoint = (v: unknown): v is Point =>
17
+ typeof v === "object" && v !== null && typeof (v as Point).x === "number" && typeof (v as Point).y === "number"
18
+
19
+ const FRAME_MS = 16
20
+
21
+ export type DesktopPlanStep = {
22
+ index: number
23
+ desc: string
24
+ /** Script line numbers this step emitted; the LAST one's `done` marker completes the step. */
25
+ lines: number[]
26
+ expectLog?: string
27
+ /** Expect keys the native host can't evaluate (no semantic tree) — reported as skipped. */
28
+ skippedExpect: string[]
29
+ /** Outputs the native host can't produce (`json`). */
30
+ skippedOutputs: string[]
31
+ screenshot?: string
32
+ }
33
+
34
+ export type DesktopPlan = {
35
+ /** The CREATOR_SCRIPT file contents. */
36
+ text: string
37
+ steps: DesktopPlanStep[]
38
+ /** A safety cap for CREATOR_MAX_FRAMES: the script's own time in frames plus slack. */
39
+ frameCap: number
40
+ }
41
+
42
+ const ACTIONS = ["tap", "type", "fill", "scroll", "wait", "frames", "drag", "hold", "key", "keyDown", "keyUp", "look", "mouseTo", "wheel", "stick", "gamepad", "camera", "view"] as const
43
+ const SEMANTIC_EXPECT = ["text", "notText", "node", "notNode", "screen", "value"]
44
+ const VIEW_NAMES = ["iso", "top", "front", "side", "back"]
45
+ const isVec3 = (v: unknown): v is [number, number, number] => Array.isArray(v) && v.length === 3 && v.every((n) => typeof n === "number")
46
+
47
+ const pt = (p: Point) => `(${p.x},${p.y})`
48
+
49
+ /** Translate scenario steps into the host's command file. `headroomFrames` runs first so the app
50
+ * can load (the `--frames` value); screenshots land under `outDir`. Throws a CliError for steps the
51
+ * native host can't honour (named targets, typing, scrolling, clips). */
52
+ export const translateScenario = (rawSteps: unknown, opts: { outDir: string, headroomFrames: number }): DesktopPlan => {
53
+ if (!Array.isArray(rawSteps)) throw new CliError(`"steps" must be an array of step objects.`)
54
+ const lines: string[] = []
55
+ const steps: DesktopPlanStep[] = []
56
+ let budgetMs = 0
57
+ const emit = (line: string): number => { lines.push(line); return lines.length }
58
+ emit(`# generated by lecodes render --desktop --script`)
59
+ emit(`frames ${Math.max(0, opts.headroomFrames)}`)
60
+
61
+ const coord = (at: string, key: string, v: unknown): Point => {
62
+ if (isPoint(v)) return v
63
+ if (typeof v === "string") {
64
+ throw new CliError(`${at}: "${key}": "${v}" — the native host can't resolve names; give --desktop scripts { "x", "y" } coordinates (logical px), or run the scenario headless (without --desktop).`)
65
+ }
66
+ throw new CliError(`${at}: "${key}" must be { "x", "y" }.`)
67
+ }
68
+ const num = (at: string, key: string, v: unknown, def?: number): number => {
69
+ if (v === undefined && def !== undefined) return def
70
+ if (typeof v !== "number") throw new CliError(`${at}: "${key}" must be a number.`)
71
+ return v
72
+ }
73
+
74
+ // While an inspection camera is active, the app's raycasts read ITS camera, not what the shot shows —
75
+ // a tap/drag there would silently hit the wrong thing, so pointer steps are refused until `"off"`.
76
+ let inspecting = false
77
+ rawSteps.forEach((step, i) => {
78
+ const at = `steps[${i}]`
79
+ if (typeof step !== "object" || step === null || Array.isArray(step)) throw new CliError(`${at}: must be an object.`)
80
+ const s = step as Record<string, unknown>
81
+ const actions = ACTIONS.filter((k) => s[k] !== undefined)
82
+ if (actions.length > 1) throw new CliError(`${at}: one action per step (found ${actions.join(" + ")}).`)
83
+ const plan: DesktopPlanStep = { index: i, desc: "", lines: [], skippedExpect: [], skippedOutputs: [] }
84
+ const parts: string[] = []
85
+ const action = actions[0]
86
+
87
+ if (action === "type" || action === "fill" || action === "scroll") {
88
+ throw new CliError(`${at}: "${action}" isn't available with --desktop (the native host has no semantic tree / soft keyboard) — run this scenario headless, or drive the gesture with "drag" coordinates.`)
89
+ }
90
+ if ((action === "tap" || action === "drag" || action === "hold") && inspecting) {
91
+ throw new CliError(`${at}: "${action}" while an inspection camera is active — the app's hit-testing uses its own camera, not the one in the shot. Add { "view": "off" } (or { "camera": "off" }) first.`)
92
+ }
93
+ if (action === "view") {
94
+ const v = s.view
95
+ const fov = s.fov !== undefined ? num(at, "fov", s.fov) : undefined
96
+ if (v === "off") { plan.lines.push(emit("view off")); inspecting = false; parts.push("view off") }
97
+ else if (typeof v === "string" && VIEW_NAMES.includes(v)) {
98
+ plan.lines.push(emit(`view ${v}${fov !== undefined ? ` ${fov}` : ""}`)); inspecting = true
99
+ parts.push(`view ${v}${fov !== undefined ? ` (fov ${fov})` : ""}`)
100
+ } else throw new CliError(`${at}: "view" wants one of ${VIEW_NAMES.join(", ")} or "off".`)
101
+ budgetMs += 2 * FRAME_MS
102
+ } else if (action === "camera") {
103
+ const cam = s.camera
104
+ if (cam === "off") { plan.lines.push(emit("camera off")); inspecting = false; parts.push("camera off") }
105
+ else if (typeof cam === "object" && cam !== null && isVec3((cam as any).at) && isVec3((cam as any).lookAt)) {
106
+ const { at: eye, lookAt, fov } = cam as { at: number[], lookAt: number[], fov?: unknown }
107
+ const fovN = fov !== undefined ? num(at, "camera.fov", fov) : undefined
108
+ plan.lines.push(emit(`camera ${eye.join(" ")} ${lookAt.join(" ")}${fovN !== undefined ? ` ${fovN}` : ""}`)); inspecting = true
109
+ parts.push(`camera at (${eye.join(",")}) → (${lookAt.join(",")})`)
110
+ } else throw new CliError(`${at}: "camera" wants { "at": [x,y,z], "lookAt": [x,y,z], "fov"? } or "off".`)
111
+ budgetMs += 2 * FRAME_MS
112
+ }
113
+ if (s.fov !== undefined && action !== "view") throw new CliError(`${at}: "fov" only goes with "view" (inside "camera" use camera.fov).`)
114
+ if (action === "tap") {
115
+ const p = coord(at, "tap", s.tap)
116
+ plan.lines.push(emit(`tap ${p.x} ${p.y}`)); budgetMs += 4 * FRAME_MS
117
+ parts.push(`tap ${pt(p)}`)
118
+ } else if (action === "drag") {
119
+ const a = coord(at, "drag", s.drag), b = coord(at, "to", s.to)
120
+ const ms = num(at, "ms", s.ms, 300)
121
+ plan.lines.push(emit(`drag ${a.x} ${a.y} ${b.x} ${b.y} ${Math.round(ms)}`)); budgetMs += ms + 4 * FRAME_MS
122
+ parts.push(`drag ${pt(a)} → ${pt(b)} over ${ms}ms`)
123
+ } else if (action === "hold") {
124
+ const p = coord(at, "hold", s.hold)
125
+ const ms = num(at, "ms", s.ms)
126
+ plan.lines.push(emit(`hold ${p.x} ${p.y} ${Math.round(ms)}`)); budgetMs += ms + 4 * FRAME_MS
127
+ parts.push(`hold ${pt(p)} ${ms}ms`)
128
+ } else if (action === "key") {
129
+ if (typeof s.key !== "string") throw new CliError(`${at}: "key" must be a KeyboardEvent.code string.`)
130
+ const ms = num(at, "ms", s.ms)
131
+ plan.lines.push(emit(`key ${s.key} ${Math.round(ms)}`)); budgetMs += ms + 2 * FRAME_MS
132
+ parts.push(`key ${s.key} ${ms}ms`)
133
+ } else if (action === "keyDown" || action === "keyUp") {
134
+ const code = s[action]
135
+ if (typeof code !== "string") throw new CliError(`${at}: "${action}" must be a code string.`)
136
+ plan.lines.push(emit(`${action.toLowerCase()} ${code}`)); budgetMs += 2 * FRAME_MS
137
+ parts.push(`${action} ${code}`)
138
+ } else if (action === "look") {
139
+ const v = s.look
140
+ if (!Array.isArray(v) || v.length !== 2 || !v.every((n: unknown) => typeof n === "number")) throw new CliError(`${at}: "look" must be [dx, dy] (logical px of mouse motion).`)
141
+ const ms = num(at, "ms", s.ms, FRAME_MS)
142
+ plan.lines.push(emit(`look ${v[0]} ${v[1]} ${Math.round(ms)}`)); budgetMs += ms + 2 * FRAME_MS
143
+ parts.push(`look ${v[0]},${v[1]} over ${ms}ms`)
144
+ } else if (action === "mouseTo") {
145
+ const v = s.mouseTo
146
+ if (!Array.isArray(v) || v.length !== 2 || !v.every((n: unknown) => typeof n === "number")) throw new CliError(`${at}: "mouseTo" must be [x, y] (logical px).`)
147
+ plan.lines.push(emit(`mouseto ${v[0]} ${v[1]}`)); budgetMs += FRAME_MS
148
+ parts.push(`mouseTo ${v[0]},${v[1]}`)
149
+ } else if (action === "wheel") {
150
+ const v = s.wheel
151
+ if (!Array.isArray(v) || v.length !== 2 || !v.every((n: unknown) => typeof n === "number")) throw new CliError(`${at}: "wheel" must be [dx, dy] (notches).`)
152
+ plan.lines.push(emit(`wheel ${v[0]} ${v[1]}`)); budgetMs += 2 * FRAME_MS
153
+ parts.push(`wheel ${v[0]},${v[1]}`)
154
+ } else if (action === "stick") {
155
+ const st = s.stick
156
+ if (typeof st !== "object" || st === null) throw new CliError(`${at}: "stick" must be { left?: [x, y], right?: [x, y], triggers?: [l, r] }.`)
157
+ const pad = typeof s.index === "number" ? s.index : 0
158
+ const parts2: string[] = []
159
+ for (const k of ["left", "right", "triggers"] as const) {
160
+ const v = (st as any)[k]
161
+ if (v === undefined) continue
162
+ if (!Array.isArray(v) || v.length !== 2 || !v.every((n: unknown) => typeof n === "number")) throw new CliError(`${at}: "stick.${k}" must be a pair of numbers.`)
163
+ plan.lines.push(emit(`stick ${pad} ${k} ${v[0]} ${v[1]}`)); parts2.push(`${k}=${v[0]},${v[1]}`)
164
+ }
165
+ for (const k of Object.keys(st)) if (!["left", "right", "triggers"].includes(k)) throw new CliError(`${at}: "stick" knows left, right, triggers — not "${k}".`)
166
+ budgetMs += 2 * FRAME_MS
167
+ parts.push(`stick #${pad} ${parts2.join(" ")}`)
168
+ } else if (action === "gamepad") {
169
+ if (s.gamepad !== "connect" && s.gamepad !== "disconnect") throw new CliError(`${at}: "gamepad" must be "connect" or "disconnect".`)
170
+ const pad = typeof s.index === "number" ? s.index : 0
171
+ plan.lines.push(emit(`gamepad ${pad} ${s.gamepad}`)); budgetMs += 2 * FRAME_MS
172
+ parts.push(`gamepad ${s.gamepad} #${pad}`)
173
+ } else if (action === "wait") {
174
+ const ms = num(at, "wait", s.wait)
175
+ plan.lines.push(emit(`wait ${Math.round(ms)}`)); budgetMs += ms + FRAME_MS
176
+ parts.push(`wait ${ms}ms`)
177
+ } else if (action === "frames") {
178
+ const n = num(at, "frames", s.frames)
179
+ plan.lines.push(emit(`frames ${Math.round(n)}`)); budgetMs += n * FRAME_MS + FRAME_MS
180
+ parts.push(`advance ${n} frames`)
181
+ }
182
+
183
+ if (s.expect !== undefined) {
184
+ if (typeof s.expect !== "object" || s.expect === null) throw new CliError(`${at}: "expect" must be an object.`)
185
+ const e = s.expect as Record<string, unknown>
186
+ const bits: string[] = []
187
+ for (const k of Object.keys(e)) {
188
+ if (k === "log") {
189
+ if (typeof e.log !== "string") throw new CliError(`${at}: expect.log must be a string.`)
190
+ plan.expectLog = e.log
191
+ bits.push(`log~"${e.log}"`)
192
+ } else if (SEMANTIC_EXPECT.includes(k)) {
193
+ plan.skippedExpect.push(k)
194
+ } else {
195
+ throw new CliError(`${at}: unknown expect key "${k}".`)
196
+ }
197
+ }
198
+ if (bits.length > 0) parts.push(`expect ${bits.join(" · ")}`)
199
+ }
200
+ if (s.clip !== undefined) throw new CliError(`${at}: "clip" isn't available with --desktop (the native host dumps the whole window).`)
201
+ if (s.screenshot !== undefined) {
202
+ if (typeof s.screenshot !== "string") throw new CliError(`${at}: "screenshot" must be a path.`)
203
+ plan.screenshot = resolve(opts.outDir, s.screenshot)
204
+ // The shot lands AFTER the action settles (two frames), like the headless act → capture order.
205
+ plan.lines.push(emit(`frames 2`))
206
+ plan.lines.push(emit(`shot ${plan.screenshot}`)); budgetMs += 3 * FRAME_MS
207
+ parts.push(`→ ${s.screenshot}`)
208
+ }
209
+ if (s.json !== undefined) plan.skippedOutputs.push("json")
210
+ if (plan.lines.length === 0) {
211
+ // An expect-only step still needs a marker to anchor its evaluation.
212
+ plan.lines.push(emit(`frames 1`)); budgetMs += FRAME_MS
213
+ }
214
+ plan.desc = typeof s.name === "string" ? s.name : (parts.join(", ") || "(empty step)")
215
+ steps.push(plan)
216
+ })
217
+
218
+ const frameCap = Math.max(0, opts.headroomFrames) + Math.ceil(budgetMs / FRAME_MS) + 600
219
+ return { text: lines.join("\n") + "\n", steps, frameCap }
220
+ }
221
+
222
+ export type DesktopStepResult = { desc: string, ok: boolean, error?: string, skipped: string[] }
223
+ export type DesktopRunResult = {
224
+ /** The host understood CREATOR_SCRIPT at all (an older host ignores it and just renders). */
225
+ loaded: boolean
226
+ ended: boolean
227
+ ok: boolean
228
+ steps: DesktopStepResult[]
229
+ }
230
+
231
+ /** Turn the host's stdout into per-step results. Console lines are every non-`[script]` line the
232
+ * host printed up to a step's completion marker (the core prints `console.log` as `[DEBUG] …`). */
233
+ export const evaluateDesktopRun = (plan: DesktopPlan, output: string): DesktopRunResult => {
234
+ const lines = output.split(/\r?\n/)
235
+ const doneAt = new Map<number, { at: number, error?: string }>()
236
+ const consoleLines: string[] = []
237
+ let loaded = false, ended = false
238
+ for (const line of lines) {
239
+ const m = /^\[script\] #(\d+) (done|error) (\S+)(?:: (.*))?$/.exec(line)
240
+ if (m) {
241
+ doneAt.set(Number(m[1]), { at: consoleLines.length, error: m[2] === "error" ? `${m[3]}: ${m[4] ?? "failed"}` : undefined })
242
+ continue
243
+ }
244
+ if (line.startsWith("[script] loaded")) { loaded = true; continue }
245
+ if (line === "[script] end") { ended = true; continue }
246
+ if (line.startsWith("[script]")) continue
247
+ consoleLines.push(line)
248
+ }
249
+
250
+ const steps: DesktopStepResult[] = []
251
+ let ok = true
252
+ for (const st of plan.steps) {
253
+ const skipped = [
254
+ ...st.skippedExpect.map((k) => `expect.${k} (no semantic tree on --desktop)`),
255
+ ...st.skippedOutputs.map((k) => `${k} output (no semantic tree on --desktop)`),
256
+ ]
257
+ let error: string | undefined
258
+ let cutoff = consoleLines.length
259
+ for (const ln of st.lines) {
260
+ const d = doneAt.get(ln)
261
+ if (!d) { error = error ?? "the host never completed this step (crashed, closed, or hit the frame cap before it)"; break }
262
+ if (d.error) { error = error ?? d.error; break }
263
+ cutoff = d.at
264
+ }
265
+ if (!error && st.expectLog !== undefined) {
266
+ const seen = consoleLines.slice(0, cutoff)
267
+ if (!seen.some((l) => l.includes(st.expectLog!))) {
268
+ const tail = seen.filter((l) => l.startsWith("[DEBUG]") || l.startsWith("[INFO]") || l.startsWith("[WARN]") || l.startsWith("[ERROR]")).slice(-8)
269
+ error = `expected a console line containing "${st.expectLog}" — ${tail.length === 0 ? "the app logged nothing" : `last lines: ${tail.map((l) => `"${l}"`).join(", ")}`}`
270
+ }
271
+ }
272
+ if (error) ok = false
273
+ steps.push({ desc: st.desc, ok: !error, error, skipped })
274
+ }
275
+ return { loaded, ended, ok: ok && loaded, steps }
276
+ }
@@ -0,0 +1,160 @@
1
+ /*
2
+ * The on-device dev client for `lecodes dev`, shipped FROM the CLI (updatable without app
3
+ * releases). Two pieces:
4
+ *
5
+ * - DEV_SCREEN_TS — a tiny placeholder project (globals-only, like any user project) compiled
6
+ * through the normal pipeline into `dev.js`, the URL the QR code points at. Its auto-generated
7
+ * header says `// gl: disable`, so the app launcher creates no engine for it.
8
+ *
9
+ * - DEV_PRELUDE_JS — plain JS appended VERBATIM to every bundle the dev server serves (dev.js and
10
+ * bundle.js alike). It only touches the raw `_creatorUtils` / `_creator` bridge — no SDK — so it
11
+ * can ride along any compiled project without duplicating the injected runtime. Each
12
+ * `_creatorUtils.run()` world therefore carries its own copy: the running world holds the live
13
+ * connection; when it loads the next bundle, the NEW world's copy connects and the native
14
+ * per-world socket teardown closes the old one. On eval error, run()'s rollback keeps the
15
+ * current world (and its connection) alive, so the reject callback here reports + keeps serving.
16
+ *
17
+ * Placeholders (substituted by the dev server, see renderPrelude):
18
+ * __DEV_ORIGIN__ http://<ip>:<port>
19
+ * __DEV_BASE__ /t/<token>
20
+ * __DEV_TRANSPORT__ "ws" | "poll" (poll = hosts without a websocket bridge, e.g. the desktop host)
21
+ * __DEV_V__ the bundle version this world runs (-1 for dev.js → always load latest)
22
+ */
23
+
24
+ export const DEV_SCREEN_TS = `const title = "LeCodes Dev"
25
+ const project = __DEV_NAME__
26
+
27
+ UIScreen([
28
+ UIText(title).style({ fontSize: 30, fontWeight: 700, color: "#ffffff" }),
29
+ UIText(project).style({ fontSize: 16, color: "#9fb0d0", mt: 8 }),
30
+ UIText("Connected — loading the project…").style({ fontSize: 14, color: "#6b7a99", mt: 20 }),
31
+ ]).style({ bgColor: "#11131a", justifyContent: "center", alignItems: "center", p: 40 }).open()
32
+ `
33
+
34
+ export const DEV_PRELUDE_JS = `
35
+ ;(function () {
36
+ var ORIGIN = "__DEV_ORIGIN__"
37
+ var BASE = "__DEV_BASE__"
38
+ var TRANSPORT = "__DEV_TRANSPORT__"
39
+ var V = __DEV_V__
40
+ var wsId = null
41
+ var busy = false
42
+ var queued = null
43
+
44
+ function post(obj) {
45
+ var text = JSON.stringify(obj)
46
+ if (TRANSPORT === "ws") {
47
+ if (wsId !== null) { try { _creatorUtils.websocketSend(wsId, text) } catch (e) {} }
48
+ return
49
+ }
50
+ try {
51
+ _creatorUtils.fetch(ORIGIN + BASE + "/log", { method: "POST", body: text, headers: { "content-type": "application/json" } },
52
+ function (id) { try { _creatorUtils.disposeFetch(id) } catch (e) {} }, function () {})
53
+ } catch (e) {}
54
+ }
55
+
56
+ // An Error carries its stack (JSON.stringify would send a useless "{}"); the dev server maps the
57
+ // frames back to project positions before printing.
58
+ function describe(a) {
59
+ if (typeof a === "string") return a
60
+ if (a && typeof a.stack === "string" && typeof a.message === "string") {
61
+ return a.stack ? String(a) + "\\n" + a.stack : String(a)
62
+ }
63
+ try { return JSON.stringify(a) } catch (e) { return String(a) }
64
+ }
65
+
66
+ // Mirror console output to the dev terminal ('silent' guards recursion through wrapped calls).
67
+ var silent = false
68
+ ;["log", "warn", "error"].forEach(function (level) {
69
+ var orig = console[level]
70
+ console[level] = function () {
71
+ var parts = []
72
+ for (var i = 0; i < arguments.length; i++) parts.push(describe(arguments[i]))
73
+ try { orig.apply(console, arguments) } catch (e) {}
74
+ if (!silent) { silent = true; try { post({ type: "log", level: level, text: parts.join(" ") }) } finally { silent = false } }
75
+ }
76
+ })
77
+
78
+ function report(err) { post({ type: "error", text: describe(err) }) }
79
+
80
+ function load(v) {
81
+ if (busy) { queued = v; return }
82
+ busy = true
83
+ _creatorUtils.fetch(ORIGIN + BASE + "/bundle.js?v=" + v + "&t=" + TRANSPORT, {}, function (id, status) {
84
+ if (status >= 400) { busy = false; report("bundle fetch: HTTP " + status); return }
85
+ // Same engine bringup the app launcher does for a fresh launch (see desktop initial.ts).
86
+ var head = ""
87
+ try { head = _creatorUtils.fetchSlice(id, 0, 300) } catch (e) {}
88
+ var preloads = head.match(/\\/\\/ preload: (.+)/)
89
+ if (head.indexOf("// gl:") < 0) {
90
+ try { _creator.createEngine(preloads ? preloads[1].split(";") : []) } catch (e) { report("createEngine: " + String(e)) }
91
+ }
92
+ _creatorUtils.run(id, function (err) {
93
+ // Eval failed → native rollback kept THIS world (and its connection) alive. Keep serving.
94
+ busy = false
95
+ report(err)
96
+ if (queued !== null) { var q = queued; queued = null; load(q) }
97
+ })
98
+ // On success this world is torn down next tick — the new bundle carries its own prelude.
99
+ }, function (err) { busy = false; report("bundle fetch failed: " + String(err)) })
100
+ }
101
+
102
+ function onServer(data) {
103
+ var m = null
104
+ try { m = JSON.parse(data) } catch (e) { return }
105
+ if (m && m.type === "reload" && typeof m.v === "number" && m.v !== V) load(m.v)
106
+ }
107
+
108
+ if (TRANSPORT === "ws") {
109
+ var url = ORIGIN.replace("http", "ws") + BASE + "/ws"
110
+ var open = function () {
111
+ try {
112
+ wsId = _creatorUtils.websocketOpen(url, function (channel, data) {
113
+ if (channel === "open") {
114
+ try { _creatorUtils.websocketSend(wsId, JSON.stringify({ type: "hello", v: V })) } catch (e) {}
115
+ } else if (channel === "message") {
116
+ onServer(data)
117
+ } else if (channel === "close" || channel === "error") {
118
+ wsId = null
119
+ setTimeout(open, 2000)
120
+ }
121
+ })
122
+ } catch (e) { setTimeout(open, 2000) }
123
+ }
124
+ open()
125
+ } else {
126
+ var poll = function () {
127
+ _creatorUtils.fetch(ORIGIN + BASE + "/version", {}, function (id, status) {
128
+ var v = null
129
+ try { v = _creatorUtils.fetchToJson(id).v } catch (e) {}
130
+ try { _creatorUtils.disposeFetch(id) } catch (e) {}
131
+ setTimeout(poll, 700)
132
+ if (status < 400 && typeof v === "number" && v !== V && !busy) load(v)
133
+ }, function () { setTimeout(poll, 1500) })
134
+ }
135
+ poll()
136
+ }
137
+ })();
138
+ `
139
+
140
+ export type PreludeParams = {
141
+ /** e.g. "http://192.168.1.5:4499" */
142
+ origin: string
143
+ /** e.g. "/t/a1b2c3d4" */
144
+ base: string
145
+ transport: "ws" | "poll"
146
+ /** The version baked into this bundle; -1 for dev.js (always loads the latest). */
147
+ v: number
148
+ }
149
+
150
+ /** The prelude with its placeholders substituted, ready to append to a compiled bundle. */
151
+ export const renderPrelude = (p: PreludeParams): string =>
152
+ DEV_PRELUDE_JS
153
+ .split("__DEV_ORIGIN__").join(p.origin)
154
+ .split("__DEV_BASE__").join(p.base)
155
+ .split("__DEV_TRANSPORT__").join(p.transport)
156
+ .split("__DEV_V__").join(String(p.v))
157
+
158
+ /** The dev.js placeholder-screen source with the project name baked in (as a JS string literal). */
159
+ export const renderDevScreen = (name: string): string =>
160
+ DEV_SCREEN_TS.split("__DEV_NAME__").join(JSON.stringify(name))