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,191 @@
1
+ // Types for the optional `lecodes-design` package's server entry (lazy-loaded by the `design`
2
+ // command). The real package is installed separately; this lets the dynamic import typecheck
3
+ // without it present.
4
+ declare module "lecodes-design/server" {
5
+ export type CompiledScreen = { js: string, warnings: string[] }
6
+
7
+ export type RenderedScreen = { json?: string, png?: Buffer, warnings: string[] }
8
+
9
+ export type DesignServerOptions = {
10
+ root: string
11
+ designDir: string
12
+ name?: string
13
+ compileScreen: (id: string, state?: string) => Promise<CompiledScreen>
14
+ renderScreen?: (id: string, format: "json" | "png" | "both", state?: string) => Promise<RenderedScreen>
15
+ port?: number
16
+ appDir?: string
17
+ log?: (message: string) => void
18
+ /** True when a platform project backs this folder — decides who owns the conversation. */
19
+ linked?: boolean
20
+ }
21
+
22
+ export type DesignServer = {
23
+ port: number
24
+ url: string
25
+ close: () => void
26
+ }
27
+
28
+ export function startDesignServer(options: DesignServerOptions): Promise<DesignServer>
29
+
30
+ export type ArrangeSummary = {
31
+ changed: boolean
32
+ moved: number
33
+ previous: Record<string, [number, number]>
34
+ mode: "full" | "new" | "selection"
35
+ score?: { before: number, after: number }
36
+ }
37
+
38
+ export function arrangeDesign(designDir: string, opts?: { onlyNew?: boolean, only?: string[] }): ArrangeSummary
39
+
40
+ export type DesignScreen = {
41
+ id: string
42
+ file: string
43
+ desc?: string
44
+ pos: [number, number]
45
+ size: [number, number]
46
+ group?: string
47
+ role?: string
48
+ /** State ids discovered from the screen source (≥1). */
49
+ states: string[]
50
+ /** The canonical state (∈ states). */
51
+ canonical: string
52
+ /** Soft: default export has an annotated non-union param (states not discoverable). */
53
+ statesWarning?: string
54
+ }
55
+
56
+ export type DesignEdge = { from: string, to: string, label?: string, activator?: string }
57
+ export type ServedEdge = {
58
+ from: string
59
+ to: string
60
+ label?: string
61
+ activator?: string
62
+ index: number
63
+ fromState?: string
64
+ toState?: string
65
+ }
66
+ export type DesignGroup = { name: string, color: string, desc?: string, members: string[], impliedBy?: string }
67
+
68
+ /** A source-discovered tab bar (`defineTabs` in shared/ + mount callsites) — see tabbar-surface.md. */
69
+ export type DesignHub = {
70
+ name: string
71
+ file: string
72
+ /** Tab ids in declaration order (root screen ids). */
73
+ tabs: string[]
74
+ /** screen id → tab id for every screen that mounts the bar. */
75
+ lanes: Record<string, string>
76
+ }
77
+
78
+ export type DesignState = {
79
+ screens: DesignScreen[]
80
+ edges: ServedEdge[]
81
+ groups: DesignGroup[]
82
+ hubs: DesignHub[]
83
+ warnings: string[]
84
+ }
85
+
86
+ export function readDesignState(designDir: string): DesignState
87
+
88
+ export type ConceptReport = {
89
+ hasSpec: boolean
90
+ screenRefs: { id: string, exists: boolean }[]
91
+ screensWithoutRole: string[]
92
+ warnings: string[]
93
+ }
94
+
95
+ export function conceptReport(designDir: string, screens: DesignScreen[]): ConceptReport
96
+
97
+ /** Provenance: `share` = unverified stranger on the link, `ai` = Claude on a member's behalf. */
98
+ export type CommentOrigin = "share" | "editor" | "cli" | "ai"
99
+ /** A review comment pinned to the board (docs/design-comments-plan.md). */
100
+ export type CommentMessage = {
101
+ id: string
102
+ author: string
103
+ origin: CommentOrigin
104
+ at: string
105
+ body: string
106
+ editedAt?: string
107
+ mine?: boolean
108
+ }
109
+ /** What the pin was dropped on, captured from the laid-out tree at pin time. `name` is the author
110
+ * `name` of the nearest named ancestor, `text` what it read; `path` is a weak positional hint. */
111
+ export type CommentTarget = { name?: string, text?: string, path?: string }
112
+ export type CommentAnchor = {
113
+ screen?: string
114
+ state?: string
115
+ target?: CommentTarget
116
+ point?: [number, number]
117
+ }
118
+ export type CommentThread = {
119
+ id: string
120
+ /** Absent until the platform allocates one — a thread started locally is a draft until pushed. */
121
+ number?: number
122
+ anchor: CommentAnchor
123
+ resolved: boolean
124
+ resolvedAt?: string | null
125
+ createdAt: string
126
+ updatedAt: string
127
+ commitId?: number | null
128
+ /** Set here and not yet sent; `lecodes push` delivers it. */
129
+ pendingResolve?: true
130
+ messages: CommentMessage[]
131
+ mine?: boolean
132
+ }
133
+
134
+ /** Read `design/comments/` — one file per thread (absent directory → no threads). */
135
+ export function listDesignComments(designDir: string): {
136
+ threads: CommentThread[]
137
+ canComment: boolean
138
+ }
139
+
140
+ /** Start a thread. Linked → a numberless draft `lecodes push` names; standalone → numbered now. */
141
+ export function startThread(
142
+ designDir: string,
143
+ draft: { anchor: CommentAnchor, body: string, author?: string, origin?: CommentOrigin },
144
+ mode: { linked: boolean },
145
+ ): CommentThread
146
+ /** Append a reply (marked pending until `lecodes push`). `as` overrides attribution — pass
147
+ * Claude/ai when the assistant, not the developer, is writing. */
148
+ export function replyToThread(
149
+ designDir: string,
150
+ threadId: string,
151
+ body: string,
152
+ as?: { author?: string, origin?: CommentOrigin },
153
+ ): CommentThread
154
+ /** Flip resolved (marked pending until `lecodes push`). */
155
+ export function resolveThread(designDir: string, threadId: string, resolved: boolean): CommentThread
156
+
157
+ export type MirrorSummary = { added: number, updated: number, closed: number }
158
+ /** Fold the platform's OPEN threads into `design/comments/`. A local file the snapshot doesn't
159
+ * carry was resolved or deleted upstream and is removed — unless it still has unsent writes. */
160
+ export function mirrorThreads(designDir: string, remote: CommentThread[]): MirrorSummary
161
+
162
+ export type QueuedThread = {
163
+ thread: CommentThread
164
+ file: string
165
+ pending: { newThread: boolean, messages: CommentMessage[], resolve: boolean }
166
+ }
167
+ /** Everything waiting to go up. Nothing leaves the machine except through `lecodes push`, which
168
+ * drains exactly this. */
169
+ export function queuedThreads(designDir: string): QueuedThread[]
170
+ /** What a push actually delivered for one queued thread (`messageIds` holds LOCAL message ids). */
171
+ export type DeliveredWrites = { messageIds: Set<string>, resolve: boolean }
172
+ /** Adopt what the platform says the thread is now. Only what `delivered` names settles — the
173
+ * rest keeps its pending flag, so a half-failed push retries instead of duplicating. */
174
+ export function settleQueued(
175
+ designDir: string,
176
+ queued: QueuedThread,
177
+ upstream: CommentThread,
178
+ delivered: DeliveredWrites,
179
+ ): void
180
+
181
+ /** How a thread is referred to: "#7", or "new" while it's an unpushed draft. */
182
+ export function threadRef(thread: CommentThread): string
183
+ /** The pre-directory `comments.json`, if it's still lying around. */
184
+ export function legacyCommentsFile(designDir: string): string | null
185
+
186
+ export type ScreenStates = { states: string[], canonical: string, warning?: string, error?: string }
187
+ /** Discover a screen's states from its source (the default export's first-param literal union). */
188
+ export function discoverScreenStates(source: string): ScreenStates
189
+ /** Split a `screen@state` reference on the last `@` (no `@` → no state). */
190
+ export function parseStateRef(ref: string): { screen: string, state?: string }
191
+ }
@@ -0,0 +1,131 @@
1
+ // Types for the optional `lecodes-renderer` package's node headless entry (lazy-loaded by the
2
+ // `render` command). The real package is installed separately; this lets the dynamic import
3
+ // typecheck without it present.
4
+ declare module "lecodes-renderer/headless" {
5
+ export type RenderOptions = {
6
+ width?: number
7
+ height?: number
8
+ images?: Record<string, { width: number, height: number }>
9
+ /** Max ms to wait for promises/timers/fetch/image decode before snapshotting (ends early on idle). */
10
+ settleMs?: number
11
+ /** Let data fetches hit the real network (off by default). */
12
+ waitNetwork?: boolean
13
+ /** Resolve `lecodesfile://<abs-path>` resource URLs from the local filesystem (the `render`
14
+ * command emits these for a local project's assets). */
15
+ localAssets?: boolean
16
+ /** Render at this animation time (ms); 0 = the initial frame-0 state. */
17
+ timeMs?: number
18
+ /** Advance animations to their resting state before snapshotting (default true). */
19
+ afterAnimations?: boolean
20
+ /** Override safe-area insets [top, right, bottom, left] (px) for the `safe-*` keywords. */
21
+ safeArea?: [number, number, number, number]
22
+ /** Receive the project's console.* output (unset → swallowed). */
23
+ onConsole?: (level: string, text: string) => void
24
+ /** Canned `_creatorUtils.fetch` responses (URL pattern → { status?, json?, text? }); they beat
25
+ * every other fetch path, including waitNetwork. */
26
+ fixtures?: Fixtures
27
+ }
28
+ export type FetchFixture = { status?: number, json?: unknown, text?: string }
29
+ export type Fixtures = Record<string, FetchFixture>
30
+ export function validateFixtures(fixtures: Fixtures): void
31
+ export type RenderResult = { text: string, tree: unknown, warnings: string[] }
32
+ export function renderToJson(js: string, opts?: RenderOptions): Promise<RenderResult>
33
+
34
+ /** A frame region in logical px: a named node (its layout rect) or an explicit rect. */
35
+ export type ClipTarget = string | { x: number, y: number, width: number, height: number }
36
+ export type PngOptions = RenderOptions & { scale?: number, clip?: ClipTarget }
37
+ export type PngResult = { png: Buffer, width: number, height: number, warnings: string[] }
38
+ export function renderToPng(js: string, opts?: PngOptions): Promise<PngResult>
39
+
40
+ // ---- top-down map (lecodes render --map) ----
41
+ export type MapRenderOptions = { maxPx?: number, title?: string, labelAll?: boolean }
42
+ export function renderToMap(
43
+ js: string, opts?: RenderOptions & MapRenderOptions,
44
+ ): Promise<{ png: Buffer, data: unknown, warnings: string[] }>
45
+
46
+ // ---- interactive session (tap/type/scroll/wait between snapshots) ----
47
+ export type SessionTarget = string | { x: number, y: number }
48
+ export type SessionOptions = RenderOptions & {
49
+ /** Which safe-area edges are exact-height system bars (Android) vs clearance insets (iOS). */
50
+ safeAreaBars?: ("top" | "right" | "bottom" | "left")[]
51
+ /** true → require @napi-rs/canvas (throw if missing); false → JSON metrics only (no png());
52
+ * "auto" (default) → rasterize when the package is installed. */
53
+ raster?: boolean | "auto"
54
+ /** Device-pixel scale for png() (default 2). */
55
+ scale?: number
56
+ }
57
+ export type Session = {
58
+ warnings: string[]
59
+ canRasterize: boolean
60
+ find: (target: string) => unknown | null
61
+ tap: (target: SessionTarget) => Promise<void>
62
+ type: (target: SessionTarget, text: string) => Promise<void>
63
+ fill: (value?: string) => Promise<void>
64
+ scroll: (delta: number, target?: SessionTarget) => Promise<void>
65
+ wait: (ms: number) => Promise<void>
66
+ frames: (n: number) => Promise<void>
67
+ drag: (from: SessionTarget, to: SessionTarget, opts?: { ms?: number }) => Promise<void>
68
+ hold: (target: SessionTarget, ms: number) => Promise<void>
69
+ keyDown: (code: string) => void
70
+ keyUp: (code: string) => void
71
+ key: (code: string, ms: number) => Promise<void>
72
+ settle: (ms?: number) => Promise<void>
73
+ json: () => unknown
74
+ text: () => string
75
+ png: (opts?: { clip?: ClipTarget }) => Promise<Buffer>
76
+ clipRect: (clip: ClipTarget) => { x: number, y: number, width: number, height: number }
77
+ logs: () => string[]
78
+ screenName: () => string | null
79
+ value: (target: string) => string
80
+ clock: () => number
81
+ dispose: () => void
82
+ }
83
+ export function createSession(js: string, opts?: SessionOptions): Promise<Session>
84
+
85
+ // ---- scenario engine (lecodes render --script / lecodes test) ----
86
+ export type ExpectSpec = {
87
+ text?: string
88
+ notText?: string
89
+ node?: string
90
+ notNode?: string
91
+ screen?: string
92
+ value?: { target: string, equals: string }
93
+ log?: string
94
+ }
95
+ export type ScenarioStep = {
96
+ name?: string
97
+ tap?: SessionTarget
98
+ type?: string
99
+ text?: string
100
+ fill?: { value?: string }
101
+ scroll?: string | number
102
+ by?: number
103
+ wait?: number
104
+ frames?: number
105
+ drag?: SessionTarget
106
+ to?: SessionTarget
107
+ ms?: number
108
+ hold?: SessionTarget
109
+ key?: string
110
+ keyDown?: string
111
+ keyUp?: string
112
+ /** 3D inspection camera (scene3d report): named auto-fitted view, fixed eye/look-at, or "off". */
113
+ view?: "iso" | "top" | "front" | "side" | "back" | "off"
114
+ fov?: number
115
+ camera?: { at: [number, number, number], lookAt: [number, number, number], fov?: number } | "off"
116
+ expect?: ExpectSpec
117
+ screenshot?: string
118
+ clip?: ClipTarget
119
+ json?: string
120
+ /** Write the annotated top-down map of the 3D scene: `<path>.png` + `<path>.json` data. */
121
+ map?: string
122
+ }
123
+ export type ScenarioIO = { write: (relPath: string, data: string | Uint8Array) => void }
124
+ export type StepOutcome = { index: number, desc: string, ok: boolean, error?: string }
125
+ export type ScenarioOutcome = { ok: boolean, steps: StepOutcome[], failureArtifacts: string[] }
126
+ export function parseScenarioSteps(raw: unknown): ScenarioStep[]
127
+ export function describeStep(step: ScenarioStep): string
128
+ export function runScenario(
129
+ session: Session, steps: ScenarioStep[], io: ScenarioIO, opts?: { artifactPrefix?: string },
130
+ ): Promise<ScenarioOutcome>
131
+ }
@@ -0,0 +1,144 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs"
2
+ import { join, relative } from "node:path"
3
+ import { sanitizeAssetName } from "lecodes-sdk/compile/assetName"
4
+ import { sha256, toKey } from "./util"
5
+ import { loadIgnore, type IgnoreMatcher } from "./ignore"
6
+ import type { FileKind, Manifest } from "./manifest"
7
+
8
+ /* Scanning the working tree and diffing it against the manifest. Paths are normalized to posix
9
+ * keys ("/a/b.ts") so they line up with the asset tree regardless of OS. */
10
+
11
+ // Directories never treated as project content. tsconfig.json / .gitignore etc. are skipped as
12
+ // files (see SKIP_FILES) since they're CLI/tooling artifacts, not assets.
13
+ const SKIP_DIRS = new Set([".lecodes", ".git", "node_modules"])
14
+ const SKIP_FILES = new Set(["tsconfig.json", "jsconfig.json", ".gitignore", ".lecodesignore", ".DS_Store"])
15
+
16
+ // Extensions stored as text by the platform. ".mat" -> shader, ".scene" -> scene; everything
17
+ // else (images, audio, glb, ...) is a binary resource.
18
+ const TEXT_EXTS = new Set([
19
+ "ts", "tsx", "js", "jsx", "mjs", "cjs", "json", "svg", "glsl", "vert", "frag",
20
+ "txt", "md", "css", "html", "xml", "yml", "yaml", "csv",
21
+ ])
22
+
23
+ const ext = (name: string) => {
24
+ const i = name.lastIndexOf(".")
25
+ return i < 0 ? "" : name.slice(i + 1).toLowerCase()
26
+ }
27
+
28
+ /** Best-effort kind for a file the user added locally (known files keep their manifest type). */
29
+ export const classifyKind = (name: string): FileKind => {
30
+ const e = ext(name)
31
+ if (e === "mat") return "shader"
32
+ if (e === "scene") return "scene"
33
+ return TEXT_EXTS.has(e) ? "text" : "resource"
34
+ }
35
+
36
+ export const isTextKind = (kind: FileKind) => kind !== "resource"
37
+
38
+ export type LocalFile = { path: string, absPath: string, sha: string }
39
+
40
+ export type LocalScan = { files: LocalFile[], dirs: string[], ignored: string[] }
41
+
42
+ export type ScanOptions = {
43
+ /** A `.lecodesignore` matcher; untracked matching files/dirs are skipped (see ignore.ts). */
44
+ ignore?: IgnoreMatcher
45
+ /** Paths already in the project — never ignored, so an ignore rule can't drop an existing asset. */
46
+ tracked?: Set<string>
47
+ }
48
+
49
+ /** Recursively scan the working tree, skipping ignored dirs/files; hashes each file. */
50
+ export const scanLocal = (root: string, opts: ScanOptions = {}): LocalScan => {
51
+ const { ignore, tracked } = opts
52
+ const files: LocalFile[] = []
53
+ const dirs: string[] = []
54
+ const ignored: string[] = []
55
+ const isTracked = (key: string) => tracked?.has(key) ?? false
56
+ // Don't prune a dir that contains a tracked file (e.g. an ignored `assets/` that holds a real asset).
57
+ const hasTrackedUnder = (key: string) => {
58
+ if (!tracked) return false
59
+ const prefix = key + "/"
60
+ for (const t of tracked) if (t.startsWith(prefix)) return true
61
+ return false
62
+ }
63
+ // `parentIgnored`: we descended into an ignored directory only because it holds a tracked file.
64
+ // Everything else inside it stays ignored (gitignore: an ignored dir's contents are ignored even
65
+ // when a single file is force-tracked).
66
+ const walk = (dir: string, parentIgnored: boolean) => {
67
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
68
+ const abs = join(dir, entry.name)
69
+ const key = toKey(relative(root, abs))
70
+ if (entry.isDirectory()) {
71
+ if (SKIP_DIRS.has(entry.name)) continue
72
+ const dirIgnored = parentIgnored || (!!ignore && ignore(key, true))
73
+ if (dirIgnored && !isTracked(key) && !hasTrackedUnder(key)) { ignored.push(key + "/"); continue }
74
+ dirs.push(key)
75
+ walk(abs, dirIgnored)
76
+ } else if (entry.isFile()) {
77
+ if (SKIP_FILES.has(entry.name)) continue
78
+ const fileIgnored = parentIgnored || (!!ignore && ignore(key, false))
79
+ if (fileIgnored && !isTracked(key)) { ignored.push(key); continue }
80
+ files.push({ path: key, absPath: abs, sha: sha256(readFileSync(abs)) })
81
+ }
82
+ }
83
+ }
84
+ walk(root, false)
85
+ return { files, dirs, ignored }
86
+ }
87
+
88
+ /**
89
+ * The file a project key ("/img/logo.png") points at on disk. Keys carry the canonical spelling of
90
+ * each name (toKey), so a file whose real name isn't canonical — an NBSP, a decomposed "й" on a
91
+ * byte-exact filesystem — is found by matching sanitized names in its folder.
92
+ */
93
+ export const resolveLocalPath = (root: string, key: string): string | null => {
94
+ const direct = join(root, key.slice(1))
95
+ if (existsSync(direct)) return direct
96
+ let dir = root
97
+ try {
98
+ for (const name of key.slice(1).split("/")) {
99
+ const match = readdirSync(dir).find(entry => entry === name || sanitizeAssetName(entry) === name)
100
+ if (!match) return null
101
+ dir = join(dir, match)
102
+ }
103
+ } catch {
104
+ return null // a segment that isn't a readable directory
105
+ }
106
+ return dir
107
+ }
108
+
109
+ /** Scan honoring the project's `.lecodesignore` (untracked files only). The common entry point. */
110
+ export const scanProject = (root: string, manifest: Manifest): LocalScan =>
111
+ scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest.files)) })
112
+
113
+ export type Diff = {
114
+ added: LocalFile[]
115
+ modified: LocalFile[]
116
+ deleted: string[]
117
+ /** newPath -> oldPath, detected by matching content of an added file to a deleted one. */
118
+ moves: Map<string, string>
119
+ }
120
+
121
+ /** Compare a local scan against the manifest's recorded state. */
122
+ export const diffLocal = (manifest: Manifest, scan: LocalScan): Diff => {
123
+ const added: LocalFile[] = []
124
+ const modified: LocalFile[] = []
125
+ const localByPath = new Map(scan.files.map(f => [f.path, f]))
126
+
127
+ for (const file of scan.files) {
128
+ const known = manifest.files[file.path]
129
+ if (!known) added.push(file)
130
+ else if (known.sha !== file.sha) modified.push(file)
131
+ }
132
+ const deleted = Object.keys(manifest.files).filter(path => !localByPath.has(path))
133
+
134
+ // Pair each added file with a deleted file of identical content -> a move/rename.
135
+ const moves = new Map<string, string>()
136
+ const claimed = new Set<string>()
137
+ for (const file of added) {
138
+ const from = deleted.find(path => !claimed.has(path) && manifest.files[path].sha === file.sha)
139
+ if (from) { moves.set(file.path, from); claimed.add(from) }
140
+ }
141
+ return { added, modified, deleted, moves }
142
+ }
143
+
144
+ export const hasChanges = (diff: Diff) => diff.added.length + diff.modified.length + diff.deleted.length > 0
@@ -0,0 +1,46 @@
1
+ import { dirname, join } from "node:path"
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"
3
+ import { CliError } from "./util"
4
+
5
+ /* The per-checkout state in <root>/.lecodes/manifest.json: which remote project this is, the
6
+ * head we last synced from (for drift detection), and a path -> { id, type, sha } map used to
7
+ * diff local edits against on push. */
8
+
9
+ export type FileKind = "text" | "shader" | "scene" | "resource"
10
+
11
+ export type ManifestFile = { id: number, type: FileKind, sha: string }
12
+
13
+ export type Manifest = {
14
+ uuid: string
15
+ apiUrl: string
16
+ name: string
17
+ baseHeadId: number | null
18
+ files: Record<string, ManifestFile>
19
+ }
20
+
21
+ export const LECODES_DIR = ".lecodes"
22
+ const manifestPath = (root: string) => join(root, LECODES_DIR, "manifest.json")
23
+
24
+ /** Walk up from `start` to find the project root (the dir holding .lecodes/manifest.json). */
25
+ export const findProjectRoot = (start: string): string => {
26
+ let dir = start
27
+ while (true) {
28
+ if (existsSync(manifestPath(dir))) return dir
29
+ const parent = dirname(dir)
30
+ if (parent === dir) throw new CliError("Not inside a lecodes project (no .lecodes/manifest.json found).")
31
+ dir = parent
32
+ }
33
+ }
34
+
35
+ export const readManifest = (root: string): Manifest => {
36
+ try {
37
+ return JSON.parse(readFileSync(manifestPath(root), "utf8")) as Manifest
38
+ } catch {
39
+ throw new CliError("Could not read .lecodes/manifest.json — is this a lecodes project?")
40
+ }
41
+ }
42
+
43
+ export const writeManifest = (root: string, manifest: Manifest) => {
44
+ mkdirSync(join(root, LECODES_DIR), { recursive: true })
45
+ writeFileSync(manifestPath(root), JSON.stringify(manifest, null, 2))
46
+ }
@@ -0,0 +1,137 @@
1
+ import { existsSync, readdirSync } from "node:fs"
2
+ import { homedir } from "node:os"
3
+ import { join, resolve } from "node:path"
4
+ import { downloadReleaseArchive, fetchReleaseProduct, semverNewer, type ManifestVersion } from "./releases"
5
+ import { CliError, note } from "./util"
6
+
7
+ /*
8
+ * Filament's matc (material compiler) — the tool behind LOCAL shader compile: `lecodes dev`
9
+ * compiles a project's .mat files into the per-platform .filamat artifact family with it
10
+ * (src/compile/shaders.ts), instead of requiring a push so the server compiles them.
11
+ *
12
+ * The binary is NOT shipped with the CLI (most projects have no custom shaders). It downloads on
13
+ * first use from the same S3 release bucket as the desktop renderer (product "matc" in
14
+ * releases/versions.json — published by scripts/release/release-windows.ps1 / release-ubuntu.sh
15
+ * from the desktop build's in-tree filament host tools, so it always matches the engines the
16
+ * hosts ship). Cache: ~/.lecodes/matc/matc-<version>/. LECODES_MATC overrides resolution entirely
17
+ * (local dev: point it at packages/desktop/build/creator-gl/filament/tools/matc/matc.exe).
18
+ *
19
+ * No version pin: a filamat's compatibility is tied to the vendored Filament, which changes
20
+ * rarely — resolution takes the newest cached copy, else downloads `latest`. `lecodes shaders
21
+ * update` moves an existing cache forward after an engine upgrade.
22
+ *
23
+ * Platforms: windows-x64, linux-x64, macos-arm64. On Windows/Linux matc rides the DESKTOP build
24
+ * (filament's host tools build in-tree there). macOS has no such tree — release-macos.sh builds the
25
+ * app from the vendored xcframeworks — so its matc comes from creator-gl's own host-tool build
26
+ * (`out/cmake-release/filament/tools/matc`), which is the same filament tree those xcframeworks are
27
+ * compiled from. Same guarantee, different source; see scripts/release/release-macos.sh.
28
+ *
29
+ * macos-x64 is deliberately absent: the host tools are built for the machine doing the release, and
30
+ * only Apple Silicon is built today (release-macos.sh skips Intel for the same reason).
31
+ */
32
+
33
+ const matcName = () => (process.platform === "win32" ? "matc.exe" : "matc")
34
+
35
+ const platformKey = (): string => {
36
+ if (process.platform === "win32") return "windows-x64"
37
+ if (process.platform === "linux") return "linux-x64"
38
+ if (process.platform === "darwin") {
39
+ // Intel Macs have no published matc (see the platform note above). Rosetta would run an
40
+ // arm64 binary anyway, but publishing a key we never build is how a download turns into a
41
+ // corrupt-archive error instead of a readable one.
42
+ if (process.arch !== "arm64") {
43
+ throw new CliError(
44
+ "matc isn't published for Intel macOS — set LECODES_MATC to a locally built matc " +
45
+ "(creator-gl/out/cmake-release/filament/tools/matc), or push the project once so shaders " +
46
+ "compile on the server.",
47
+ )
48
+ }
49
+ return "macos-arm64"
50
+ }
51
+ throw new CliError(`matc isn't published for ${process.platform}.`)
52
+ }
53
+
54
+ const cacheRoot = () => join(homedir(), ".lecodes", "matc")
55
+
56
+ /** Locate the matc binary inside a cache/stage dir (the archive may or may not nest a folder). */
57
+ const findMatc = (dir: string): string | null => {
58
+ const name = matcName()
59
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
60
+ const p = join(dir, entry.name)
61
+ if (entry.isFile() && entry.name === name) return p
62
+ if (entry.isDirectory()) { const found = findMatc(p); if (found) return found }
63
+ }
64
+ return null
65
+ }
66
+
67
+ /** Versions with a working matc binary in the cache, newest first. */
68
+ export const installedMatcVersions = (): string[] => {
69
+ try {
70
+ return readdirSync(cacheRoot(), { withFileTypes: true })
71
+ .filter((e) => e.isDirectory() && e.name.startsWith("matc-"))
72
+ .map((e) => e.name.slice("matc-".length))
73
+ .filter((v) => findMatc(join(cacheRoot(), `matc-${v}`)) !== null)
74
+ .sort(semverNewer)
75
+ } catch {
76
+ return []
77
+ }
78
+ }
79
+
80
+ export const fetchMatcManifest = (): Promise<{ latest: string, versions: ManifestVersion[] }> =>
81
+ fetchReleaseProduct("matc", "matc (shader compiler)")
82
+
83
+ const downloadMatc = async (version: string, versions: ManifestVersion[], dest: string): Promise<void> => {
84
+ const key = platformKey()
85
+ const file = versions.find((v) => v.version === version)?.files[key]
86
+ if (!file) {
87
+ throw new CliError(
88
+ `matc ${version} [${key}] isn't published — push the project once so shaders compile on the ` +
89
+ `server, or set LECODES_MATC to a locally built matc.`,
90
+ )
91
+ }
92
+ note(`Downloading the shader compiler (matc ${version}, ${(file.size / 1024 / 1024).toFixed(1)} MB)…`)
93
+ await downloadReleaseArchive(file, {
94
+ cacheRoot: cacheRoot(),
95
+ dest,
96
+ validate: (dir) => (findMatc(dir) ? null : "The downloaded archive doesn't contain the matc binary."),
97
+ })
98
+ }
99
+
100
+ /** The matc binary WITHOUT touching the network: LECODES_MATC override → newest cached version →
101
+ * null. `lecodes shaders` status uses this (a status command must not download). */
102
+ export const resolveMatcExeCached = (): string | null => {
103
+ const override = process.env.LECODES_MATC
104
+ if (override) {
105
+ if (!existsSync(override)) throw new CliError(`LECODES_MATC points at "${override}" which doesn't exist.`)
106
+ return resolve(override)
107
+ }
108
+ const cached = installedMatcVersions()[0]
109
+ return cached ? findMatc(join(cacheRoot(), `matc-${cached}`)) : null
110
+ }
111
+
112
+ /** The matc binary: LECODES_MATC override → newest cached version → download the latest release.
113
+ * Throws CliError when unavailable (callers degrade to the push-once server-compile warning). */
114
+ export const resolveMatcExe = async (): Promise<string> => {
115
+ const cached = resolveMatcExeCached()
116
+ if (cached) return cached
117
+ const { latest, versions } = await fetchMatcManifest()
118
+ const dir = join(cacheRoot(), `matc-${latest}`)
119
+ await downloadMatc(latest, versions, dir)
120
+ const exe = findMatc(dir)
121
+ if (!exe) throw new CliError(`matc cache at ${dir} is broken — delete the folder and retry.`)
122
+ return exe
123
+ }
124
+
125
+ /** `lecodes shaders update`: download the newest published matc (cached versions stay; the newest
126
+ * wins resolution). Run it after an engine upgrade so local shader artifacts match the hosts. */
127
+ export const updateMatc = async (): Promise<void> => {
128
+ const { latest, versions } = await fetchMatcManifest()
129
+ const before = installedMatcVersions()[0]
130
+ const dir = join(cacheRoot(), `matc-${latest}`)
131
+ if (existsSync(dir) && findMatc(dir) !== null) {
132
+ note(`Already up to date: matc ${latest}.`)
133
+ return
134
+ }
135
+ await downloadMatc(latest, versions, dir)
136
+ note(before ? `matc updated to ${latest} (was ${before}).` : `matc ${latest} installed.`)
137
+ }