lecodes-cli 0.19.0 → 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 (254) hide show
  1. package/dist/index.js +730 -235
  2. package/package.json +14 -13
  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/compile/aspectMacro.ts +0 -86
  86. package/runtime/sdk/compile/assetIconMacro.ts +0 -384
  87. package/runtime/sdk/compile/assetMacro.ts +0 -146
  88. package/runtime/sdk/compile/assetName.ts +0 -50
  89. package/runtime/sdk/compile/bundler.ts +0 -287
  90. package/runtime/sdk/compile/compileProject.ts +0 -129
  91. package/runtime/sdk/compile/detectEntry.ts +0 -128
  92. package/runtime/sdk/compile/fontMacro.ts +0 -459
  93. package/runtime/sdk/compile/fontRegistry.ts +0 -78
  94. package/runtime/sdk/compile/header.ts +0 -67
  95. package/runtime/sdk/compile/index.ts +0 -100
  96. package/runtime/sdk/compile/libraryImports.ts +0 -52
  97. package/runtime/sdk/compile/liteMaterial.ts +0 -247
  98. package/runtime/sdk/compile/sceneEditor.ts +0 -88
  99. package/runtime/sdk/compile/serverSplit.ts +0 -233
  100. package/runtime/sdk/compile/serverTypes.ts +0 -227
  101. package/runtime/sdk/compile/sfnt.ts +0 -98
  102. package/runtime/sdk/compile/shaderSchema.ts +0 -202
  103. package/runtime/sdk/compile/shaderTargets.ts +0 -81
  104. package/runtime/sdk/compile/sourcemap.ts +0 -25
  105. package/runtime/sdk/core/Aspect.ts +0 -568
  106. package/runtime/sdk/core/InspectorUI.ts +0 -212
  107. package/runtime/sdk/core/StateMachine.ts +0 -308
  108. package/runtime/sdk/core/color.ts +0 -66
  109. package/runtime/sdk/core/compWrite.ts +0 -42
  110. package/runtime/sdk/core/events.ts +0 -38
  111. package/runtime/sdk/core/fields.ts +0 -120
  112. package/runtime/sdk/core/registry.ts +0 -23
  113. package/runtime/sdk/core/signals.ts +0 -277
  114. package/runtime/sdk/core/time.ts +0 -84
  115. package/runtime/sdk/g2/Camera2D.ts +0 -40
  116. package/runtime/sdk/g2/CharacterController2D.ts +0 -276
  117. package/runtime/sdk/g2/Node2D.ts +0 -267
  118. package/runtime/sdk/g2/OneWay2D.ts +0 -66
  119. package/runtime/sdk/g2/Physics2D.ts +0 -346
  120. package/runtime/sdk/g2/Scene2D.ts +0 -209
  121. package/runtime/sdk/g2/Shape2D.ts +0 -259
  122. package/runtime/sdk/g2/Sprite.ts +0 -89
  123. package/runtime/sdk/g2/SpriteAnimation.ts +0 -171
  124. package/runtime/sdk/g2/SpriteSheet.ts +0 -166
  125. package/runtime/sdk/g2/Texture2D.ts +0 -47
  126. package/runtime/sdk/g2/Tilemap.ts +0 -41
  127. package/runtime/sdk/g2/Tileset.ts +0 -71
  128. package/runtime/sdk/g2/Trigger2D.ts +0 -77
  129. package/runtime/sdk/g2/autotile.ts +0 -433
  130. package/runtime/sdk/g2/cells.ts +0 -91
  131. package/runtime/sdk/g2/defineScene2d.ts +0 -381
  132. package/runtime/sdk/g2/groups2d.ts +0 -106
  133. package/runtime/sdk/g2/loop.ts +0 -50
  134. package/runtime/sdk/g2/scenarios2d.ts +0 -69
  135. package/runtime/sdk/g2/touch.ts +0 -83
  136. package/runtime/sdk/gl/Camera.ts +0 -160
  137. package/runtime/sdk/gl/CameraPlace.ts +0 -52
  138. package/runtime/sdk/gl/CharacterController.ts +0 -238
  139. package/runtime/sdk/gl/Gearbox.ts +0 -212
  140. package/runtime/sdk/gl/Geometry.ts +0 -279
  141. package/runtime/sdk/gl/IK.ts +0 -193
  142. package/runtime/sdk/gl/InstancedMesh.ts +0 -132
  143. package/runtime/sdk/gl/Light.ts +0 -107
  144. package/runtime/sdk/gl/Lightmap.ts +0 -256
  145. package/runtime/sdk/gl/Material.ts +0 -260
  146. package/runtime/sdk/gl/Mesh.ts +0 -83
  147. package/runtime/sdk/gl/Model.ts +0 -124
  148. package/runtime/sdk/gl/NavAgent.ts +0 -337
  149. package/runtime/sdk/gl/NavMesh.ts +0 -397
  150. package/runtime/sdk/gl/Node.ts +0 -350
  151. package/runtime/sdk/gl/Noise.ts +0 -30
  152. package/runtime/sdk/gl/Particles.ts +0 -676
  153. package/runtime/sdk/gl/Physics.ts +0 -222
  154. package/runtime/sdk/gl/Plane.ts +0 -53
  155. package/runtime/sdk/gl/Ragdoll.ts +0 -270
  156. package/runtime/sdk/gl/Ray.ts +0 -16
  157. package/runtime/sdk/gl/Scene.ts +0 -486
  158. package/runtime/sdk/gl/Shape.ts +0 -409
  159. package/runtime/sdk/gl/Terrain.ts +0 -1092
  160. package/runtime/sdk/gl/Texture.ts +0 -63
  161. package/runtime/sdk/gl/Trigger.ts +0 -45
  162. package/runtime/sdk/gl/Vehicle.ts +0 -473
  163. package/runtime/sdk/gl/Wheel.ts +0 -240
  164. package/runtime/sdk/gl/animation/AnimationClip.ts +0 -204
  165. package/runtime/sdk/gl/animation/Animator.ts +0 -120
  166. package/runtime/sdk/gl/animation/Layer.ts +0 -31
  167. package/runtime/sdk/gl/animation/Loop.ts +0 -30
  168. package/runtime/sdk/gl/animation/Playback.ts +0 -44
  169. package/runtime/sdk/gl/animation/core.ts +0 -336
  170. package/runtime/sdk/gl/controls.ts +0 -95
  171. package/runtime/sdk/gl/physicsEvents.ts +0 -20
  172. package/runtime/sdk/gl/scenarios.ts +0 -291
  173. package/runtime/sdk/gl/state.ts +0 -6
  174. package/runtime/sdk/gl/terrainMesh.ts +0 -219
  175. package/runtime/sdk/gl/touch.ts +0 -68
  176. package/runtime/sdk/inject.ts +0 -197
  177. package/runtime/sdk/math/Mathf.ts +0 -118
  178. package/runtime/sdk/math/mat4.ts +0 -278
  179. package/runtime/sdk/math/quat.ts +0 -232
  180. package/runtime/sdk/math/vec.ts +0 -255
  181. package/runtime/sdk/net/codec.ts +0 -119
  182. package/runtime/sdk/net/core.ts +0 -384
  183. package/runtime/sdk/net/index.ts +0 -181
  184. package/runtime/sdk/net/replication.ts +0 -622
  185. package/runtime/sdk/plugins/camera.ts +0 -81
  186. package/runtime/sdk/plugins/geolocation.ts +0 -123
  187. package/runtime/sdk/plugins/oauth.ts +0 -61
  188. package/runtime/sdk/plugins/permission.ts +0 -7
  189. package/runtime/sdk/plugins/push.ts +0 -132
  190. package/runtime/sdk/plugins/qr.ts +0 -73
  191. package/runtime/sdk/plugins/service.ts +0 -47
  192. package/runtime/sdk/runtime/app.ts +0 -101
  193. package/runtime/sdk/runtime/appEvents.ts +0 -54
  194. package/runtime/sdk/runtime/channel.ts +0 -61
  195. package/runtime/sdk/runtime/clipboard.ts +0 -20
  196. package/runtime/sdk/runtime/datetime.ts +0 -329
  197. package/runtime/sdk/runtime/device.ts +0 -293
  198. package/runtime/sdk/runtime/fetch.ts +0 -77
  199. package/runtime/sdk/runtime/files.ts +0 -23
  200. package/runtime/sdk/runtime/input.ts +0 -175
  201. package/runtime/sdk/runtime/media.ts +0 -111
  202. package/runtime/sdk/runtime/misc.ts +0 -16
  203. package/runtime/sdk/runtime/net.ts +0 -36
  204. package/runtime/sdk/runtime/rpc.ts +0 -218
  205. package/runtime/sdk/runtime/service.ts +0 -83
  206. package/runtime/sdk/runtime/share.ts +0 -9
  207. package/runtime/sdk/runtime/storage.ts +0 -13
  208. package/runtime/sdk/runtime/touch.ts +0 -76
  209. package/runtime/sdk/scene/defineScene.ts +0 -1353
  210. package/runtime/sdk/scene/editorPlugins.ts +0 -110
  211. package/runtime/sdk/scene/gizmos.ts +0 -148
  212. package/runtime/sdk/scene/grammar.ts +0 -120
  213. package/runtime/sdk/scene/material.ts +0 -188
  214. package/runtime/sdk/server/auth/appConfig.ts +0 -12
  215. package/runtime/sdk/server/auth/global.ts +0 -80
  216. package/runtime/sdk/server/auth/host.ts +0 -318
  217. package/runtime/sdk/server/auth/models.ts +0 -83
  218. package/runtime/sdk/server/auth/types.ts +0 -50
  219. package/runtime/sdk/server/channel.ts +0 -56
  220. package/runtime/sdk/server/context.ts +0 -36
  221. package/runtime/sdk/server/db/defineDb.ts +0 -237
  222. package/runtime/sdk/server/db/fields.ts +0 -132
  223. package/runtime/sdk/server/db/httpTransport.ts +0 -93
  224. package/runtime/sdk/server/db/index.ts +0 -7
  225. package/runtime/sdk/server/db/marci/query.ts +0 -412
  226. package/runtime/sdk/server/db/types.ts +0 -202
  227. package/runtime/sdk/server/errors.ts +0 -12
  228. package/runtime/sdk/server/host.ts +0 -74
  229. package/runtime/sdk/server/inject.ts +0 -13
  230. package/runtime/sdk/server/runtime.ts +0 -133
  231. package/runtime/sdk/server/validate.ts +0 -87
  232. package/runtime/sdk/ui/NativeView.ts +0 -142
  233. package/runtime/sdk/ui/UI.ts +0 -39
  234. package/runtime/sdk/ui/UIBottomSheet.ts +0 -139
  235. package/runtime/sdk/ui/UIButton.ts +0 -101
  236. package/runtime/sdk/ui/UIContainer.ts +0 -60
  237. package/runtime/sdk/ui/UIImage.ts +0 -83
  238. package/runtime/sdk/ui/UIInput.ts +0 -185
  239. package/runtime/sdk/ui/UIModal.ts +0 -139
  240. package/runtime/sdk/ui/UINode.ts +0 -830
  241. package/runtime/sdk/ui/UIPager.ts +0 -362
  242. package/runtime/sdk/ui/UIPopover.ts +0 -100
  243. package/runtime/sdk/ui/UIScreen.ts +0 -123
  244. package/runtime/sdk/ui/UIScrollable.ts +0 -87
  245. package/runtime/sdk/ui/UISpacer.ts +0 -14
  246. package/runtime/sdk/ui/UITabs.ts +0 -236
  247. package/runtime/sdk/ui/UIText.ts +0 -51
  248. package/runtime/sdk/ui/UIVideo.ts +0 -88
  249. package/runtime/sdk/ui/UIVirtualizedList.ts +0 -241
  250. package/runtime/sdk/ui/UIWidget.ts +0 -127
  251. package/runtime/sdk/ui/fonts.ts +0 -13
  252. package/runtime/sdk/ui/presentable.ts +0 -117
  253. package/runtime/sdk/ui/router.ts +0 -132
  254. package/runtime/sdk/ui/theme.ts +0 -84
@@ -0,0 +1,292 @@
1
+ import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"
2
+ import { createReadStream, statSync } from "node:fs"
3
+ import { join } from "node:path"
4
+ import type { Manifest } from "../manifest"
5
+ import { resolveLocalPath } from "../localFiles"
6
+ import { collectLocalEntries } from "../compile/collectLocal"
7
+ import { applyLocalShaders, isShaderArtifactName, shadersCacheDir } from "../compile/shaders"
8
+ import { ambiguousEntryWarning, compileProject } from "../compile/projectCompile"
9
+ import { applyProjectFonts } from "../compile/fonts"
10
+ import { applyAppAssetIcons } from "../compile/assetIcons"
11
+ import { renderDevScreen, renderPrelude } from "./clientTemplates"
12
+ import { upgradeWebSocket, type WsConnection } from "./wsServer"
13
+ import { toKey } from "../util"
14
+ import { makeStackDemangler } from "../compile/nativeStack"
15
+
16
+ /*
17
+ * The `lecodes dev` LAN server: serves the project to devices and hot-reloads it on change.
18
+ *
19
+ * All routes live under a capability-token base path (/t/<token>):
20
+ * GET /t/<tok>/dev.js the bootstrap bundle the QR points at (placeholder screen + prelude)
21
+ * GET /t/<tok>/bundle.js?t=… the current project bundle + reload prelude (?t=poll for no-WS hosts)
22
+ * GET /t/<tok>/assets/<path> files streamed from the working tree (the bundle's asset URLs)
23
+ * GET /t/<tok>/shaders/<name> locally compiled shader artifacts (.lecodes/shaders — matc)
24
+ * GET /t/<tok>/version {"v":N} — polling fallback
25
+ * POST /t/<tok>/log log/error backchannel for polling clients
26
+ * WS /t/<tok>/ws reload push + log/error backchannel
27
+ *
28
+ * Compile is eager: the command calls recompile() on every file change; a successful compile bumps
29
+ * `v` and broadcasts {type:"reload", v} to connected sockets (polling clients see /version move).
30
+ * A failed compile keeps the last good bundle — devices stay on it until the next good compile.
31
+ */
32
+
33
+ export type DevServerOptions = {
34
+ root: string
35
+ manifest: Manifest | null
36
+ name: string
37
+ /** The advertised origin baked into bundles + the prelude, e.g. "http://192.168.1.5:4499". */
38
+ origin: string
39
+ /** Bind address (default 0.0.0.0). */
40
+ bind?: string
41
+ port: number
42
+ token: string
43
+ entryOverride?: string
44
+ /** Diagnostics (compile errors, device logs); defaults to stderr-style silence-safe noop. */
45
+ log?: (msg: string) => void
46
+ }
47
+
48
+ export type RecompileResult =
49
+ | { ok: true, changed: boolean, v: number, warnings: string[] }
50
+ | { ok: false, error: string }
51
+
52
+ export type DevServer = {
53
+ url: string
54
+ /** The token base path ("/t/<token>"). */
55
+ base: string
56
+ /** The actually-bound port (differs from options.port when 0 was passed, e.g. in tests). */
57
+ port: number
58
+ v: () => number
59
+ clientCount: () => number
60
+ recompile: () => Promise<RecompileResult>
61
+ close: () => void
62
+ }
63
+
64
+ const MIME: Record<string, string> = {
65
+ png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", webp: "image/webp", gif: "image/gif",
66
+ svg: "image/svg+xml", ktx: "application/octet-stream", ktx2: "application/octet-stream",
67
+ glb: "model/gltf-binary", bin: "application/octet-stream",
68
+ mp3: "audio/mpeg", wav: "audio/wav", ogg: "audio/ogg", m4a: "audio/mp4",
69
+ mp4: "video/mp4", webm: "video/webm",
70
+ ttf: "font/ttf", otf: "font/otf", woff: "font/woff", woff2: "font/woff2",
71
+ json: "application/json", txt: "text/plain", js: "text/javascript",
72
+ }
73
+
74
+ const contentType = (path: string): string => {
75
+ const i = path.lastIndexOf(".")
76
+ return (i >= 0 && MIME[path.slice(i + 1).toLowerCase()]) || "application/octet-stream"
77
+ }
78
+
79
+ const sendText = (res: ServerResponse, status: number, type: string, body: string) => {
80
+ res.writeHead(status, { "content-type": type, "cache-control": "no-store" })
81
+ res.end(body)
82
+ }
83
+
84
+ const sendJson = (res: ServerResponse, status: number, body: unknown) =>
85
+ sendText(res, status, "application/json", JSON.stringify(body))
86
+
87
+ const readBody = (req: IncomingMessage): Promise<string> =>
88
+ new Promise((resolve) => {
89
+ let body = ""
90
+ req.on("data", (c: Buffer) => { if (body.length < 256 * 1024) body += c.toString("utf8") })
91
+ req.on("end", () => resolve(body))
92
+ req.on("error", () => resolve(body))
93
+ })
94
+
95
+ export const startDevServer = async (options: DevServerOptions): Promise<DevServer> => {
96
+ const { root, manifest, name, origin, token, entryOverride } = options
97
+ const log = options.log ?? (() => {})
98
+ const base = `/t/${token}`
99
+
100
+ // ---- compile state (the last good bundle; devices never see a broken one) ----------------------
101
+ let v = 0
102
+ let bundleJs: string | null = null
103
+ let compileChain: Promise<unknown> = Promise.resolve()
104
+
105
+ // The header's `// last-updated:` timestamp differs on every compile — ignore it when deciding
106
+ // whether anything really changed (a no-op save must not reload devices).
107
+ const stripVolatile = (js: string) => js.replace(/^\/\/ last-updated: .*$/m, "")
108
+
109
+ const compileOnce = async (): Promise<RecompileResult> => {
110
+ try {
111
+ const { entries, warnings } = collectLocalEntries(root, manifest, `${base}/assets`, { localShaders: true })
112
+ // Custom shaders compile LOCALLY via matc (downloaded on first use) into .lecodes/shaders,
113
+ // served under /shaders/ below — no push needed. Degrades to a warning when matc is
114
+ // unavailable; a broken .mat throws and fails this compile like any code error.
115
+ warnings.push(...(await applyLocalShaders(entries, { root, srcPrefix: `${base}/shaders` })).warnings)
116
+ // font() macro: vendor registry faces into assets/fonts/ (served from the working tree like
117
+ // any asset — LAN/offline friendly) and emit the `// fonts:` boot header.
118
+ const fontsHeader = await applyProjectFonts(entries, {
119
+ mode: "vendor",
120
+ root,
121
+ vendorDir: root,
122
+ vendorPrefix: "",
123
+ publicUrl: origin,
124
+ makeResourceEntry: (path, absPath) => ({ path, type: "resource", fileSrc: `${base}/assets` + encodeURI(path), absPath }),
125
+ })
126
+ // assetIcon() macro: resolve from the user-level cache (~/.lecodes/cache/icons) and inline —
127
+ // one registry fetch per icon ever, so hot recompiles stay LAN/offline friendly.
128
+ const iconsHeader = await applyAppAssetIcons(entries)
129
+ if (!entryOverride) {
130
+ const ambiguous = ambiguousEntryWarning(entries)
131
+ if (ambiguous) warnings.push(ambiguous)
132
+ }
133
+ const js = await compileProject({ entries, name, publicUrl: origin, entryOverride, header: true, fontsHeader, iconsHeader })
134
+ const changed = bundleJs === null || stripVolatile(js) !== stripVolatile(bundleJs)
135
+ if (changed) {
136
+ bundleJs = js
137
+ v++
138
+ broadcast(JSON.stringify({ type: "reload", v }))
139
+ }
140
+ return { ok: true, changed, v, warnings }
141
+ } catch (e) {
142
+ return { ok: false, error: e instanceof Error ? e.message : String(e) }
143
+ }
144
+ }
145
+
146
+ // Serialized: a recompile requested mid-compile runs after it (compileOnce never rejects).
147
+ const recompile = (): Promise<RecompileResult> => {
148
+ const run = compileChain.then(compileOnce)
149
+ compileChain = run
150
+ return run
151
+ }
152
+
153
+ // dev.js: the placeholder project, compiled once per server start.
154
+ const devJs = await compileProject({
155
+ entries: [{ path: "/.dev-entry.ts", type: "text", text: renderDevScreen(name) }],
156
+ name: "LeCodes Dev",
157
+ publicUrl: origin,
158
+ entryOverride: "/.dev-entry.ts",
159
+ header: true,
160
+ })
161
+
162
+ // ---- websocket clients -------------------------------------------------------------------------
163
+ const clients = new Set<WsConnection>()
164
+ const broadcast = (text: string) => {
165
+ for (const client of clients) client.send(text)
166
+ }
167
+
168
+ // A device reports stacks against the bundle it evaluated ("module.js:5:51631" on the creator-pkg
169
+ // hosts) — rebuild the mapper whenever the bundle changes so those frames print as `main.ts:92:23`.
170
+ let demangleFor: { js: string, map: ((s: string) => string) | null } | null = null
171
+ const demangle = (text: string): string => {
172
+ if (bundleJs === null) return text
173
+ if (demangleFor?.js !== bundleJs) demangleFor = { js: bundleJs, map: makeStackDemangler(bundleJs) }
174
+ return demangleFor.map ? demangleFor.map(text) : text
175
+ }
176
+
177
+ const printDeviceMessage = (raw: string) => {
178
+ let m: { type?: string, level?: string, text?: string, v?: number } = {}
179
+ try { m = JSON.parse(raw) } catch { return }
180
+ if (m.type === "log") log(`[device:${m.level ?? "log"}] ${demangle(m.text ?? "")}`)
181
+ else if (m.type === "error") log(`[device:error] ${demangle(m.text ?? "")}`)
182
+ }
183
+
184
+ // ---- http --------------------------------------------------------------------------------------
185
+ const transportOf = (url: URL): "ws" | "poll" => (url.searchParams.get("t") === "poll" ? "poll" : "ws")
186
+
187
+ const handle = async (req: IncomingMessage, res: ServerResponse) => {
188
+ const url = new URL(req.url ?? "/", origin)
189
+ const path = url.pathname
190
+ if (!path.startsWith(base + "/")) return sendText(res, 404, "text/plain", "Not found")
191
+ const route = path.slice(base.length)
192
+
193
+ if (route === "/dev.js") {
194
+ return sendText(res, 200, "text/javascript",
195
+ devJs + "\n" + renderPrelude({ origin, base, transport: transportOf(url), v: -1 }))
196
+ }
197
+ if (route === "/bundle.js") {
198
+ if (bundleJs === null) return sendText(res, 503, "text/plain", "No successful compile yet — check the terminal.")
199
+ return sendText(res, 200, "text/javascript",
200
+ bundleJs + "\n" + renderPrelude({ origin, base, transport: transportOf(url), v }))
201
+ }
202
+ if (route === "/version") {
203
+ return sendJson(res, 200, { v })
204
+ }
205
+ if (route === "/log" && req.method === "POST") {
206
+ printDeviceMessage(await readBody(req))
207
+ return sendJson(res, 200, { ok: true })
208
+ }
209
+ if (route.startsWith("/shaders/")) {
210
+ const name = route.slice("/shaders/".length)
211
+ if (!isShaderArtifactName(name)) return sendText(res, 400, "text/plain", "Bad path")
212
+ const abs = join(shadersCacheDir(root), name)
213
+ try {
214
+ if (!statSync(abs).isFile()) throw new Error("not a file")
215
+ } catch {
216
+ return sendText(res, 404, "text/plain", "Not found")
217
+ }
218
+ res.writeHead(200, { "content-type": "application/octet-stream", "cache-control": "no-store" })
219
+ createReadStream(abs).pipe(res)
220
+ return
221
+ }
222
+ if (route.startsWith("/assets/")) {
223
+ let key: string
224
+ try {
225
+ key = toKey(decodeURIComponent(route.slice("/assets".length)))
226
+ } catch {
227
+ return sendText(res, 400, "text/plain", "Bad path")
228
+ }
229
+ const abs = resolveLocalPath(root, key)
230
+ if (!abs || !statSync(abs).isFile()) return sendText(res, 404, "text/plain", "Not found")
231
+ res.writeHead(200, { "content-type": contentType(key), "cache-control": "no-store" })
232
+ createReadStream(abs).pipe(res)
233
+ return
234
+ }
235
+ return sendText(res, 404, "text/plain", "Not found")
236
+ }
237
+
238
+ const server: Server = createServer((req, res) => {
239
+ handle(req, res).catch((e) => {
240
+ log(`request failed: ${e instanceof Error ? e.message : String(e)}`)
241
+ if (!res.headersSent) sendText(res, 500, "text/plain", "Internal error")
242
+ })
243
+ })
244
+
245
+ server.on("upgrade", (req, socket) => {
246
+ const url = new URL(req.url ?? "/", origin)
247
+ if (url.pathname !== base + "/ws") {
248
+ socket.destroy()
249
+ return
250
+ }
251
+ const conn = upgradeWebSocket(req, socket, {
252
+ onMessage: (text) => {
253
+ let m: { type?: string, v?: number } = {}
254
+ try { m = JSON.parse(text) } catch { /* not ours */ }
255
+ if (m.type === "hello") {
256
+ // A (re)connecting world announces what it runs; nudge it forward if it's stale.
257
+ if (typeof m.v === "number" && m.v !== v && bundleJs !== null) {
258
+ conn?.send(JSON.stringify({ type: "reload", v }))
259
+ }
260
+ } else {
261
+ printDeviceMessage(text)
262
+ }
263
+ },
264
+ onClose: () => { if (conn) clients.delete(conn) },
265
+ })
266
+ if (conn) clients.add(conn)
267
+ })
268
+
269
+ await new Promise<void>((resolve, reject) => {
270
+ server.once("error", reject)
271
+ server.listen(options.port, options.bind ?? "0.0.0.0", () => {
272
+ server.removeListener("error", reject)
273
+ resolve()
274
+ })
275
+ })
276
+
277
+ const address = server.address()
278
+ const boundPort = typeof address === "object" && address !== null ? address.port : options.port
279
+
280
+ return {
281
+ url: `${origin}${base}/dev.js`,
282
+ base,
283
+ port: boundPort,
284
+ v: () => v,
285
+ clientCount: () => clients.size,
286
+ recompile,
287
+ close: () => {
288
+ for (const client of clients) client.close()
289
+ server.close()
290
+ },
291
+ }
292
+ }
@@ -0,0 +1,144 @@
1
+ import { createHash } from "node:crypto"
2
+ import type { IncomingMessage } from "node:http"
3
+ import type { Duplex } from "node:stream"
4
+
5
+ /*
6
+ * A minimal RFC 6455 WebSocket server for the `lecodes dev` reload channel — dependency-free, like
7
+ * the rest of the CLI. Scope: server side only, text frames (the protocol is small JSON messages),
8
+ * ping→pong, close handshake. No fragmentation support (continuation frames are dropped) and no
9
+ * extensions — fine for OkHttp/NSURLSession/WinHTTP clients sending short messages.
10
+ */
11
+
12
+ const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
13
+
14
+ /** Sec-WebSocket-Accept for a client's Sec-WebSocket-Key (RFC 6455 §4.2.2). */
15
+ export const acceptKey = (key: string): string =>
16
+ createHash("sha1").update(key + WS_GUID).digest("base64")
17
+
18
+ /** A server→client text frame (FIN, unmasked — servers MUST NOT mask). */
19
+ export const encodeTextFrame = (text: string): Buffer => {
20
+ const payload = Buffer.from(text, "utf8")
21
+ const len = payload.length
22
+ let header: Buffer
23
+ if (len < 126) {
24
+ header = Buffer.from([ 0x81, len ])
25
+ } else if (len < 65536) {
26
+ header = Buffer.alloc(4)
27
+ header[0] = 0x81
28
+ header[1] = 126
29
+ header.writeUInt16BE(len, 2)
30
+ } else {
31
+ header = Buffer.alloc(10)
32
+ header[0] = 0x81
33
+ header[1] = 127
34
+ header.writeBigUInt64BE(BigInt(len), 2)
35
+ }
36
+ return Buffer.concat([ header, payload ])
37
+ }
38
+
39
+ const encodeControlFrame = (opcode: number, payload: Buffer = Buffer.alloc(0)): Buffer =>
40
+ Buffer.concat([ Buffer.from([ 0x80 | opcode, Math.min(payload.length, 125) ]), payload.subarray(0, 125) ])
41
+
42
+ export const OPCODE = { CONTINUATION: 0x0, TEXT: 0x1, BINARY: 0x2, CLOSE: 0x8, PING: 0x9, PONG: 0xa } as const
43
+
44
+ export type WsFrame = { opcode: number, payload: Buffer }
45
+
46
+ /** Stateful frame decoder: feed raw socket chunks, get complete (unmasked) frames back. */
47
+ export const createFrameDecoder = () => {
48
+ let buf: Buffer = Buffer.alloc(0)
49
+ return (chunk: Buffer): WsFrame[] => {
50
+ buf = buf.length === 0 ? chunk : Buffer.concat([ buf, chunk ])
51
+ const frames: WsFrame[] = []
52
+ while (buf.length >= 2) {
53
+ const opcode = buf[0] & 0x0f
54
+ const masked = (buf[1] & 0x80) !== 0
55
+ let len = buf[1] & 0x7f
56
+ let off = 2
57
+ if (len === 126) {
58
+ if (buf.length < 4) break
59
+ len = buf.readUInt16BE(2)
60
+ off = 4
61
+ } else if (len === 127) {
62
+ if (buf.length < 10) break
63
+ len = Number(buf.readBigUInt64BE(2))
64
+ off = 10
65
+ }
66
+ const maskLen = masked ? 4 : 0
67
+ if (buf.length < off + maskLen + len) break
68
+ let payload = buf.subarray(off + maskLen, off + maskLen + len)
69
+ if (masked) {
70
+ const mask = buf.subarray(off, off + 4)
71
+ const unmasked = Buffer.alloc(len)
72
+ for (let i = 0; i < len; i++) unmasked[i] = payload[i] ^ mask[i & 3]
73
+ payload = unmasked
74
+ }
75
+ buf = buf.subarray(off + maskLen + len)
76
+ frames.push({ opcode, payload })
77
+ }
78
+ return frames
79
+ }
80
+ }
81
+
82
+ export type WsConnection = {
83
+ send: (text: string) => void
84
+ close: () => void
85
+ }
86
+
87
+ export type WsHandlers = {
88
+ onMessage: (text: string) => void
89
+ /** Fired exactly once, however the connection ends. */
90
+ onClose: () => void
91
+ }
92
+
93
+ /**
94
+ * Complete the upgrade handshake on an `http.Server` `'upgrade'` event and speak WebSocket on the
95
+ * raw socket. Returns null (socket destroyed) if the request isn't a valid upgrade.
96
+ */
97
+ export const upgradeWebSocket = (req: IncomingMessage, socket: Duplex, handlers: WsHandlers): WsConnection | null => {
98
+ const key = req.headers["sec-websocket-key"]
99
+ if (typeof key !== "string" || (req.headers.upgrade ?? "").toLowerCase() !== "websocket") {
100
+ socket.destroy()
101
+ return null
102
+ }
103
+ socket.write(
104
+ "HTTP/1.1 101 Switching Protocols\r\n" +
105
+ "Upgrade: websocket\r\n" +
106
+ "Connection: Upgrade\r\n" +
107
+ `Sec-WebSocket-Accept: ${acceptKey(key)}\r\n\r\n`,
108
+ )
109
+
110
+ let open = true
111
+ const finish = () => {
112
+ if (!open) return
113
+ open = false
114
+ handlers.onClose()
115
+ }
116
+ const decode = createFrameDecoder()
117
+ socket.on("data", (chunk: Buffer) => {
118
+ for (const frame of decode(chunk)) {
119
+ if (frame.opcode === OPCODE.TEXT) {
120
+ handlers.onMessage(frame.payload.toString("utf8"))
121
+ } else if (frame.opcode === OPCODE.PING) {
122
+ if (open) socket.write(encodeControlFrame(OPCODE.PONG, frame.payload))
123
+ } else if (frame.opcode === OPCODE.CLOSE) {
124
+ if (open) socket.write(encodeControlFrame(OPCODE.CLOSE))
125
+ socket.end()
126
+ finish()
127
+ }
128
+ }
129
+ })
130
+ socket.on("close", finish)
131
+ socket.on("error", () => socket.destroy())
132
+
133
+ return {
134
+ send: (text: string) => {
135
+ if (open) socket.write(encodeTextFrame(text))
136
+ },
137
+ close: () => {
138
+ if (!open) return
139
+ socket.write(encodeControlFrame(OPCODE.CLOSE))
140
+ socket.end()
141
+ finish()
142
+ },
143
+ }
144
+ }
@@ -0,0 +1,20 @@
1
+ import { existsSync } from "node:fs"
2
+ import { dirname, join } from "node:path"
3
+ import { fileURLToPath } from "node:url"
4
+
5
+ /* Root of the CLI's on-disk companion files (runtime/ with the vendored SDK source + type bundle).
6
+ *
7
+ * Two distribution shapes:
8
+ * - npm package / monorepo dev: this module lives in dist/ (or src/), companions at ../runtime —
9
+ * resolve relative to import.meta.url as before.
10
+ * - standalone binary (`bun build --compile`, see scripts/build-bin.ts): import.meta.url points into
11
+ * the virtual bunfs archive which does NOT exist on disk, so the companions ship in the release
12
+ * archive NEXT TO the executable and are resolved via process.execPath. */
13
+ export const distRoot = (() => {
14
+ const candidates = [
15
+ join(dirname(fileURLToPath(import.meta.url)), ".."), // package root (dist/.. or src/..)
16
+ dirname(process.execPath), // standalone binary: next to the exe
17
+ ]
18
+ for (const c of candidates) if (existsSync(join(c, "runtime"))) return c
19
+ return candidates[0]
20
+ })()
package/src/ignore.ts ADDED
@@ -0,0 +1,163 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs"
2
+ import { join } from "node:path"
3
+
4
+ /*
5
+ * `.lecodesignore` support — keeps scratch files (temp probes, render screenshots, logs) out of
6
+ * `status` / `push` so they never land in a checkpoint. Syntax is a practical subset of .gitignore:
7
+ * - blank lines and `# comments` are skipped
8
+ * - `!pattern` re-includes something a previous pattern ignored
9
+ * - a trailing `/` matches directories only
10
+ * - a leading `/` (or any embedded `/`) anchors the pattern to the project root
11
+ * - `*` matches within a path segment, `**` across segments, `?` a single char
12
+ * - a slash-free pattern (e.g. `_*`, `*.png`) matches a basename at any depth
13
+ *
14
+ * IMPORTANT: ignores apply to UNTRACKED files only. A file already in the project (recorded in the
15
+ * manifest) is always tracked, even if it matches a pattern — so ignoring `*.png` can never make an
16
+ * existing image asset silently disappear from the project. Pruning happens in `scanLocal`.
17
+ */
18
+
19
+ export type IgnoreMatcher = (key: string, isDir: boolean) => boolean
20
+
21
+ type Rule = { re: RegExp, negate: boolean, dirOnly: boolean }
22
+
23
+ // Always applied, even with no .lecodesignore — only patterns that are never real project assets, so
24
+ // they're safe as invisible defaults. Everything asset-shaped (*.png, *.json, …) is opt-in via the
25
+ // file written at clone time (see DEFAULT_IGNORE_FILE). `.artifacts/` is `lecodes test`'s
26
+ // failure-frame dump dir (tests/.artifacts/) — regenerated scratch, never a project asset.
27
+ // `dist/` is `lecodes app build desktop`'s default output (<root>/dist/<slug>, appDesktop.ts) —
28
+ // same class, but it must be ignored for a second, load-bearing reason: that folder holds a COPY of
29
+ // the host's runtime assets (lit.filamat, uberarchive.bin, …). Collected as project assets they get
30
+ // staged next to the bundle, and onFetchLocal searches the bundle's dir BEFORE the host's assets dir
31
+ // (main.cpp) — so the copy shadows the host's own. Harmless while every host was OpenGL and the
32
+ // bytes matched; fatal once a build output can come from the Vulkan host, where the shadowing
33
+ // lit.filamat is SPIR-V and Filament aborts with "not built for any of the OpenGL backend's
34
+ // supported shader languages".
35
+ // `.env` / `.env.*`: the project's LECODES_* config (projectEnv.ts) — it holds tokens and keystore
36
+ // passwords, so it never leaves the machine whatever .lecodesignore says.
37
+ const BUILTIN_PATTERNS = ["node_modules/", ".git/", "_*", "*.log", ".DS_Store", ".artifacts/", "dist/", ".env", ".env.*"]
38
+
39
+ /** Template `.lecodesignore` written on clone — the explicit, editable "what we don't push" list. */
40
+ export const DEFAULT_IGNORE_FILE = `# Files matched here are NOT pushed to LeCodes.
41
+ # Only untracked files are affected — assets already in the project stay tracked
42
+ # even if they match a pattern. Built-in defaults (node_modules, _*, *.log, .artifacts)
43
+ # always apply.
44
+
45
+ # Render / preview outputs (the default \`lecodes render --png\` target)
46
+ screenshot.png
47
+ shots/
48
+
49
+ # Design-board comments live on the platform; design/comments/ is the local mirror of the
50
+ # open ones (\`lecodes design comments pull\`) — read by Claude, but pushing it back as
51
+ # project files would only duplicate the database.
52
+ design/comments/
53
+
54
+ # Image & other binary assets ARE pushed by default — they're real project resources.
55
+ # Uncomment a pattern below only if you want to keep some untracked ones out of pushes.
56
+ # *.png
57
+
58
+ # Local notes & dumps (uncomment to ignore)
59
+ # *.json
60
+ # notes.md
61
+ `
62
+
63
+ /** Translate a glob (one path segment's worth of `*`/`?`/`**`) into a regex fragment. */
64
+ const translate = (glob: string): string => {
65
+ let re = ""
66
+ for (let i = 0; i < glob.length; i++) {
67
+ const ch = glob[i]
68
+ if (ch === "*") {
69
+ if (glob[i + 1] === "*") { re += ".*"; i++; if (glob[i + 1] === "/") i++ } // `**/` spans dirs
70
+ else re += "[^/]*"
71
+ } else if (ch === "?") re += "[^/]"
72
+ else if (ch === "/") re += "/"
73
+ else if ("\\^$.|+()[]{}".includes(ch)) re += "\\" + ch
74
+ else re += ch
75
+ }
76
+ return re
77
+ }
78
+
79
+ const compile = (raw: string): Rule | null => {
80
+ let p = raw.trim()
81
+ if (!p || p.startsWith("#")) return null
82
+ let negate = false
83
+ if (p.startsWith("!")) { negate = true; p = p.slice(1) }
84
+ let dirOnly = false
85
+ if (p.endsWith("/")) { dirOnly = true; p = p.slice(0, -1) }
86
+ const anchored = p.startsWith("/") || p.includes("/")
87
+ if (p.startsWith("/")) p = p.slice(1)
88
+ if (!p) return null
89
+ const body = translate(p)
90
+ // `(/.*)?$` lets a directory pattern also match files nested under it, matching gitignore behavior
91
+ // even without relying on directory pruning.
92
+ const re = anchored ? new RegExp("^/" + body + "(/.*)?$") : new RegExp("(^|/)" + body + "(/.*)?$")
93
+ return { re, negate, dirOnly }
94
+ }
95
+
96
+ const buildMatcher = (rules: Rule[]): IgnoreMatcher => (key, isDir) => {
97
+ let ignored = false
98
+ for (const r of rules) {
99
+ if (r.dirOnly && !isDir) continue // a `foo/` rule only matches dirs directly (files via prune)
100
+ if (r.re.test(key)) ignored = !r.negate
101
+ }
102
+ return ignored
103
+ }
104
+
105
+ /** Compile a standalone glob list (same syntax as .lecodesignore lines, including `!` re-includes)
106
+ * into a file-path matcher — used for app.json's `resources.remote` patterns. */
107
+ export const buildGlobMatcher = (patterns: string[]): ((path: string) => boolean) => {
108
+ // Matching is over FILE paths — normalize `videos/` (dir-only, would match nothing here) to
109
+ // `videos`, whose `(/.*)?$` tail matches everything under the directory.
110
+ const rules = patterns.map((p) => compile(p.trim().replace(/\/+$/, ""))).filter((r): r is Rule => r !== null)
111
+ const match = buildMatcher(rules)
112
+ // Anchored rules compile to `^/…` — they expect the canonical leading-slash key form.
113
+ return (path) => match(path.startsWith("/") ? path : "/" + path, false)
114
+ }
115
+
116
+ /** Read `<root>/.lecodesignore` (if any) and combine with the built-in defaults into a matcher. */
117
+ export const loadIgnore = (root: string): IgnoreMatcher => {
118
+ const lines = [...BUILTIN_PATTERNS]
119
+ const file = join(root, ".lecodesignore")
120
+ if (existsSync(file)) {
121
+ try { lines.push(...readFileSync(file, "utf8").split(/\r?\n/)) } catch { /* unreadable → defaults only */ }
122
+ }
123
+ const rules = lines.map(compile).filter((r): r is Rule => r !== null)
124
+ return buildMatcher(rules)
125
+ }
126
+
127
+ /** Write the template `.lecodesignore` if the project doesn't have one yet (used on clone). */
128
+ export const writeDefaultIgnore = (root: string) => {
129
+ const file = join(root, ".lecodesignore")
130
+ if (!existsSync(file)) writeFileSync(file, DEFAULT_IGNORE_FILE)
131
+ }
132
+
133
+ /* `.gitignore` for the project's OWN git repo. `.lecodes/` is per-checkout state — regenerable IDE
134
+ * types (`lecodes update`) + `manifest.json`, this machine's push cursor (baseHeadId + per-file shas).
135
+ * Committing it makes collaborators fight over the cursor and ship stale types, so every scaffold
136
+ * path (init / clone / link) makes sure it's ignored. Render outputs mirror DEFAULT_IGNORE_FILE. */
137
+ export const DEFAULT_GITIGNORE = `# LeCodes per-checkout state: generated IDE types + this machine's sync cursor
138
+ # (.lecodes/manifest.json). Regenerate with \`lecodes update\` / re-attach with \`lecodes link\`.
139
+ .lecodes/
140
+
141
+ # Render / preview outputs and test failure frames
142
+ screenshot.png
143
+ shots/
144
+ tests/.artifacts/
145
+ tests/.shots/
146
+
147
+ node_modules/
148
+
149
+ dist/
150
+ `
151
+
152
+ /** Create `.gitignore` if missing; if it exists but doesn't ignore `.lecodes/`, append that rule.
153
+ * Returns what happened so callers can log it ("created" | "updated" | null). */
154
+ export const ensureGitignore = (root: string): "created" | "updated" | null => {
155
+ const file = join(root, ".gitignore")
156
+ if (!existsSync(file)) { writeFileSync(file, DEFAULT_GITIGNORE); return "created" }
157
+ const text = readFileSync(file, "utf8")
158
+ const covered = text.split(/\r?\n/).some(l => /^\/?\.lecodes\/?\s*$/.test(l.trim()))
159
+ if (covered) return null
160
+ const sep = text.length === 0 || text.endsWith("\n") ? "" : "\n"
161
+ writeFileSync(file, `${text}${sep}\n# LeCodes per-checkout state (generated types + sync cursor) — not source\n.lecodes/\n`)
162
+ return "updated"
163
+ }