lecodes-cli 0.18.2 → 0.19.2

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 (224) hide show
  1. package/dist/index.js +1866 -301
  2. package/package.json +14 -12
  3. package/runtime/scene-harness.json +1 -1
  4. package/runtime/sdk-types.json +1 -1
  5. package/src/api.ts +302 -0
  6. package/src/browserAuth.ts +87 -0
  7. package/src/cmgenTool.ts +104 -0
  8. package/src/commands/app.ts +891 -0
  9. package/src/commands/appAndroid.ts +603 -0
  10. package/src/commands/appDesktop.ts +326 -0
  11. package/src/commands/appDesktopMac.ts +470 -0
  12. package/src/commands/appIcon.ts +187 -0
  13. package/src/commands/appShared.ts +448 -0
  14. package/src/commands/appTemplates.ts +883 -0
  15. package/src/commands/appTemplatesAndroid.ts +599 -0
  16. package/src/commands/appTemplatesGradlew.ts +9 -0
  17. package/src/commands/assets.ts +28 -0
  18. package/src/commands/clone.ts +60 -0
  19. package/src/commands/compile.ts +253 -0
  20. package/src/commands/create.ts +59 -0
  21. package/src/commands/design.ts +863 -0
  22. package/src/commands/designTemplates.ts +11 -0
  23. package/src/commands/desktop.ts +122 -0
  24. package/src/commands/dev.ts +214 -0
  25. package/src/commands/diff.ts +59 -0
  26. package/src/commands/init.ts +191 -0
  27. package/src/commands/install.ts +198 -0
  28. package/src/commands/lightmap.ts +290 -0
  29. package/src/commands/link.ts +147 -0
  30. package/src/commands/login.ts +24 -0
  31. package/src/commands/navmesh.ts +225 -0
  32. package/src/commands/pn.ts +305 -0
  33. package/src/commands/projectTemplates.ts +129 -0
  34. package/src/commands/pull.ts +109 -0
  35. package/src/commands/push.ts +173 -0
  36. package/src/commands/render.ts +414 -0
  37. package/src/commands/scene.ts +148 -0
  38. package/src/commands/shaders.ts +157 -0
  39. package/src/commands/status.ts +29 -0
  40. package/src/commands/test.ts +370 -0
  41. package/src/commands/thumbs.ts +178 -0
  42. package/src/commands/types.ts +87 -0
  43. package/src/commands/update.ts +190 -0
  44. package/src/compile/assetIcons.ts +214 -0
  45. package/src/compile/collect.ts +46 -0
  46. package/src/compile/collectLocal.ts +37 -0
  47. package/src/compile/designCompile.ts +104 -0
  48. package/src/compile/fonts.ts +156 -0
  49. package/src/compile/headlessBundle.ts +129 -0
  50. package/src/compile/nativeStack.ts +21 -0
  51. package/src/compile/projectCompile.ts +109 -0
  52. package/src/compile/sceneCompile.ts +113 -0
  53. package/src/compile/screenEntry.ts +127 -0
  54. package/src/compile/shaders.ts +245 -0
  55. package/src/config.ts +42 -0
  56. package/src/designMeta.ts +35 -0
  57. package/src/desktopRenderer.ts +532 -0
  58. package/src/desktopScript.ts +276 -0
  59. package/src/dev/clientTemplates.ts +160 -0
  60. package/src/dev/devServer.ts +292 -0
  61. package/src/dev/wsServer.ts +144 -0
  62. package/src/distRoot.ts +20 -0
  63. package/src/ignore.ts +163 -0
  64. package/src/index.ts +491 -0
  65. package/src/lecodes-3d-editor.d.ts +41 -0
  66. package/src/lecodes-assets.d.ts +7 -0
  67. package/src/lecodes-design.d.ts +191 -0
  68. package/src/lecodes-renderer.d.ts +131 -0
  69. package/src/localFiles.ts +144 -0
  70. package/src/manifest.ts +46 -0
  71. package/src/matcTool.ts +137 -0
  72. package/src/peers.ts +40 -0
  73. package/src/project.ts +61 -0
  74. package/src/projectEnv.ts +94 -0
  75. package/src/qrcode-terminal.d.ts +9 -0
  76. package/src/releases.ts +125 -0
  77. package/src/serverDiff.ts +78 -0
  78. package/src/textDiff.ts +103 -0
  79. package/src/types.ts +0 -0
  80. package/src/util.ts +146 -0
  81. package/runtime/sdk/animate/animate.ts +0 -238
  82. package/runtime/sdk/animate/bezier.ts +0 -138
  83. package/runtime/sdk/animate/easings.ts +0 -126
  84. package/runtime/sdk/canvas/Canvas.ts +0 -305
  85. package/runtime/sdk/core/Aspect.ts +0 -512
  86. package/runtime/sdk/core/InspectorUI.ts +0 -212
  87. package/runtime/sdk/core/color.ts +0 -66
  88. package/runtime/sdk/core/compWrite.ts +0 -42
  89. package/runtime/sdk/core/events.ts +0 -38
  90. package/runtime/sdk/core/fields.ts +0 -120
  91. package/runtime/sdk/core/registry.ts +0 -23
  92. package/runtime/sdk/core/signals.ts +0 -277
  93. package/runtime/sdk/core/time.ts +0 -81
  94. package/runtime/sdk/g2/Camera2D.ts +0 -40
  95. package/runtime/sdk/g2/CharacterController2D.ts +0 -276
  96. package/runtime/sdk/g2/Node2D.ts +0 -267
  97. package/runtime/sdk/g2/OneWay2D.ts +0 -66
  98. package/runtime/sdk/g2/Physics2D.ts +0 -346
  99. package/runtime/sdk/g2/Scene2D.ts +0 -209
  100. package/runtime/sdk/g2/Shape2D.ts +0 -259
  101. package/runtime/sdk/g2/Sprite.ts +0 -89
  102. package/runtime/sdk/g2/SpriteAnimation.ts +0 -171
  103. package/runtime/sdk/g2/SpriteSheet.ts +0 -166
  104. package/runtime/sdk/g2/Texture2D.ts +0 -47
  105. package/runtime/sdk/g2/Tilemap.ts +0 -41
  106. package/runtime/sdk/g2/Tileset.ts +0 -71
  107. package/runtime/sdk/g2/Trigger2D.ts +0 -77
  108. package/runtime/sdk/g2/autotile.ts +0 -433
  109. package/runtime/sdk/g2/cells.ts +0 -91
  110. package/runtime/sdk/g2/defineScene2d.ts +0 -381
  111. package/runtime/sdk/g2/groups2d.ts +0 -106
  112. package/runtime/sdk/g2/loop.ts +0 -50
  113. package/runtime/sdk/g2/scenarios2d.ts +0 -69
  114. package/runtime/sdk/g2/touch.ts +0 -83
  115. package/runtime/sdk/gl/Camera.ts +0 -160
  116. package/runtime/sdk/gl/CameraPlace.ts +0 -52
  117. package/runtime/sdk/gl/CharacterController.ts +0 -238
  118. package/runtime/sdk/gl/Gearbox.ts +0 -212
  119. package/runtime/sdk/gl/Geometry.ts +0 -279
  120. package/runtime/sdk/gl/IK.ts +0 -193
  121. package/runtime/sdk/gl/InstancedMesh.ts +0 -132
  122. package/runtime/sdk/gl/Light.ts +0 -99
  123. package/runtime/sdk/gl/Lightmap.ts +0 -179
  124. package/runtime/sdk/gl/Material.ts +0 -245
  125. package/runtime/sdk/gl/Mesh.ts +0 -83
  126. package/runtime/sdk/gl/Model.ts +0 -64
  127. package/runtime/sdk/gl/Node.ts +0 -350
  128. package/runtime/sdk/gl/Noise.ts +0 -30
  129. package/runtime/sdk/gl/Particles.ts +0 -676
  130. package/runtime/sdk/gl/Physics.ts +0 -222
  131. package/runtime/sdk/gl/Plane.ts +0 -53
  132. package/runtime/sdk/gl/Ray.ts +0 -16
  133. package/runtime/sdk/gl/Scene.ts +0 -479
  134. package/runtime/sdk/gl/Shape.ts +0 -377
  135. package/runtime/sdk/gl/Texture.ts +0 -46
  136. package/runtime/sdk/gl/Trigger.ts +0 -45
  137. package/runtime/sdk/gl/Vehicle.ts +0 -473
  138. package/runtime/sdk/gl/Wheel.ts +0 -240
  139. package/runtime/sdk/gl/animation/AnimationClip.ts +0 -204
  140. package/runtime/sdk/gl/animation/Animator.ts +0 -87
  141. package/runtime/sdk/gl/animation/Layer.ts +0 -29
  142. package/runtime/sdk/gl/animation/Loop.ts +0 -25
  143. package/runtime/sdk/gl/animation/Playback.ts +0 -43
  144. package/runtime/sdk/gl/animation/core.ts +0 -294
  145. package/runtime/sdk/gl/controls.ts +0 -95
  146. package/runtime/sdk/gl/physicsEvents.ts +0 -20
  147. package/runtime/sdk/gl/scenarios.ts +0 -291
  148. package/runtime/sdk/gl/state.ts +0 -6
  149. package/runtime/sdk/gl/touch.ts +0 -68
  150. package/runtime/sdk/inject.ts +0 -186
  151. package/runtime/sdk/math/Mathf.ts +0 -118
  152. package/runtime/sdk/math/mat4.ts +0 -278
  153. package/runtime/sdk/math/quat.ts +0 -232
  154. package/runtime/sdk/math/vec.ts +0 -255
  155. package/runtime/sdk/plugins/camera.ts +0 -81
  156. package/runtime/sdk/plugins/geolocation.ts +0 -123
  157. package/runtime/sdk/plugins/oauth.ts +0 -61
  158. package/runtime/sdk/plugins/permission.ts +0 -7
  159. package/runtime/sdk/plugins/push.ts +0 -132
  160. package/runtime/sdk/plugins/qr.ts +0 -73
  161. package/runtime/sdk/plugins/service.ts +0 -47
  162. package/runtime/sdk/runtime/app.ts +0 -101
  163. package/runtime/sdk/runtime/appEvents.ts +0 -54
  164. package/runtime/sdk/runtime/channel.ts +0 -61
  165. package/runtime/sdk/runtime/clipboard.ts +0 -20
  166. package/runtime/sdk/runtime/datetime.ts +0 -329
  167. package/runtime/sdk/runtime/device.ts +0 -293
  168. package/runtime/sdk/runtime/fetch.ts +0 -77
  169. package/runtime/sdk/runtime/files.ts +0 -23
  170. package/runtime/sdk/runtime/input.ts +0 -175
  171. package/runtime/sdk/runtime/media.ts +0 -111
  172. package/runtime/sdk/runtime/misc.ts +0 -16
  173. package/runtime/sdk/runtime/net.ts +0 -36
  174. package/runtime/sdk/runtime/rpc.ts +0 -218
  175. package/runtime/sdk/runtime/service.ts +0 -83
  176. package/runtime/sdk/runtime/share.ts +0 -9
  177. package/runtime/sdk/runtime/storage.ts +0 -13
  178. package/runtime/sdk/runtime/touch.ts +0 -76
  179. package/runtime/sdk/scene/defineScene.ts +0 -1227
  180. package/runtime/sdk/scene/editorPlugins.ts +0 -92
  181. package/runtime/sdk/scene/gizmos.ts +0 -148
  182. package/runtime/sdk/scene/grammar.ts +0 -120
  183. package/runtime/sdk/scene/material.ts +0 -188
  184. package/runtime/sdk/server/auth/appConfig.ts +0 -12
  185. package/runtime/sdk/server/auth/global.ts +0 -80
  186. package/runtime/sdk/server/auth/host.ts +0 -318
  187. package/runtime/sdk/server/auth/models.ts +0 -83
  188. package/runtime/sdk/server/auth/types.ts +0 -50
  189. package/runtime/sdk/server/channel.ts +0 -56
  190. package/runtime/sdk/server/context.ts +0 -36
  191. package/runtime/sdk/server/db/defineDb.ts +0 -237
  192. package/runtime/sdk/server/db/fields.ts +0 -132
  193. package/runtime/sdk/server/db/httpTransport.ts +0 -93
  194. package/runtime/sdk/server/db/index.ts +0 -7
  195. package/runtime/sdk/server/db/marci/query.ts +0 -412
  196. package/runtime/sdk/server/db/types.ts +0 -202
  197. package/runtime/sdk/server/errors.ts +0 -12
  198. package/runtime/sdk/server/host.ts +0 -74
  199. package/runtime/sdk/server/inject.ts +0 -13
  200. package/runtime/sdk/server/runtime.ts +0 -133
  201. package/runtime/sdk/server/validate.ts +0 -87
  202. package/runtime/sdk/ui/NativeView.ts +0 -142
  203. package/runtime/sdk/ui/UI.ts +0 -39
  204. package/runtime/sdk/ui/UIBottomSheet.ts +0 -139
  205. package/runtime/sdk/ui/UIButton.ts +0 -101
  206. package/runtime/sdk/ui/UIContainer.ts +0 -60
  207. package/runtime/sdk/ui/UIImage.ts +0 -83
  208. package/runtime/sdk/ui/UIInput.ts +0 -185
  209. package/runtime/sdk/ui/UIModal.ts +0 -139
  210. package/runtime/sdk/ui/UINode.ts +0 -826
  211. package/runtime/sdk/ui/UIPager.ts +0 -362
  212. package/runtime/sdk/ui/UIPopover.ts +0 -100
  213. package/runtime/sdk/ui/UIScreen.ts +0 -123
  214. package/runtime/sdk/ui/UIScrollable.ts +0 -87
  215. package/runtime/sdk/ui/UISpacer.ts +0 -14
  216. package/runtime/sdk/ui/UITabs.ts +0 -236
  217. package/runtime/sdk/ui/UIText.ts +0 -51
  218. package/runtime/sdk/ui/UIVideo.ts +0 -88
  219. package/runtime/sdk/ui/UIVirtualizedList.ts +0 -241
  220. package/runtime/sdk/ui/UIWidget.ts +0 -127
  221. package/runtime/sdk/ui/fonts.ts +0 -13
  222. package/runtime/sdk/ui/presentable.ts +0 -117
  223. package/runtime/sdk/ui/router.ts +0 -132
  224. 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))