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,113 @@
1
+ import { createHash } from "node:crypto"
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
3
+ import { join } from "node:path"
4
+ import type { Manifest } from "../manifest"
5
+ import { scanLocal } from "../localFiles"
6
+ import { loadIgnore } from "../ignore"
7
+ import { distRoot } from "../distRoot"
8
+ import { CliError, warnErr } from "../util"
9
+ import { compileProject, type CompileEntry } from "./projectCompile"
10
+ import { applyLocalShaders, shadersCacheDir } from "./shaders"
11
+
12
+ /*
13
+ * The `lecodes scene` compile closure — the CLI's implementation of the scene-editor host's inject
14
+ * point (the platform backend and the standalone playground are the other two). Reads the project
15
+ * off disk, composes the edit-mode bundle with the SDK's shared `sceneEditorEntries`, and drives it
16
+ * through the CLI's chisel + vendored-SDK pipeline. Deliberately mirrors the playground's in-process
17
+ * closure (scene-editor/src/server/inProcess.ts) field-for-field so every host emits the same
18
+ * bundle — only the file source (disk vs. the folder) and the harness source differ.
19
+ *
20
+ * publicUrl "" + resource fileSrc "/project/<rel>" → the scene server's /project/* mount.
21
+ * minify:false — mangling erases class names the inspector matches aspect schemas by.
22
+ * editor:true — the EDITOR global is `true`; `*.editor.ts` plugin files are bundled in.
23
+ */
24
+
25
+ /** Text files handed to the compiler as source; everything else is a binary resource served from
26
+ * the project folder (matches the playground's TEXT_EXT split, not the manifest's kind map). */
27
+ const TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i
28
+
29
+ /** Strip scanLocal's leading-slash key convention ("/city.scene.ts" → "city.scene.ts"). */
30
+ const rel = (key: string) => key.replace(/^\//, "")
31
+
32
+ /**
33
+ * Resolve the scene-editor harness sources (packages/projects/src/scene-editor):
34
+ * LECODES_SCENE_HARNESS_DIR env → the in-repo dir (monorepo dev), both via the SDK helper, else
35
+ * the JSON snapshot vendored into the CLI at publish time (runtime/scene-harness.json).
36
+ */
37
+ const resolveHarness = (loadSceneHarness: (dir?: string) => Record<string, string>): Record<string, string> => {
38
+ const inRepo = join(distRoot, "..", "projects", "src", "scene-editor")
39
+ try {
40
+ return loadSceneHarness(existsSync(inRepo) ? inRepo : undefined)
41
+ } catch {
42
+ const vendored = join(distRoot, "runtime", "scene-harness.json")
43
+ if (existsSync(vendored)) return JSON.parse(readFileSync(vendored, "utf8")) as Record<string, string>
44
+ throw new CliError("scene-editor harness sources not found — reinstall the CLI or set LECODES_SCENE_HARNESS_DIR.")
45
+ }
46
+ }
47
+
48
+ export const compileSceneEditor = async (opts: {
49
+ root: string
50
+ /** Null in standalone mode (a bare folder, no cloned project). */
51
+ manifest: Manifest | null
52
+ /** Project display name (compile metadata only, header is off). */
53
+ name: string
54
+ /** Project path of the scene file as the editor sends it ("./city.scene.ts"). */
55
+ scenePath: string
56
+ }): Promise<{ js: string | null, error?: string }> => {
57
+ const { root, scenePath } = opts
58
+ const { loadSceneHarness, sceneEditorEntries, SCENE_ENTRY_PATH } = await import("lecodes-sdk/compile")
59
+
60
+ const entries: CompileEntry[] = []
61
+ const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(opts.manifest?.files ?? {})) })
62
+ for (const file of scan.files) {
63
+ const path = "./" + rel(file.path)
64
+ if (TEXT_EXT.test(path)) {
65
+ entries.push({ path, type: "text", text: readFileSync(file.absPath, "utf8") })
66
+ } else if (/\.mat$/i.test(path)) {
67
+ // custom shaders compile locally (matc + the lite JSON) into .lecodes/shaders, served through
68
+ // the same /project/* mount — see the fallback below when matc isn't available
69
+ entries.push({ path, type: "shader", text: readFileSync(file.absPath, "utf8"), absPath: file.absPath })
70
+ } else {
71
+ // Binary assets resolve to URLs the scene server serves from the project folder (/project/*).
72
+ entries.push({ path, type: "resource", fileSrc: "/project/" + rel(file.path) })
73
+ }
74
+ }
75
+
76
+ // Shader artifacts: the web targets only (the editor runs in a browser — lite viewport +
77
+ // filament preview, which loads the mobile `opengl` artifact — there is no web-specific matc
78
+ // build). Without matc (offline first run) the lite artifact is still built from the
79
+ // source alone, hashed on the text, the way the playground does — the lite viewport keeps
80
+ // working and only the Rendered mode lacks the shader.
81
+ const shaderEntries = entries.filter((e) => e.type === "shader")
82
+ if (shaderEntries.length > 0) {
83
+ const { warnings } = await applyLocalShaders(shaderEntries, { root, srcPrefix: "/project/.lecodes/shaders", platforms: [ "opengl", "webgl-lite" ] })
84
+ for (const w of warnings) warnErr(w)
85
+ const { buildLiteMaterialArtifact } = await import("lecodes-sdk/compile")
86
+ for (const e of shaderEntries) {
87
+ if (e.shaders?.length) continue
88
+ const hash = createHash("sha256").update(e.text ?? "").digest("hex").slice(0, 16)
89
+ const dir = shadersCacheDir(root)
90
+ mkdirSync(dir, { recursive: true })
91
+ const liteName = `${hash}_webgl-lite.filamat`
92
+ if (!existsSync(join(dir, liteName))) writeFileSync(join(dir, liteName), JSON.stringify(buildLiteMaterialArtifact(e.text ?? "")))
93
+ const src = `/project/.lecodes/shaders/${hash}_opengl.filamat`
94
+ e.shaders = [ "opengl", "webgl-lite" ].map((platform) => ({ platform, src }))
95
+ }
96
+ }
97
+
98
+ const scene = scenePath.startsWith("./") ? scenePath : "./" + rel(scenePath)
99
+ if (!entries.some((e) => e.path === scene && e.type === "text")) {
100
+ return { js: null, error: `Scene file not found: ${scene}` }
101
+ }
102
+ entries.push(...sceneEditorEntries(scene, resolveHarness(loadSceneHarness), entries.map((e) => e.path)))
103
+
104
+ try {
105
+ const js = await compileProject({
106
+ entries, name: opts.name, publicUrl: "", entryOverride: SCENE_ENTRY_PATH,
107
+ header: false, format: "iife", minify: false, editor: true,
108
+ })
109
+ return { js }
110
+ } catch (e: any) {
111
+ return { js: null, error: e?.message?.replace(/mem:/g, "") ?? String(e) }
112
+ }
113
+ }
@@ -0,0 +1,127 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs"
2
+ import { isAbsolute, join, relative, resolve } from "node:path"
3
+ import { screenFrame } from "../designMeta"
4
+ import { CliError, flagStr, toKey, type Args } from "../util"
5
+ import type { CompileEntry } from "./projectCompile"
6
+
7
+ /*
8
+ * Rendering ONE screen module instead of the whole app — `lecodes render <file>` (and
9
+ * `--desktop`). A design screen (`design/screens/login.ts`) never boots itself: it default-exports
10
+ * a `UIScreen`, or a `(state) => UIScreen` function for its states, and the design board / snapshot
11
+ * supply the `.open()`. This builds the same virtual entry for the normal project pipeline, so any
12
+ * file written that way — inside design/ or not — renders with the full `lecodes render` flag set
13
+ * (--png/--clip/--script/--device/…) against the project's real imports and assets.
14
+ *
15
+ * Kept in step with compile/designCompile.ts, which does the same wrapping per screen for the board.
16
+ */
17
+
18
+ /** Extensions a screen module may carry (so `render design/screens/login` works too). */
19
+ const SCREEN_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs"]
20
+
21
+ /** Where the generated entry lives — the project root, so its import specifier is the screen's
22
+ * own project key ("./design/screens/login"). Leading dot: never a real project file. */
23
+ const SCREEN_ENTRY_PATH = "/.screen-entry.ts"
24
+
25
+ /** The design folder a bare screen id (`lecodes render login`) is looked up in. */
26
+ const DEFAULT_DESIGN_DIR = "design"
27
+
28
+ export type ScreenTarget = {
29
+ /** The virtual entry module — hand it to the compiler alongside the project's own entries. */
30
+ entry: CompileEntry
31
+ /** Path of that entry, for `entryOverride`. */
32
+ entryPath: string
33
+ /** The board frame from the design folder's meta.json, when this IS a design screen. */
34
+ frame?: [number, number]
35
+ /** How the screen is named in progress notes ("design/screens/login.ts", "login@empty"). */
36
+ label: string
37
+ }
38
+
39
+ /** `lecodes render <file|screen-id>` — the positional a screen render is asked for by. */
40
+ const screenArg = (args: Args): string | undefined => args._[0]
41
+
42
+ /** Whether the argument names a path at all (vs a bare screen id). */
43
+ const hasSeparator = (file: string) => file.includes("/") || file.includes("\\")
44
+
45
+ /** Find the screen file: a path (with or without extension), or a bare id resolved against the
46
+ * design folder's screens/ (`lecodes render login`, honoring --dir). */
47
+ const findScreenFile = (root: string, file: string, dirFlag: string | undefined): string => {
48
+ const candidates = [resolve(process.cwd(), file)]
49
+ if (!hasSeparator(file)) {
50
+ candidates.push(join(root, (dirFlag ?? DEFAULT_DESIGN_DIR).replace(/[/\\]+$/, ""), "screens", file))
51
+ }
52
+ for (const base of candidates) {
53
+ if (existsSync(base) && statSync(base).isFile()) return base
54
+ for (const ext of SCREEN_EXTS) if (existsSync(base + ext)) return base + ext
55
+ }
56
+ throw new CliError(
57
+ `No screen file at "${file}".` +
58
+ (hasSeparator(file) ? "" : ` Pass a path, or a screen id that exists in ${dirFlag ?? DEFAULT_DESIGN_DIR}/screens/.`))
59
+ }
60
+
61
+ /** The design folder a screen file belongs to ("<root>/design" for "/design/screens/login.ts"),
62
+ * or null when it isn't laid out as a design screen. */
63
+ const designDirOf = (root: string, key: string): { dir: string, id: string } | null => {
64
+ const m = /^\/(.*)screens\/([^/]+)\.[^./]+$/.exec(key)
65
+ if (!m) return null
66
+ const dir = join(root, m[1])
67
+ return existsSync(join(dir, "meta.json")) ? { dir, id: m[2] } : null
68
+ }
69
+
70
+ /**
71
+ * Resolve `<file>` into the virtual entry that opens its default export.
72
+ * `state` picks the branch of a `(state) => UIScreen` screen (ignored by a value export).
73
+ */
74
+ const resolveScreenTarget = (
75
+ root: string, file: string, opts: { state?: string, dir?: string } = {},
76
+ ): ScreenTarget => {
77
+ const abs = findScreenFile(root, file, opts.dir)
78
+ const rel = relative(root, abs)
79
+ if (rel.startsWith("..") || isAbsolute(rel)) {
80
+ throw new CliError(
81
+ `"${file}" is outside the project (${root}) — a screen renders as part of its project, so its ` +
82
+ `imports and asset() files resolve from the same tree.`)
83
+ }
84
+ const key = toKey(rel)
85
+
86
+ // A file with no default export compiles to an empty render; say so here instead.
87
+ if (!/^\s*export\s+default\b/m.test(readFileSync(abs, "utf8"))) {
88
+ throw new CliError(
89
+ `${key.slice(1)} has no default export. A screen file ends with ` +
90
+ `\`export default UIScreen([...])\` — or \`export default (state) => UIScreen([...])\` for screen states.`)
91
+ }
92
+
93
+ // Same wrapper the design board compiles (designCompile.ts): a function export is invoked with the
94
+ // chosen state (a bare call uses the parameter's default = the canonical state), a value is used
95
+ // as-is, and either way the screen is opened so the runtime has something to render.
96
+ const invoke = opts.state !== undefined ? `screen(${JSON.stringify(opts.state)})` : "screen()"
97
+ const spec = "." + key.replace(/\.[^./]+$/, "")
98
+ const design = designDirOf(root, key)
99
+ return {
100
+ entryPath: SCREEN_ENTRY_PATH,
101
+ entry: {
102
+ path: SCREEN_ENTRY_PATH,
103
+ type: "text",
104
+ text: `import screen from ${JSON.stringify(spec)}\n`
105
+ + `const __screen = typeof screen === "function" ? ${invoke} : screen\n`
106
+ + `__screen.open()\n`,
107
+ },
108
+ frame: design ? screenFrame(design.dir, design.id) : undefined,
109
+ label: (design ? design.id : key.slice(1)) + (opts.state !== undefined ? `@${opts.state}` : ""),
110
+ }
111
+ }
112
+
113
+ /** Read the screen target off the CLI args, or undefined when no screen was named. Rejects the
114
+ * flag combinations that would fight over the entrypoint. */
115
+ export const screenTargetFromArgs = (root: string, args: Args): ScreenTarget | undefined => {
116
+ const file = screenArg(args)
117
+ if (file === undefined) {
118
+ if (flagStr(args, "state") !== undefined) {
119
+ throw new CliError("--state picks a screen's state — name the screen file too: lecodes render <file> --state <name>")
120
+ }
121
+ return undefined
122
+ }
123
+ if (flagStr(args, "entry") !== undefined) {
124
+ throw new CliError("Use either a screen file or --entry, not both — each names the module to compile from.")
125
+ }
126
+ return resolveScreenTarget(root, file, { state: flagStr(args, "state"), dir: flagStr(args, "dir") })
127
+ }
@@ -0,0 +1,245 @@
1
+ import { execFile } from "node:child_process"
2
+ import { createHash, randomBytes } from "node:crypto"
3
+ import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"
4
+ import { tmpdir } from "node:os"
5
+ import { basename, join } from "node:path"
6
+ import {
7
+ ALL_PLATFORM_BUILD, buildLiteMaterialArtifact, externalSamplerReg, LOCAL_ONLY_PLATFORMS,
8
+ matcArgs, MATC_PLATFORMS, TARGET_MATRIX_GENERATION,
9
+ type AnyMatcPlatform, type AnyShaderPlatform,
10
+ } from "lecodes-sdk/compile"
11
+ import { resolveMatcExe, resolveMatcExeCached } from "../matcTool"
12
+ import { LECODES_DIR } from "../manifest"
13
+ import { CliError } from "../util"
14
+ import type { CompileEntry } from "./projectCompile"
15
+
16
+ /*
17
+ * LOCAL shader compile — the working-tree twin of the backend's compileShaderAsset
18
+ * (backend/src/utils/compileShader.ts): each .mat source compiles via matc (matcTool.ts —
19
+ * downloaded on first use) into the same artifact family the server produces, so `lecodes dev`
20
+ * doesn't need a push for shaders to work. The target matrix + the webgl-lite JSON artifact
21
+ * builder are shared through sdk/compile (shaderTargets.ts / liteMaterial.ts) — keep the two
22
+ * compile paths producing the SAME set of names: the bundle derives per-backend URLs by
23
+ * substituting `_creator.backend` into the opengl src.
24
+ *
25
+ * Artifacts cache in <root>/.lecodes/shaders/<hash>_<platform>.filamat, keyed by a hash of the
26
+ * source text + the matc binary's identity (size/mtime) — a changed shader or an updated matc
27
+ * recompiles, an unchanged one costs a hash. The dev server serves the cache under /shaders/;
28
+ * `lecodes shaders` (commands/shaders.ts) lists/compiles the same cache explicitly.
29
+ *
30
+ * When matc can't be resolved (offline first run, unpublished platform), entries degrade to the
31
+ * old behavior — empty artifact list + the push-once warning — instead of failing the compile.
32
+ */
33
+
34
+ const SHADERS_DIR = "shaders"
35
+
36
+ export const shadersCacheDir = (root: string): string => join(root, LECODES_DIR, SHADERS_DIR)
37
+
38
+ /** Served artifact names are exactly what applyLocalShaders emits — hash + a known platform. */
39
+ export const isShaderArtifactName = (name: string): boolean =>
40
+ /^[0-9a-f]{16}_[a-z-]+\.filamat$/.test(name)
41
+
42
+ /** The matc binary's cache-key identity — an updated (or locally rebuilt) matc recompiles. */
43
+ export const matcIdentity = (exe: string): string => {
44
+ const stat = statSync(exe)
45
+ return `${stat.size}:${Math.trunc(stat.mtimeMs)}`
46
+ }
47
+
48
+ // TARGET_MATRIX_GENERATION is in the key so that changing the matc argv behind an EXISTING target
49
+ // name invalidates caches that would otherwise look complete (adding/removing a name is already
50
+ // caught by artifactFamily). Same class as matcId: a toolchain or matrix change means recompile.
51
+ const shaderHash = (text: string, matcId: string): string =>
52
+ createHash("sha256").update(`${TARGET_MATRIX_GENERATION}\0${matcId}\0${text}`).digest("hex").slice(0, 16)
53
+
54
+ /** The full artifact family: the server's matc targets + the CLI's local-only ones + the lite JSON.
55
+ * This is what "compiled" means for `lecodes shaders` status and what a caller gets when it
56
+ * doesn't ask for a narrower set. The web runtime loads the mobile `opengl` artifact (the engine
57
+ * rewrites external samplers itself — sdk/compile shaderTargets.ts), so there is no web target. */
58
+ const artifactFamily = (): AnyShaderPlatform[] => [...MATC_PLATFORMS, ...LOCAL_ONLY_PLATFORMS, "webgl-lite"]
59
+
60
+ /** True when every artifact for this source is already in the cache (`lecodes shaders` status). */
61
+ export const shaderIsCompiled = (root: string, text: string, matcId: string): boolean => {
62
+ const hash = shaderHash(text, matcId)
63
+ return artifactFamily().every((p) => existsSync(join(shadersCacheDir(root), `${hash}_${p}.filamat`)))
64
+ }
65
+
66
+ /** Which artifacts of the family are present — `lecodes shaders` status distinguishes a deliberate
67
+ * subset (`lecodes shaders compile --platform …`, or a desktop build's single target) from a cache
68
+ * that is simply stale. Without this, anyone using a narrower set reads a permanent "not compiled"
69
+ * and re-runs a full compile to clear it, undoing the saving. */
70
+ export const shaderCompiledPlatforms = (root: string, text: string, matcId: string): AnyShaderPlatform[] => {
71
+ const hash = shaderHash(text, matcId)
72
+ return artifactFamily().filter((p) => existsSync(join(shadersCacheDir(root), `${hash}_${p}.filamat`)))
73
+ }
74
+
75
+ /** The cached artifact FILE for a shader source + platform, when matc is installed and the
76
+ * artifact was compiled (the scene editor host's `/api/shader` reads it for live material
77
+ * patches). Null otherwise — the caller falls back to building the lite JSON on the fly. */
78
+ export const localShaderArtifact = (root: string, text: string, platform: string): string | null => {
79
+ const matcExe = resolveMatcExeCached()
80
+ if (!matcExe) return null
81
+ const file = join(shadersCacheDir(root), `${shaderHash(text, matcIdentity(matcExe))}_${platform}.filamat`)
82
+ return existsSync(file) ? file : null
83
+ }
84
+
85
+ /** Every platform name the local compile knows, for validating a user-supplied `--platform` list. */
86
+ export const allShaderPlatforms = (): AnyShaderPlatform[] =>
87
+ [...(Object.keys(ALL_PLATFORM_BUILD) as AnyMatcPlatform[]), "webgl-lite"]
88
+
89
+ const runMatc = (exe: string, args: string[]): Promise<{ ok: boolean, stderr: string }> =>
90
+ new Promise((resolve) => {
91
+ execFile(exe, args, { windowsHide: true }, (error, _stdout, stderr) => {
92
+ // error covers both a nonzero exit (compile error) and a failed spawn — the message joins
93
+ // stderr so a missing/undownloadable binary still explains itself.
94
+ const text = String(stderr ?? "") || (error ? error.message : "")
95
+ resolve({ ok: !error, stderr: text })
96
+ })
97
+ })
98
+
99
+ /** matc's stderr names the temp file — rewrite to the project path and cut the trailing noise. */
100
+ const cleanMatcOutput = (err: string, tempName: string, realName: string): string => {
101
+ const output = err.replaceAll("\r", "").replaceAll(tempName, realName)
102
+ const cut = output.indexOf("\n\n")
103
+ return (cut >= 0 ? output.slice(0, cut) : output).trim()
104
+ }
105
+
106
+ /** Artifacts the local compile produced, for a caller that must SHIP them (the desktop host reads
107
+ * them by name from next to the bundle — see `local` mode below). */
108
+ export type StagedShader = { name: string, absPath: string }
109
+
110
+ export type LocalShaderResult = { warnings: string[], artifacts: StagedShader[] }
111
+
112
+ /** The artifacts a native host can actually load. webgl-lite is web-only, so it stays in
113
+ * the cache instead of being staged into a desktop build. `_creator.backend` returns exactly one
114
+ * of these: "desktop" (Windows desktop GL), "desktop-vulkan" (Windows desktop Vulkan),
115
+ * "opengl" (Linux desktop, Android), "metal" (Apple).
116
+ *
117
+ * NATIVE_PLATFORMS is the default when the caller does NOT know which host will run the bundle (a
118
+ * generic `preview --local`): every native artifact ships and the runtime picks. It deliberately
119
+ * omits "desktop-vulkan" — that host is opt-in, and shipping ~293 KB per shader to every unknown
120
+ * target for it would be a poor trade. A caller that DOES know — every desktop path, which stages
121
+ * the very exe that will run — passes `platforms` and ships exactly one, which is where
122
+ * desktop-vulkan gets staged. ALL_NATIVE_PLATFORMS is the set such a request is intersected with,
123
+ * i.e. "which of the requested platforms is a native host able to load at all". */
124
+ const NATIVE_PLATFORMS: AnyMatcPlatform[] = ["opengl", "metal", "desktop"]
125
+ const ALL_NATIVE_PLATFORMS: AnyMatcPlatform[] = ["opengl", "metal", "desktop", "desktop-vulkan"]
126
+
127
+ /** The single artifact a desktop host asks for, by (target OS, renderer) — the same mapping
128
+ * `js_getBackend` implements in creator-pkg. Linux desktop runs GLES and reports "opengl"; the
129
+ * macOS host (the Apple repo's LeCodes Desktop.app) is Filament-on-Metal and reports "metal" —
130
+ * staging "opengl" there leaves every custom material failing to load at the first scene. */
131
+ export const desktopShaderPlatform = (
132
+ platform: NodeJS.Platform, renderer: "gl" | "vulkan",
133
+ ): AnyMatcPlatform =>
134
+ platform === "win32" ? (renderer === "vulkan" ? "desktop-vulkan" : "desktop")
135
+ : platform === "darwin" ? "metal"
136
+ : "opengl"
137
+
138
+ /**
139
+ * Compile the project's shaders into the local cache and point each entry's `shaders` list at the
140
+ * results. Two modes:
141
+ * - URL mode (`srcPrefix`, the dev server): srcs become `<srcPrefix>/<name>`, served over HTTP.
142
+ * - `local` mode (hosts with no asset server — the desktop host): srcs become bare artifact
143
+ * names, `shadersLocal` marks the entry, and the returned artifacts must be staged next to
144
+ * the bundle so the host's fetchLocal finds them.
145
+ * Entries that already carry artifacts are skipped in URL mode (the server compiled them); in
146
+ * local mode they are recompiled, since a local-assets run reads the working tree by definition.
147
+ * Returns warnings (matc unavailable → the entry keeps whatever it had); throws CliError on an
148
+ * actual matc compile error (a broken .mat fails the compile like a TS error).
149
+ *
150
+ * `platforms` narrows the set compiled AND staged — pass it only when the target host is known with
151
+ * certainty (every desktop path: the exe is bundled or spawned by the same command). Leave it off
152
+ * for `lecodes dev`, which feeds a phone, a browser and the desktop host from one bundle and cannot
153
+ * know which will connect. Compiling is incremental per artifact, so a narrow run and a later full
154
+ * one add up instead of redoing each other's work.
155
+ */
156
+ export const applyLocalShaders = async (
157
+ entries: CompileEntry[],
158
+ opts: { root: string, srcPrefix?: string, local?: boolean, platforms?: AnyShaderPlatform[] },
159
+ ): Promise<LocalShaderResult> => {
160
+ const shaderEntries = entries.filter((e) =>
161
+ e.type === "shader" && e.absPath && (opts.local || !e.shaders?.length))
162
+ if (shaderEntries.length === 0) return { warnings: [], artifacts: [] }
163
+
164
+ let matcExe: string
165
+ try {
166
+ matcExe = await resolveMatcExe()
167
+ } catch (e) {
168
+ const reason = e instanceof CliError ? e.message : String(e)
169
+ return { warnings: [`Local shader compile unavailable (${reason})\n Shaders fall back to the server — push the project once for them to work.`], artifacts: [] }
170
+ }
171
+ const matcId = matcIdentity(matcExe)
172
+
173
+ const cacheDir = shadersCacheDir(opts.root)
174
+ mkdirSync(cacheDir, { recursive: true })
175
+ const artifacts: StagedShader[] = []
176
+
177
+ for (const entry of shaderEntries) {
178
+ const text = readFileSync(entry.absPath!, "utf8")
179
+ const hash = shaderHash(text, matcId)
180
+ const family = artifactFamily()
181
+ const artifact = (platform: string) => `${hash}_${platform}.filamat`
182
+ const has = (platform: string) => existsSync(join(cacheDir, artifact(platform)))
183
+
184
+ // What this run is responsible for: the whole family, or the caller's subset intersected with it.
185
+ const wanted = opts.platforms ? family.filter((p) => opts.platforms!.includes(p)) : family
186
+
187
+ // Compile only what is MISSING. Per-artifact rather than all-or-nothing so a narrow run
188
+ // (`--platform`, a desktop build) and a later full one add up instead of redoing each other's
189
+ // work. The old whole-family gate existed so a run that died midway could not serve a partial
190
+ // set — that property is preserved by each artifact being written-or-removed individually
191
+ // (the rmSync on failure below), which is what actually matters.
192
+ const missing = wanted.filter((p) => !has(p))
193
+ const matcMissing = missing.filter((p): p is AnyMatcPlatform => p !== "webgl-lite")
194
+ if (matcMissing.length > 0) {
195
+ // Temp source in tmpdir, rewritten per platform for the samplerExternal → sampler2d targets.
196
+ const tempName = `lecodes-matc-${randomBytes(6).toString("hex")}.mat`
197
+ const tempFile = join(tmpdir(), tempName)
198
+ try {
199
+ for (const platform of matcMissing) {
200
+ const build = ALL_PLATFORM_BUILD[platform]
201
+ writeFileSync(tempFile, build.replaceExternal ? text.replace(externalSamplerReg, "type:sampler2d") : text)
202
+ const out = join(cacheDir, artifact(platform))
203
+ const { ok, stderr } = await runMatc(matcExe, matcArgs(platform, out, tempFile))
204
+ if (!ok) {
205
+ rmSync(out, { force: true })
206
+ throw new CliError(`${entry.path}: shader compile failed [${platform}]\n${cleanMatcOutput(stderr, tempName, basename(entry.path))}`)
207
+ }
208
+ }
209
+ } finally {
210
+ rmSync(tempFile, { force: true })
211
+ }
212
+ }
213
+ // Web Lite artifact — JSON, not a filamat package (viewer-lite wraps the parsed fragment in
214
+ // its own GLSL template). buildLiteMaterialArtifact never throws — an exotic material becomes
215
+ // an `unsupported` artifact and the lite runtime falls back to unlit.
216
+ if (missing.includes("webgl-lite")) {
217
+ writeFileSync(join(cacheDir, artifact("webgl-lite")), JSON.stringify(buildLiteMaterialArtifact(text)))
218
+ }
219
+
220
+ // opengl first — its src is the template the bundle substitutes the runtime backend into.
221
+ if (opts.local) {
222
+ // Stage the native artifacts the target can ask for: the caller's set when it knows which host
223
+ // will run (one, for a desktop build), else every native one. `_creator.backend` picks at
224
+ // runtime, so shipping more than one is only ever wasted bytes — never a correctness need.
225
+ const staged = opts.platforms
226
+ ? ALL_NATIVE_PLATFORMS.filter((p) => opts.platforms!.includes(p))
227
+ : NATIVE_PLATFORMS
228
+ // opengl must lead: compileProject derives the runtime name by substituting the backend into
229
+ // shaders[0]'s "_opengl". A staged set without it (a Windows desktop build) still needs the
230
+ // template, so synthesise the entry from the opengl NAME even when that file isn't shipped.
231
+ entry.shaders = [
232
+ { platform: "opengl", src: artifact("opengl") },
233
+ ...staged.filter((p) => p !== "opengl").map((platform) => ({ platform, src: artifact(platform) })),
234
+ ]
235
+ entry.shadersLocal = true
236
+ for (const platform of staged) {
237
+ artifacts.push({ name: artifact(platform), absPath: join(cacheDir, artifact(platform)) })
238
+ }
239
+ } else {
240
+ const order = ["opengl", "metal", "desktop", "webgl-lite"]
241
+ entry.shaders = order.map((platform) => ({ platform, src: `${opts.srcPrefix}/${artifact(platform)}` }))
242
+ }
243
+ }
244
+ return { warnings: [], artifacts }
245
+ }
package/src/config.ts ADDED
@@ -0,0 +1,42 @@
1
+ import { homedir } from "node:os"
2
+ import { join } from "node:path"
3
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs"
4
+ import { CliError } from "./util"
5
+
6
+ /* Global CLI config in ~/.lecodes/config.json: the server URL + the personal access token.
7
+ * Env vars LECODES_API / LECODES_TOKEN override the stored values (handy for CI). */
8
+
9
+ export type Config = { apiUrl?: string, token?: string }
10
+
11
+ export const DEFAULT_API = "https://le.codes"
12
+
13
+ const CONFIG_DIR = join(homedir(), ".lecodes")
14
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json")
15
+
16
+ export const loadConfig = (): Config => {
17
+ let stored: Config = {}
18
+ try { stored = JSON.parse(readFileSync(CONFIG_FILE, "utf8")) } catch { /* none yet */ }
19
+ return {
20
+ apiUrl: process.env.LECODES_API ?? stored.apiUrl,
21
+ token: process.env.LECODES_TOKEN ?? stored.token,
22
+ }
23
+ }
24
+
25
+ export const saveConfig = (config: Config) => {
26
+ mkdirSync(CONFIG_DIR, { recursive: true })
27
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2))
28
+ }
29
+
30
+ /** Strip a trailing slash so apiUrl + "/api" / apiUrl + src compose cleanly. */
31
+ export const normalizeApiUrl = (url: string) => url.trim().replace(/\/+$/, "")
32
+
33
+ /** The token, or a friendly error telling the user to log in. */
34
+ export const requireToken = (config: Config): string => {
35
+ if (!config.token) throw new CliError("Not logged in. Run `lecodes login` first.")
36
+ return config.token
37
+ }
38
+
39
+ export const requireApiUrl = (config: Config): string => {
40
+ if (!config.apiUrl) throw new CliError("No server URL configured. Run `lecodes login` first.")
41
+ return normalizeApiUrl(config.apiUrl)
42
+ }
@@ -0,0 +1,35 @@
1
+ import { readFileSync } from "node:fs"
2
+ import { join } from "node:path"
3
+
4
+ /* The design folder's meta.json, read WITHOUT the optional `lecodes-design` package: the CLI needs
5
+ * a screen's board frame in paths that must work on a bare install (`lecodes design snapshot`,
6
+ * `lecodes render <screen>`). Mirrors lecodes-design's `metaDefaultSize` validation — duplicated
7
+ * on purpose, since the design package is an optional peer. */
8
+
9
+ export type ScreenMeta = { size?: [number, number] }
10
+ export type MetaFrames = { screens: Record<string, ScreenMeta>, defaultSize: [number, number] }
11
+
12
+ export const PHONE_SIZE: [number, number] = [390, 844]
13
+
14
+ /** Per-screen sizes + the board default frame (meta.defaultSize — a desktop design sets
15
+ * [1280, 800]). A missing/broken meta.json degrades to the phone frame. */
16
+ export const readMetaFrames = (designDir: string): MetaFrames => {
17
+ try {
18
+ const meta = JSON.parse(readFileSync(join(designDir, "meta.json"), "utf8"))
19
+ const d = meta?.defaultSize
20
+ const valid = Array.isArray(d) && d.length === 2 &&
21
+ d.every((n: unknown) => typeof n === "number" && Number.isFinite(n) && n >= 64 && n <= 8192)
22
+ return {
23
+ screens: meta?.screens ?? {},
24
+ defaultSize: valid ? [Math.round(d[0]), Math.round(d[1])] : PHONE_SIZE,
25
+ }
26
+ } catch {
27
+ return { screens: {}, defaultSize: PHONE_SIZE }
28
+ }
29
+ }
30
+
31
+ /** The frame a screen renders at: its own `size` in meta.json, else the board default. */
32
+ export const screenFrame = (designDir: string, screenId: string): [number, number] => {
33
+ const frames = readMetaFrames(designDir)
34
+ return frames.screens[screenId]?.size ?? frames.defaultSize
35
+ }