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
package/dist/index.js CHANGED
@@ -389,7 +389,7 @@ design/comments/
389
389
  if (!existsSync2(file))
390
390
  writeFileSync3(file, DEFAULT_IGNORE_FILE);
391
391
  }, DEFAULT_GITIGNORE = `# LeCodes per-checkout state: generated IDE types + this machine's sync cursor
392
- # (.lecodes/manifest.json). Regenerate with \`lecodes types\` / re-attach with \`lecodes link\`.
392
+ # (.lecodes/manifest.json). Regenerate with \`lecodes update\` / re-attach with \`lecodes link\`.
393
393
  .lecodes/
394
394
 
395
395
  # Render / preview outputs and test failure frames
@@ -3177,7 +3177,7 @@ var require_source_map_support = __commonJS((exports, module) => {
3177
3177
 
3178
3178
  // ../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/typescript.js
3179
3179
  var require_typescript = __commonJS((exports, module) => {
3180
- var __dirname = "C:\\Users\\Den\\Desktop\\LeCodes\\lecodes\\node_modules\\.bun\\typescript@5.8.3\\node_modules\\typescript\\lib", __filename = "C:\\Users\\Den\\Desktop\\LeCodes\\lecodes\\node_modules\\.bun\\typescript@5.8.3\\node_modules\\typescript\\lib\\typescript.js";
3180
+ var __dirname = "/Users/den/Desktop/Ingenuity/le-codes/node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib", __filename = "/Users/den/Desktop/Ingenuity/le-codes/node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/typescript.js";
3181
3181
  /*! *****************************************************************************
3182
3182
  Copyright (c) Microsoft Corporation. All rights reserved.
3183
3183
  Licensed under the Apache License, Version 2.0 (the "License"); you may not use
@@ -171654,10 +171654,7 @@ var init_detectEntry = __esm(() => {
171654
171654
  });
171655
171655
 
171656
171656
  // ../sdk/src/compile/shaderTargets.ts
171657
- var externalSamplerReg, PLATFORM_BUILD, LOCAL_PLATFORM_BUILD, ALL_PLATFORM_BUILD, LOCAL_ONLY_PLATFORMS, TARGET_MATRIX_GENERATION = 1, matcPlatformsFor = (text) => {
171658
- externalSamplerReg.lastIndex = 0;
171659
- return externalSamplerReg.test(text) ? ["opengl", "metal", "webgl", "desktop"] : ["opengl", "metal", "desktop"];
171660
- }, matcArgs = (platform, outPath, inPath) => {
171657
+ var externalSamplerReg, PLATFORM_BUILD, LOCAL_PLATFORM_BUILD, ALL_PLATFORM_BUILD, LOCAL_ONLY_PLATFORMS, TARGET_MATRIX_GENERATION = 1, MATC_PLATFORMS, matcArgs = (platform, outPath, inPath) => {
171661
171658
  const build = ALL_PLATFORM_BUILD[platform];
171662
171659
  return ["-p", build.profile, "--no-essl1", "-a", build.api, "-o", outPath, inPath];
171663
171660
  };
@@ -171666,7 +171663,6 @@ var init_shaderTargets = __esm(() => {
171666
171663
  PLATFORM_BUILD = {
171667
171664
  opengl: { profile: "mobile", api: "opengl", replaceExternal: false },
171668
171665
  metal: { profile: "mobile", api: "metal", replaceExternal: false },
171669
- webgl: { profile: "mobile", api: "opengl", replaceExternal: true },
171670
171666
  desktop: { profile: "desktop", api: "opengl", replaceExternal: true }
171671
171667
  };
171672
171668
  LOCAL_PLATFORM_BUILD = {
@@ -171674,6 +171670,7 @@ var init_shaderTargets = __esm(() => {
171674
171670
  };
171675
171671
  ALL_PLATFORM_BUILD = { ...PLATFORM_BUILD, ...LOCAL_PLATFORM_BUILD };
171676
171672
  LOCAL_ONLY_PLATFORMS = ["desktop-vulkan"];
171673
+ MATC_PLATFORMS = ["opengl", "metal", "desktop"];
171677
171674
  });
171678
171675
 
171679
171676
  // ../sdk/src/compile/liteMaterial.ts
@@ -172189,8 +172186,6 @@ var compileProject = async (opts) => {
172189
172186
  const name = shaders[0].src.split(/[\\/]/).pop() ?? "";
172190
172187
  const [prefix, suffix = ".filamat"] = name.split("_opengl");
172191
172188
  files[e.path] = `export default ("id:" + _creatorUtils.fetchLocal(${JSON.stringify(prefix + "_")} + _creator.backend + ${JSON.stringify(suffix)}))`;
172192
- } else if (shaders.length > 2) {
172193
- files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.subBackend ?? _creator.backend}")}\``;
172194
172189
  } else if (shaders.length > 0) {
172195
172190
  files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.backend}")}\``;
172196
172191
  } else {
@@ -172198,6 +172193,8 @@ var compileProject = async (opts) => {
172198
172193
  }
172199
172194
  } else if (e.path.toLowerCase().endsWith(".svg")) {
172200
172195
  files[e.path] = `export default SvgSource(\`${e.text ?? ""}\`)`;
172196
+ } else if (e.path.toLowerCase().endsWith(".json")) {
172197
+ files[e.path] = `export default ${e.text?.trim() || "null"}`;
172201
172198
  } else {
172202
172199
  files[e.path] = e.text ?? "";
172203
172200
  }
@@ -173137,7 +173134,6 @@ __export(exports_compile, {
173137
173134
  parseSfnt: () => parseSfnt,
173138
173135
  parseJsonish: () => parseJsonish,
173139
173136
  offsetSourceMap: () => offsetSourceMap,
173140
- matcPlatformsFor: () => matcPlatformsFor,
173141
173137
  matcArgs: () => matcArgs,
173142
173138
  loadSceneHarness: () => loadSceneHarness,
173143
173139
  isServerPath: () => isServerPath,
@@ -173168,6 +173164,7 @@ __export(exports_compile, {
173168
173164
  SCENE_HARNESS_DIR: () => SCENE_HARNESS_DIR,
173169
173165
  SCENE_ENTRY_PATH: () => SCENE_ENTRY_PATH,
173170
173166
  PLATFORM_BUILD: () => PLATFORM_BUILD,
173167
+ MATC_PLATFORMS: () => MATC_PLATFORMS,
173171
173168
  LOCAL_PLATFORM_BUILD: () => LOCAL_PLATFORM_BUILD,
173172
173169
  LOCAL_ONLY_PLATFORMS: () => LOCAL_ONLY_PLATFORMS,
173173
173170
  ICON_ID: () => ICON_ID,
@@ -173198,7 +173195,13 @@ var init_compile = __esm(() => {
173198
173195
  import { existsSync as existsSync7 } from "node:fs";
173199
173196
  import { createRequire as createRequire3 } from "node:module";
173200
173197
  import { dirname as dirname5, join as join9 } from "node:path";
173201
- var vendoredInject, resolveChiselBin2 = () => {
173198
+ var resolveSdkInject = () => {
173199
+ try {
173200
+ return createRequire3(import.meta.url).resolve("lecodes-sdk/inject");
173201
+ } catch {}
173202
+ const vendored = join9(distRoot, "runtime", "sdk", "inject.ts");
173203
+ return existsSync7(vendored) ? vendored : null;
173204
+ }, sdkInject, resolveChiselBin2 = () => {
173202
173205
  if (process.env.LECODES_CHISEL_BIN)
173203
173206
  return process.env.LECODES_CHISEL_BIN;
173204
173207
  try {
@@ -173230,9 +173233,9 @@ var vendoredInject, resolveChiselBin2 = () => {
173230
173233
  var init_projectCompile = __esm(() => {
173231
173234
  init_distRoot();
173232
173235
  init_compile();
173233
- vendoredInject = join9(distRoot, "runtime", "sdk", "inject.ts");
173234
- if (existsSync7(vendoredInject))
173235
- process.env.LECODES_SDK_INJECT = vendoredInject;
173236
+ sdkInject = resolveSdkInject();
173237
+ if (sdkInject)
173238
+ process.env.LECODES_SDK_INJECT = sdkInject;
173236
173239
  chiselBin = resolveChiselBin2();
173237
173240
  if (chiselBin)
173238
173241
  process.env.LECODES_CHISEL_BIN = chiselBin;
@@ -214124,25 +214127,26 @@ var init_esm30 = __esm(() => {
214124
214127
 
214125
214128
  // src/types.ts
214126
214129
  import { dirname as dirname13, join as join22, relative as relative3, sep } from "node:path";
214127
- import { existsSync as existsSync21, mkdirSync as mkdirSync15, readdirSync as readdirSync10, readFileSync as readFileSync18, rmSync as rmSync8, statSync as statSync8, writeFileSync as writeFileSync17 } from "node:fs";
214130
+ import { existsSync as existsSync21, mkdirSync as mkdirSync15, readdirSync as readdirSync11, readFileSync as readFileSync18, rmSync as rmSync8, statSync as statSync9, writeFileSync as writeFileSync17 } from "node:fs";
214131
+ import { createRequire as createRequire4 } from "node:module";
214128
214132
  var TSCONFIG, listBundleFiles = (dir, base = dir) => {
214129
214133
  if (!existsSync21(dir))
214130
214134
  return [];
214131
214135
  const out = [];
214132
- for (const name of readdirSync10(dir)) {
214136
+ for (const name of readdirSync11(dir)) {
214133
214137
  const path2 = join22(dir, name);
214134
- if (statSync8(path2).isDirectory())
214138
+ if (statSync9(path2).isDirectory())
214135
214139
  out.push(...listBundleFiles(path2, base));
214136
214140
  else
214137
214141
  out.push(relative3(base, path2).split(sep).join("/"));
214138
214142
  }
214139
214143
  return out;
214140
214144
  }, pruneEmptyDirs = (dir, root) => {
214141
- if (!existsSync21(dir) || !statSync8(dir).isDirectory())
214145
+ if (!existsSync21(dir) || !statSync9(dir).isDirectory())
214142
214146
  return;
214143
- for (const name of readdirSync10(dir))
214147
+ for (const name of readdirSync11(dir))
214144
214148
  pruneEmptyDirs(join22(dir, name), root);
214145
- if (dir !== root && readdirSync10(dir).length === 0)
214149
+ if (dir !== root && readdirSync11(dir).length === 0)
214146
214150
  rmSync8(dir, { recursive: true });
214147
214151
  }, writeTypeBundle = (root, bundle) => {
214148
214152
  const typesDir = join22(root, LECODES_DIR, "types");
@@ -214174,8 +214178,16 @@ var TSCONFIG, listBundleFiles = (dir, base = dir) => {
214174
214178
  bundle[path2] = readFileSync18(join22(typesDir, path2), "utf8");
214175
214179
  return bundle;
214176
214180
  }, materializeTypes = async (root, apiUrl) => writeTypeBundle(root, await getTypes(apiUrl)), readLocalTypeBundle = () => {
214181
+ const fromPackage = (() => {
214182
+ try {
214183
+ return createRequire4(import.meta.url).resolve("lecodes-sdk/types.json");
214184
+ } catch {
214185
+ return null;
214186
+ }
214187
+ })();
214177
214188
  const candidates = [
214178
214189
  join22(distRoot, "runtime", "sdk-types.json"),
214190
+ ...fromPackage ? [fromPackage] : [],
214179
214191
  join22(distRoot, "..", "sdk", "dist", "types.json")
214180
214192
  ];
214181
214193
  for (const path2 of candidates) {
@@ -214220,13 +214232,13 @@ var init_types4 = __esm(() => {
214220
214232
 
214221
214233
  // src/compile/designCompile.ts
214222
214234
  import { readFileSync as readFileSync19 } from "node:fs";
214223
- import { basename as basename9 } from "node:path";
214235
+ import { basename as basename10 } from "node:path";
214224
214236
  var LOCAL_ASSET_SCHEME = "lecodesfile://", compileDesignScreen = async (opts) => {
214225
214237
  const { root, manifest, designDir, designKey, screenId, state } = opts;
214226
214238
  const entries = [];
214227
214239
  const scan2 = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest?.files ?? {})) });
214228
214240
  for (const file of scan2.files) {
214229
- const type = manifest?.files[file.path]?.type ?? classifyKind(basename9(file.path));
214241
+ const type = manifest?.files[file.path]?.type ?? classifyKind(basename10(file.path));
214230
214242
  if (type === "resource") {
214231
214243
  entries.push({
214232
214244
  path: file.path,
@@ -214358,7 +214370,7 @@ var init_browserAuth = __esm(() => {
214358
214370
  init_util();
214359
214371
  });
214360
214372
 
214361
- // ../lecodes-design/src/core/concept.ts
214373
+ // ../../../lecodes-tools/packages/lecodes-design/src/core/concept.ts
214362
214374
  var SPEC_TEMPLATE = `# Concept
214363
214375
 
214364
214376
  The narrative half of this design. \`meta.json\` + the canvas hold the *visual/flow* half (screens,
@@ -214394,7 +214406,7 @@ Behaviour that isn't visible in a single screen — validation, permissions, not
214394
214406
  cases — and the decisions you've made and why, so they aren't re-litigated later.
214395
214407
  `;
214396
214408
 
214397
- // ../lecodes-design/src/templates.ts
214409
+ // ../../../lecodes-tools/packages/lecodes-design/src/templates.ts
214398
214410
  var SPEC_MD, META_JSON = `{
214399
214411
  "screens": {
214400
214412
  "home": { "pos": [0, 0], "desc": "Example screen — replace me", "role": "list" }
@@ -214862,8 +214874,8 @@ __export(exports_design, {
214862
214874
  flushDesignComments: () => flushDesignComments,
214863
214875
  design: () => design
214864
214876
  });
214865
- import { existsSync as existsSync24, mkdirSync as mkdirSync18, readFileSync as readFileSync21, readdirSync as readdirSync13, writeFileSync as writeFileSync19, appendFileSync } from "node:fs";
214866
- import { basename as basename10, join as join25 } from "node:path";
214877
+ import { existsSync as existsSync24, mkdirSync as mkdirSync18, readFileSync as readFileSync21, readdirSync as readdirSync14, writeFileSync as writeFileSync19, appendFileSync } from "node:fs";
214878
+ import { basename as basename11, join as join25 } from "node:path";
214867
214879
  var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214868
214880
  let root;
214869
214881
  let manifest;
@@ -214878,7 +214890,7 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214878
214890
  return {
214879
214891
  root,
214880
214892
  manifest,
214881
- name: manifest?.name ?? basename10(root),
214893
+ name: manifest?.name ?? basename11(root),
214882
214894
  dirName,
214883
214895
  designDir: join25(root, dirName),
214884
214896
  designKey: "/" + dirName.replace(/\\/g, "/")
@@ -214919,7 +214931,7 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214919
214931
  const dir = join25(designDir, "screens");
214920
214932
  if (!existsSync24(dir))
214921
214933
  return [];
214922
- return readdirSync13(dir).filter((f3) => f3.endsWith(".ts") && !f3.startsWith(".")).map((f3) => f3.slice(0, -3)).sort();
214934
+ return readdirSync14(dir).filter((f3) => f3.endsWith(".ts") && !f3.startsWith(".")).map((f3) => f3.slice(0, -3)).sort();
214923
214935
  }, writeMcpConfig = (ctx, port) => {
214924
214936
  const path2 = join25(ctx.root, ".mcp.json");
214925
214937
  const entry = { type: "http", url: `http://localhost:${port}/mcp` };
@@ -216529,26 +216541,26 @@ var require_main = __commonJS((exports, module) => {
216529
216541
  // package.json
216530
216542
  var package_default = {
216531
216543
  name: "lecodes-cli",
216532
- version: "0.19.0",
216544
+ version: "0.20.0",
216533
216545
  dependencies: {
216534
216546
  "@letary/chisel": "^0.10.0",
216535
- jimp: "^1.6.1"
216547
+ jimp: "^1.6.1",
216548
+ "lecodes-sdk": "workspace:*"
216536
216549
  },
216537
216550
  devDependencies: {
216538
- "lecodes-design": "workspace:*",
216551
+ "lecodes-design": "link:lecodes-design",
216539
216552
  "lecodes-renderer": "workspace:*",
216540
216553
  "qrcode-terminal": "^0.12.0",
216541
- "lecodes-3d-editor": "workspace:*",
216554
+ "lecodes-3d-editor": "link:lecodes-3d-editor",
216542
216555
  "lecodes-assets": "workspace:*",
216543
- "creator-nav": "workspace:*",
216544
- sdk: "workspace:*",
216545
- "viewer-lite": "workspace:*"
216556
+ "lecodes-creator-nav": "workspace:*",
216557
+ "lecodes-viewer-lite": "workspace:*"
216546
216558
  },
216547
216559
  peerDependencies: {
216548
- "lecodes-design": "^0.8.1",
216549
- "lecodes-renderer": "^0.8.9",
216550
- "lecodes-3d-editor": "^0.6.0",
216551
- "lecodes-assets": "^0.1.0"
216560
+ "lecodes-design": "^0.9.0",
216561
+ "lecodes-renderer": "^0.20.0",
216562
+ "lecodes-3d-editor": "^0.7.0",
216563
+ "lecodes-assets": "^0.2.1"
216552
216564
  },
216553
216565
  bin: {
216554
216566
  lecodes: "dist/index.js"
@@ -216559,6 +216571,7 @@ var package_default = {
216559
216571
  },
216560
216572
  files: [
216561
216573
  "dist",
216574
+ "src",
216562
216575
  "runtime",
216563
216576
  "README.md"
216564
216577
  ],
@@ -216598,7 +216611,7 @@ var package_default = {
216598
216611
  init_manifest();
216599
216612
  init_config();
216600
216613
  import { execFileSync as execFileSync6 } from "node:child_process";
216601
- import { cpSync as cpSync4, existsSync as existsSync20, mkdirSync as mkdirSync14, readFileSync as readFileSync17, readdirSync as readdirSync9, rmSync as rmSync7, statSync as statSync7, writeFileSync as writeFileSync16 } from "node:fs";
216614
+ import { cpSync as cpSync4, existsSync as existsSync20, mkdirSync as mkdirSync14, readFileSync as readFileSync17, readdirSync as readdirSync10, rmSync as rmSync7, statSync as statSync8, writeFileSync as writeFileSync16 } from "node:fs";
216602
216615
  import { homedir as homedir7 } from "node:os";
216603
216616
  import { basename as basename8, dirname as dirname12, join as join21, resolve as resolve8 } from "node:path";
216604
216617
 
@@ -216725,7 +216738,7 @@ init_fonts();
216725
216738
  init_compile();
216726
216739
  import { execFile } from "node:child_process";
216727
216740
  import { createHash as createHash4, randomBytes } from "node:crypto";
216728
- import { copyFileSync, existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync11, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync7 } from "node:fs";
216741
+ import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync11, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync7 } from "node:fs";
216729
216742
  import { tmpdir } from "node:os";
216730
216743
  import { basename as basename3, join as join13 } from "node:path";
216731
216744
 
@@ -216918,17 +216931,14 @@ var matcIdentity = (exe) => {
216918
216931
  return `${stat.size}:${Math.trunc(stat.mtimeMs)}`;
216919
216932
  };
216920
216933
  var shaderHash = (text, matcId) => createHash4("sha256").update(`${TARGET_MATRIX_GENERATION}\x00${matcId}\x00${text}`).digest("hex").slice(0, 16);
216921
- var artifactFamily = (text) => {
216922
- const matcPlatforms = [...matcPlatformsFor(text), ...LOCAL_ONLY_PLATFORMS];
216923
- return [...matcPlatforms, ...matcPlatforms.includes("webgl") ? [] : ["webgl"], "webgl-lite"];
216924
- };
216934
+ var artifactFamily = () => [...MATC_PLATFORMS, ...LOCAL_ONLY_PLATFORMS, "webgl-lite"];
216925
216935
  var shaderIsCompiled = (root, text, matcId) => {
216926
216936
  const hash = shaderHash(text, matcId);
216927
- return artifactFamily(text).every((p) => existsSync11(join13(shadersCacheDir(root), `${hash}_${p}.filamat`)));
216937
+ return artifactFamily().every((p) => existsSync11(join13(shadersCacheDir(root), `${hash}_${p}.filamat`)));
216928
216938
  };
216929
216939
  var shaderCompiledPlatforms = (root, text, matcId) => {
216930
216940
  const hash = shaderHash(text, matcId);
216931
- return artifactFamily(text).filter((p) => existsSync11(join13(shadersCacheDir(root), `${hash}_${p}.filamat`)));
216941
+ return artifactFamily().filter((p) => existsSync11(join13(shadersCacheDir(root), `${hash}_${p}.filamat`)));
216932
216942
  };
216933
216943
  var localShaderArtifact = (root, text, platform) => {
216934
216944
  const matcExe = resolveMatcExeCached();
@@ -216973,15 +216983,12 @@ var applyLocalShaders = async (entries, opts) => {
216973
216983
  for (const entry of shaderEntries) {
216974
216984
  const text = readFileSync11(entry.absPath, "utf8");
216975
216985
  const hash = shaderHash(text, matcId);
216976
- const family = artifactFamily(text);
216986
+ const family = artifactFamily();
216977
216987
  const artifact = (platform) => `${hash}_${platform}.filamat`;
216978
216988
  const has = (platform) => existsSync11(join13(cacheDir, artifact(platform)));
216979
216989
  const wanted = opts.platforms ? family.filter((p) => opts.platforms.includes(p)) : family;
216980
- if (wanted.includes("webgl") && !matcPlatformsFor(text).includes("webgl") && !wanted.includes("opengl")) {
216981
- wanted.push("opengl");
216982
- }
216983
216990
  const missing = wanted.filter((p) => !has(p));
216984
- const matcMissing = missing.filter((p) => p !== "webgl-lite" && !(p === "webgl" && !matcPlatformsFor(text).includes("webgl")));
216991
+ const matcMissing = missing.filter((p) => p !== "webgl-lite");
216985
216992
  if (matcMissing.length > 0) {
216986
216993
  const tempName = `lecodes-matc-${randomBytes(6).toString("hex")}.mat`;
216987
216994
  const tempFile = join13(tmpdir(), tempName);
@@ -217001,9 +217008,6 @@ ${cleanMatcOutput(stderr, tempName, basename3(entry.path))}`);
217001
217008
  rmSync2(tempFile, { force: true });
217002
217009
  }
217003
217010
  }
217004
- if (missing.includes("webgl") && !matcPlatformsFor(text).includes("webgl")) {
217005
- copyFileSync(join13(cacheDir, artifact("opengl")), join13(cacheDir, artifact("webgl")));
217006
- }
217007
217011
  if (missing.includes("webgl-lite")) {
217008
217012
  writeFileSync7(join13(cacheDir, artifact("webgl-lite")), JSON.stringify(buildLiteMaterialArtifact(text)));
217009
217013
  }
@@ -217018,7 +217022,7 @@ ${cleanMatcOutput(stderr, tempName, basename3(entry.path))}`);
217018
217022
  artifacts.push({ name: artifact(platform), absPath: join13(cacheDir, artifact(platform)) });
217019
217023
  }
217020
217024
  } else {
217021
- const order = ["opengl", "metal", "desktop", "webgl", "webgl-lite"];
217025
+ const order = ["opengl", "metal", "desktop", "webgl-lite"];
217022
217026
  entry.shaders = order.map((platform) => ({ platform, src: `${opts.srcPrefix}/${artifact(platform)}` }));
217023
217027
  }
217024
217028
  }
@@ -217034,12 +217038,13 @@ init_manifest();
217034
217038
  init_ignore();
217035
217039
  init_util();
217036
217040
  import { execFileSync } from "node:child_process";
217037
- import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "node:fs";
217041
+ import { existsSync as existsSync13, mkdirSync as mkdirSync8, readdirSync as readdirSync5, readFileSync as readFileSync13, statSync as statSync3, writeFileSync as writeFileSync9 } from "node:fs";
217038
217042
  import { homedir as homedir4 } from "node:os";
217039
217043
  import { dirname as dirname8, join as join15, resolve as resolve2 } from "node:path";
217040
217044
  var FIRST_PARTY_PLUGINS = {
217041
217045
  camera: "letary/lecodes-plugins",
217042
217046
  "qr-scanner": "letary/lecodes-plugins",
217047
+ map: "letary/lecodes-plugins",
217043
217048
  geolocation: "letary/lecodes-plugins",
217044
217049
  push: "letary/lecodes-plugins"
217045
217050
  };
@@ -217070,6 +217075,7 @@ var desktopDefaultViewport = (root) => {
217070
217075
  height: cfg.desktop.window?.height ?? DESKTOP_DEFAULT_WINDOW.height
217071
217076
  };
217072
217077
  };
217078
+ var swiftPackageName = (url) => (url.replace(/\/+$/, "").split("/").pop() ?? url).replace(/\.git$/, "");
217073
217079
  var readJson = (path) => {
217074
217080
  if (!existsSync13(path))
217075
217081
  return null;
@@ -217223,7 +217229,8 @@ var ensureRepo = (repo, wantTag, lock) => {
217223
217229
  git(cache, "checkout", "--quiet", "--detach", commit);
217224
217230
  return { dir: cache, tag: tag ?? "unknown", commit };
217225
217231
  };
217226
- var resolvePlugin = (id, spec, lock, platform) => {
217232
+ var PLUGIN_PLATFORM_LABEL = { ios: "iOS", android: "Android", desktop: "desktop" };
217233
+ function resolvePlugin(id, spec, lock, platform, opts) {
217227
217234
  let repo;
217228
217235
  let subdir;
217229
217236
  let wantTag = null;
@@ -217247,7 +217254,9 @@ var resolvePlugin = (id, spec, lock, platform) => {
217247
217254
  if (manifest.id !== id)
217248
217255
  throw new CliError(`Plugin "${id}": manifest declares id "${manifest.id}"`);
217249
217256
  if (!manifest[platform]) {
217250
- throw new CliError(`Plugin "${id}" does not support ${platform === "ios" ? "iOS" : "Android"} (manifest "${platform}" is null)`);
217257
+ if (opts?.optional)
217258
+ return null;
217259
+ throw new CliError(`Plugin "${id}" does not support ${PLUGIN_PLATFORM_LABEL[platform]} (manifest "${platform}" is null)`);
217251
217260
  }
217252
217261
  if (spec.startsWith("github:")) {
217253
217262
  const names = [...manifest.provides?.views ?? [], ...manifest.provides?.services ?? []];
@@ -217262,7 +217271,7 @@ var resolvePlugin = (id, spec, lock, platform) => {
217262
217271
  manifest,
217263
217272
  lock: { repo, tag: checkout.tag, commit: checkout.commit, version: manifest.version ?? "unknown" }
217264
217273
  };
217265
- };
217274
+ }
217266
217275
  var latestPluginTag = (repo) => {
217267
217276
  try {
217268
217277
  return ensureRepo(repo, null, undefined).tag;
@@ -217270,6 +217279,55 @@ var latestPluginTag = (repo) => {
217270
217279
  return null;
217271
217280
  }
217272
217281
  };
217282
+ var desktopPluginTarget = () => `${process.platform}-${process.arch}`;
217283
+ var isDesktopPluginLibrary = (name) => {
217284
+ const n = name.toLowerCase();
217285
+ return process.platform === "win32" ? n.endsWith(".dll") : n.endsWith(".so");
217286
+ };
217287
+ var desktopPluginLibraries = (root, opts = {}) => {
217288
+ const dirs = [];
217289
+ const files = [];
217290
+ const local = join15(root, "plugins");
217291
+ if (existsSync13(local) && statSync3(local).isDirectory()) {
217292
+ dirs.push(local);
217293
+ for (const f2 of readdirSync5(local))
217294
+ if (isDesktopPluginLibrary(f2))
217295
+ files.push(join15(local, f2));
217296
+ }
217297
+ const config = readJson(join15(root, "app.json"));
217298
+ const specs = Object.entries(config?.plugins ?? {});
217299
+ if (specs.length === 0)
217300
+ return { dirs, files };
217301
+ const lock = readJson(join15(root, "plugins.lock.json"))?.plugins ?? {};
217302
+ const target = desktopPluginTarget();
217303
+ for (const [id, spec] of specs) {
217304
+ let plugin;
217305
+ try {
217306
+ plugin = resolvePlugin(id, spec, lock[id], "desktop", { optional: true });
217307
+ } catch (e) {
217308
+ if (opts.strict)
217309
+ throw e;
217310
+ warn(`Plugin "${id}": ${e.message} — skipped for this desktop run`);
217311
+ continue;
217312
+ }
217313
+ const rel = plugin?.manifest.desktop?.library?.[target];
217314
+ if (!plugin || !rel)
217315
+ continue;
217316
+ const abs = resolve2(plugin.dir, rel);
217317
+ if (!existsSync13(abs)) {
217318
+ const msg = `Plugin "${id}": desktop library ${abs} is missing (manifest desktop.library["${target}"])`;
217319
+ if (opts.strict)
217320
+ throw new CliError(msg);
217321
+ warn(`${msg} — skipped for this desktop run`);
217322
+ continue;
217323
+ }
217324
+ files.push(abs);
217325
+ const dir = dirname8(abs);
217326
+ if (!dirs.includes(dir))
217327
+ dirs.push(dir);
217328
+ }
217329
+ return { dirs, files };
217330
+ };
217273
217331
 
217274
217332
  // src/commands/compile.ts
217275
217333
  init_util();
@@ -218074,13 +218132,31 @@ var APP_ICON_JSON = `{
218074
218132
  `;
218075
218133
  var runtimePackageTemplate = (sdkVersion, variant, plugins) => {
218076
218134
  const product = `LeCodesSDK-${variant}`;
218077
- const pluginTargets = plugins.map((p) => `
218135
+ const packages = new Map;
218136
+ for (const p of plugins) {
218137
+ for (const pkg of p.packages ?? []) {
218138
+ const prior = packages.get(pkg.url);
218139
+ if (prior && (prior.exact !== pkg.exact || prior.from !== pkg.from)) {
218140
+ throw new Error(`Plugin "${p.id}" pins ${pkg.url} at ${pkg.exact ?? `from ${pkg.from}`} but another plugin pins ${prior.exact ?? `from ${prior.from}`} — the shell can link one version.`);
218141
+ }
218142
+ packages.set(pkg.url, pkg);
218143
+ }
218144
+ }
218145
+ const packageDecls = [...packages.values()].map((pkg) => `
218146
+ .package(url: "${pkg.url}", ${pkg.exact ? `exact: "${pkg.exact}"` : `from: "${pkg.from}"`}),`).join("");
218147
+ const pluginTargets = plugins.map((p) => {
218148
+ const deps = [
218149
+ `.product(name: "${product}", package: "lecodes-ios-sdk")`,
218150
+ ...(p.packages ?? []).flatMap((pkg) => pkg.products.map((name) => `.product(name: "${name}", package: "${swiftPackageName(pkg.url)}")`))
218151
+ ];
218152
+ return `
218078
218153
  // plugin: ${p.id} (vendored — see plugins.lock.json)
218079
218154
  .target(
218080
218155
  name: "${p.module}",
218081
- dependencies: [.product(name: "${product}", package: "lecodes-ios-sdk")],
218156
+ dependencies: [${deps.join(", ")}],
218082
218157
  path: "Sources/${p.module}"
218083
- ),`).join("");
218158
+ ),`;
218159
+ }).join("");
218084
218160
  const runtimeDeps = [`.product(name: "${product}", package: "lecodes-ios-sdk")`, ...plugins.map((p) => `"${p.module}"`)];
218085
218161
  return `// swift-tools-version:5.9
218086
218162
  //
@@ -218099,7 +218175,7 @@ let package = Package(
218099
218175
  .library(name: "LeCodesRuntime", targets: ["LeCodesRuntime"]),
218100
218176
  ],
218101
218177
  dependencies: [
218102
- .package(url: "https://github.com/letary/lecodes-ios-sdk.git", exact: "${sdkVersion}"),
218178
+ .package(url: "https://github.com/letary/lecodes-ios-sdk.git", exact: "${sdkVersion}"),${packageDecls}
218103
218179
  ],
218104
218180
  targets: [
218105
218181
  .target(
@@ -218245,7 +218321,7 @@ buildNumber) — edit it there, then re-run \`lecodes app sync\`. Plugins are ma
218245
218321
  init_manifest();
218246
218322
  import { execFileSync as execFileSync2, spawn, spawnSync as spawnSync3 } from "node:child_process";
218247
218323
  import { createHash as createHash6 } from "node:crypto";
218248
- import { chmodSync, cpSync, existsSync as existsSync16, mkdirSync as mkdirSync10, readFileSync as readFileSync15, readdirSync as readdirSync5, renameSync as renameSync2, rmSync as rmSync3, statSync as statSync3, writeFileSync as writeFileSync12 } from "node:fs";
218324
+ import { chmodSync, cpSync, existsSync as existsSync16, mkdirSync as mkdirSync10, readFileSync as readFileSync15, readdirSync as readdirSync6, renameSync as renameSync2, rmSync as rmSync3, statSync as statSync4, writeFileSync as writeFileSync12 } from "node:fs";
218249
218325
  import { homedir as homedir5 } from "node:os";
218250
218326
  import { basename as basename5, dirname as dirname10, join as join17 } from "node:path";
218251
218327
  init_util();
@@ -218630,6 +218706,12 @@ Sync links the smallest SDK artifact covering the engines the compiled bundle ac
218630
218706
  engine, sync switches automatically on the next run; set \`android.variant\` in \`../app.json\`
218631
218707
  to force one (needs lecodes-android-sdk >= 1.3.0; older releases ship only the full AAR).
218632
218708
 
218709
+ Physics, navigation meshes and multiplayer ship in \`lecodes-sdk\` (full) ONLY —
218710
+ \`lecodes-sdk-3d\` is 3D rendering without the game stack. Auto-selection reads which RENDERING
218711
+ engines the bundle uses and cannot see those, so a project using \`Physics\`,
218712
+ \`CharacterController\`, \`NavMesh\` or networking must set \`android.variant: "full"\` in
218713
+ \`../app.json\` — otherwise those aspects silently do nothing on device.
218714
+
218633
218715
  ## App-local native code
218634
218716
 
218635
218717
  Drop Kotlin files into \`app/src/main/java/\` and register them in \`MainActivity\` after the
@@ -219486,7 +219568,7 @@ var syncAndroid = async (root, config, prior) => {
219486
219568
  }
219487
219569
  const namespace = register.split(".").slice(0, -1).join(".");
219488
219570
  const libsDir = join17(dest, "libs");
219489
- const aarNames = existsSync16(libsDir) ? readdirSync5(libsDir).filter((f3) => f3.endsWith(".aar")) : [];
219571
+ const aarNames = existsSync16(libsDir) ? readdirSync6(libsDir).filter((f3) => f3.endsWith(".aar")) : [];
219490
219572
  for (const aar of aarNames) {
219491
219573
  const base = aar.replace(/\.aar$/, "");
219492
219574
  const wrapDir = join17(libsDir, base);
@@ -219502,8 +219584,8 @@ var syncAndroid = async (root, config, prior) => {
219502
219584
  log(`${c.green("plugin")} ${id}@${plugin.lock.version} (${plugin.lock.tag} → plugins/${module})`);
219503
219585
  }
219504
219586
  const keep = new Set(runtimePlugins.map((p2) => p2.module));
219505
- for (const entry of existsSync16(pluginsDir) ? readdirSync5(pluginsDir) : []) {
219506
- if (!keep.has(entry) && statSync3(join17(pluginsDir, entry)).isDirectory()) {
219587
+ for (const entry of existsSync16(pluginsDir) ? readdirSync6(pluginsDir) : []) {
219588
+ if (!keep.has(entry) && statSync4(join17(pluginsDir, entry)).isDirectory()) {
219507
219589
  rmrf(join17(pluginsDir, entry));
219508
219590
  log(`${c.yellow("removed")} android/plugins/${entry} (plugin uninstalled)`);
219509
219591
  }
@@ -219668,7 +219750,7 @@ var buildAndroid = async (args) => {
219668
219750
  for (const dir of [apkDir, aabDir]) {
219669
219751
  if (!existsSync16(dir))
219670
219752
  continue;
219671
- for (const f3 of readdirSync5(dir)) {
219753
+ for (const f3 of readdirSync6(dir)) {
219672
219754
  if (f3.endsWith(".apk") || f3.endsWith(".aab"))
219673
219755
  outputs.push(join17(dir, f3));
219674
219756
  }
@@ -219725,7 +219807,7 @@ var doctorAndroidChecks = async (root, config, check) => {
219725
219807
  const validVariant = requestedVariant === "auto" || SDK_VARIANTS.includes(requestedVariant);
219726
219808
  check(validVariant, `android.variant "${requestedVariant}"`, `use "auto" or one of ${SDK_VARIANTS.join(", ")}`);
219727
219809
  const wrapperJar = join17(androidDir, "gradle", "wrapper", "gradle-wrapper.jar");
219728
- check(existsSync16(wrapperJar) && statSync3(wrapperJar).size > 0, "Gradle wrapper jar", `run \`gradle wrapper --gradle-version ${GRADLE_VERSION}\` in android/ (or let Android Studio repair it)`);
219810
+ check(existsSync16(wrapperJar) && statSync4(wrapperJar).size > 0, "Gradle wrapper jar", `run \`gradle wrapper --gradle-version ${GRADLE_VERSION}\` in android/ (or let Android Studio repair it)`);
219729
219811
  check(existsSync16(join17(androidDir, "app", "src", "main", "assets", "app.js")), "Bundled app.js", "run `lecodes app sync`");
219730
219812
  const linked = shellSdkVersionAndroid(androidDir);
219731
219813
  const pin = config.android?.sdk ?? "latest";
@@ -219748,14 +219830,14 @@ var doctorAndroidChecks = async (root, config, check) => {
219748
219830
 
219749
219831
  // src/commands/appDesktop.ts
219750
219832
  import { execFileSync as execFileSync5, spawn as spawn4 } from "node:child_process";
219751
- import { cpSync as cpSync3, existsSync as existsSync19, mkdirSync as mkdirSync13, readdirSync as readdirSync8, rmSync as rmSync6, statSync as statSync6, writeFileSync as writeFileSync15 } from "node:fs";
219833
+ import { cpSync as cpSync3, existsSync as existsSync19, mkdirSync as mkdirSync13, readdirSync as readdirSync9, rmSync as rmSync6, statSync as statSync7, writeFileSync as writeFileSync15 } from "node:fs";
219752
219834
  import { basename as basename7, dirname as dirname11, join as join20, resolve as resolve7 } from "node:path";
219753
219835
 
219754
219836
  // src/desktopRenderer.ts
219755
219837
  import { execFileSync as execFileSync3, spawn as spawn2, spawnSync as spawnSync4 } from "node:child_process";
219756
- import { copyFileSync as copyFileSync2, existsSync as existsSync17, mkdirSync as mkdirSync11, mkdtempSync as mkdtempSync2, readdirSync as readdirSync6, readFileSync as readFileSync16, rmSync as rmSync4, statSync as statSync4, writeFileSync as writeFileSync13 } from "node:fs";
219838
+ import { copyFileSync, existsSync as existsSync17, mkdirSync as mkdirSync11, mkdtempSync as mkdtempSync2, readdirSync as readdirSync7, readFileSync as readFileSync16, rmSync as rmSync4, statSync as statSync5, writeFileSync as writeFileSync13 } from "node:fs";
219757
219839
  import { homedir as homedir6, tmpdir as tmpdir2 } from "node:os";
219758
- import { join as join18, resolve as resolve5 } from "node:path";
219840
+ import { delimiter, join as join18, resolve as resolve5 } from "node:path";
219759
219841
  init_util();
219760
219842
 
219761
219843
  // ../viewer-lite/src/sourcemapError.ts
@@ -219875,7 +219957,7 @@ var isMac = () => process.platform === "darwin";
219875
219957
  var exeName = () => process.platform === "win32" ? "lecodes-desktop.exe" : isMac() ? "lecodes-app" : "lecodes-desktop";
219876
219958
  var findExe = (dir) => {
219877
219959
  const name = exeName();
219878
- for (const entry of readdirSync6(dir, { withFileTypes: true })) {
219960
+ for (const entry of readdirSync7(dir, { withFileTypes: true })) {
219879
219961
  const p2 = join18(dir, entry.name);
219880
219962
  if (entry.isFile() && entry.name === name)
219881
219963
  return p2;
@@ -219891,7 +219973,7 @@ var cacheRoot2 = () => join18(homedir6(), ".lecodes", "renderer");
219891
219973
  var currentFile = () => join18(cacheRoot2(), "current.json");
219892
219974
  var installedDesktopVersions = () => {
219893
219975
  try {
219894
- return readdirSync6(cacheRoot2(), { withFileTypes: true }).filter((e2) => e2.isDirectory() && e2.name.startsWith("desktop-")).map((e2) => e2.name.slice("desktop-".length)).filter((v) => findExe(join18(cacheRoot2(), `desktop-${v}`)) !== null).sort(semverNewer);
219976
+ return readdirSync7(cacheRoot2(), { withFileTypes: true }).filter((e2) => e2.isDirectory() && e2.name.startsWith("desktop-")).map((e2) => e2.name.slice("desktop-".length)).filter((v) => findExe(join18(cacheRoot2(), `desktop-${v}`)) !== null).sort(semverNewer);
219895
219977
  } catch {
219896
219978
  return [];
219897
219979
  }
@@ -219916,7 +219998,7 @@ var extractDmg = (archive, stage) => {
219916
219998
  mkdirSync11(mount, { recursive: true });
219917
219999
  execFileSync3("hdiutil", ["attach", "-nobrowse", "-readonly", "-quiet", "-mountpoint", mount, archive]);
219918
220000
  try {
219919
- const app = readdirSync6(mount).find((n2) => n2.endsWith(".app"));
220001
+ const app = readdirSync7(mount).find((n2) => n2.endsWith(".app"));
219920
220002
  if (!app)
219921
220003
  throw new CliError("The downloaded DMG holds no .app bundle.");
219922
220004
  execFileSync3("cp", ["-R", join18(mount, app), join18(stage, app)]);
@@ -220037,8 +220119,12 @@ var updateDesktop = async () => {
220037
220119
  else
220038
220120
  note(`Desktop renderer set to ${latest} (was ${before}).`);
220039
220121
  };
220122
+ var pluginEnv = (dirs) => {
220123
+ const all = [...dirs ?? [], ...process.env.CREATOR_PLUGIN_DIRS ? [process.env.CREATOR_PLUGIN_DIRS] : []];
220124
+ return all.length > 0 ? { CREATOR_PLUGIN_DIRS: all.join(delimiter) } : {};
220125
+ };
220040
220126
  var windowEnv = (opts) => {
220041
- const env = {};
220127
+ const env = { ...pluginEnv(opts.pluginDirs) };
220042
220128
  const width = opts.width ?? (opts.height !== undefined ? 1280 : undefined);
220043
220129
  const height = opts.height ?? (opts.width !== undefined ? 720 : undefined);
220044
220130
  if (width && height)
@@ -220082,8 +220168,8 @@ var stageBundle = (js, resources) => {
220082
220168
  const runDir = mkdtempSync2(join18(tmpdir2(), "lecodes-render-"));
220083
220169
  writeFileSync13(join18(runDir, "app.js"), js);
220084
220170
  for (const r2 of resources) {
220085
- if (r2.absPath && existsSync17(r2.absPath) && statSync4(r2.absPath).isFile()) {
220086
- copyFileSync2(r2.absPath, join18(runDir, r2.name));
220171
+ if (r2.absPath && existsSync17(r2.absPath) && statSync5(r2.absPath).isFile()) {
220172
+ copyFileSync(r2.absPath, join18(runDir, r2.name));
220087
220173
  }
220088
220174
  }
220089
220175
  return runDir;
@@ -220126,7 +220212,16 @@ var runDesktopApp = async (js, resources, opts = {}) => {
220126
220212
  });
220127
220213
  });
220128
220214
  } finally {
220129
- rmSync4(runDir, { recursive: true, force: true });
220215
+ for (let attempt = 0;; attempt++) {
220216
+ try {
220217
+ rmSync4(runDir, { recursive: true, force: true });
220218
+ break;
220219
+ } catch {
220220
+ if (attempt >= 5)
220221
+ break;
220222
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200);
220223
+ }
220224
+ }
220130
220225
  }
220131
220226
  };
220132
220227
  var runDesktopDev = async (url, opts = {}) => {
@@ -220165,6 +220260,7 @@ var runDesktopRender = async (js, resources, opts) => {
220165
220260
  env.CREATOR_CAMERA = opts.camera;
220166
220261
  if (opts.view)
220167
220262
  env.CREATOR_VIEW = opts.view;
220263
+ Object.assign(env, pluginEnv(opts.pluginDirs));
220168
220264
  const demangle = makeStackDemangler(js) ?? ((text) => text);
220169
220265
  return await new Promise((done, fail) => {
220170
220266
  const child = spawn2(exe, [appJs], { cwd: runDir, env, stdio: ["ignore", "pipe", "pipe"] });
@@ -220204,7 +220300,16 @@ ${tail}` : "";
220204
220300
  });
220205
220301
  });
220206
220302
  } finally {
220207
- rmSync4(runDir, { recursive: true, force: true });
220303
+ for (let attempt = 0;; attempt++) {
220304
+ try {
220305
+ rmSync4(runDir, { recursive: true, force: true });
220306
+ break;
220307
+ } catch {
220308
+ if (attempt >= 5)
220309
+ break;
220310
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200);
220311
+ }
220312
+ }
220208
220313
  }
220209
220314
  };
220210
220315
 
@@ -220214,13 +220319,13 @@ init_util();
220214
220319
  // src/commands/appDesktopMac.ts
220215
220320
  init_util();
220216
220321
  import { execFileSync as execFileSync4, spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
220217
- import { cpSync as cpSync2, existsSync as existsSync18, mkdirSync as mkdirSync12, mkdtempSync as mkdtempSync3, readdirSync as readdirSync7, rmSync as rmSync5, statSync as statSync5, writeFileSync as writeFileSync14 } from "node:fs";
220322
+ import { cpSync as cpSync2, existsSync as existsSync18, mkdirSync as mkdirSync12, mkdtempSync as mkdtempSync3, readdirSync as readdirSync8, rmSync as rmSync5, statSync as statSync6, symlinkSync, writeFileSync as writeFileSync14 } from "node:fs";
220218
220323
  import { tmpdir as tmpdir3 } from "node:os";
220219
220324
  import { basename as basename6, join as join19, resolve as resolve6 } from "node:path";
220220
220325
  var mb = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)} MB`;
220221
- var dirSize = (dir) => readdirSync7(dir, { withFileTypes: true }).reduce((sum, e2) => {
220326
+ var dirSize = (dir) => readdirSync8(dir, { withFileTypes: true }).reduce((sum, e2) => {
220222
220327
  const p2 = join19(dir, e2.name);
220223
- return sum + (e2.isDirectory() ? dirSize(p2) : e2.isSymbolicLink() ? 0 : statSync5(p2).size);
220328
+ return sum + (e2.isDirectory() ? dirSize(p2) : e2.isSymbolicLink() ? 0 : statSync6(p2).size);
220224
220329
  }, 0);
220225
220330
  var cleanBundleName = (name) => name.replace(/[/:]/g, "").trim() || "App";
220226
220331
  var macHostAppOf = (exe) => {
@@ -220233,7 +220338,7 @@ var macHostAppOf = (exe) => {
220233
220338
  var macHostProvidedNames = (exe) => {
220234
220339
  const names3 = new Set;
220235
220340
  const walk = (dir) => {
220236
- for (const e2 of readdirSync7(dir, { withFileTypes: true })) {
220341
+ for (const e2 of readdirSync8(dir, { withFileTypes: true })) {
220237
220342
  if (e2.isDirectory())
220238
220343
  walk(join19(dir, e2.name));
220239
220344
  else
@@ -220336,11 +220441,12 @@ var signApp = async (app, signing, hostApp, tmp) => {
220336
220441
  throw new CliError(`The signature doesn't verify:
220337
220442
  ${(verify.stdout + verify.stderr).trim()}`);
220338
220443
  };
220339
- var notarizeApp = (app, profile, tmp) => {
220340
- const zip = join19(tmp, `${basename6(app, ".app")}.zip`);
220341
- execFileSync4("ditto", ["-c", "-k", "--keepParent", app, zip]);
220342
- note(`Notarizing with profile "${profile}" — this waits on Apple (usually 1–5 minutes)…`);
220343
- const r2 = spawnSync5("xcrun", ["notarytool", "submit", zip, "--keychain-profile", profile, "--wait"], { encoding: "utf8" });
220444
+ var notarize = (target, profile, tmp, what) => {
220445
+ const submission = target.endsWith(".app") ? join19(tmp, `${basename6(target, ".app")}.zip`) : target;
220446
+ if (submission !== target)
220447
+ execFileSync4("ditto", ["-c", "-k", "--keepParent", target, submission]);
220448
+ note(`Notarizing the ${what} with profile "${profile}" — this waits on Apple (usually 1–5 minutes)…`);
220449
+ const r2 = spawnSync5("xcrun", ["notarytool", "submit", submission, "--keychain-profile", profile, "--wait"], { encoding: "utf8" });
220344
220450
  const out = (r2.stdout + `
220345
220451
  ` + r2.stderr).trim();
220346
220452
  const id = /id: ([0-9a-f-]{36})/.exec(out)?.[1];
@@ -220349,11 +220455,44 @@ var notarizeApp = (app, profile, tmp) => {
220349
220455
  ${out}
220350
220456
  ` + (id ? ` Details: xcrun notarytool log ${id} --keychain-profile ${profile}` : ""));
220351
220457
  }
220352
- const staple = spawnSync5("xcrun", ["stapler", "staple", app], { encoding: "utf8" });
220458
+ const staple = spawnSync5("xcrun", ["stapler", "staple", target], { encoding: "utf8" });
220353
220459
  if (staple.status !== 0)
220354
220460
  throw new CliError(`Stapling failed:
220355
220461
  ${(staple.stdout + staple.stderr).trim()}`);
220356
- success("Notarized + stapled.");
220462
+ success(`Notarized + stapled (${what}).`);
220463
+ };
220464
+ var buildDmg = async (app, out, appName, signing, tmp) => {
220465
+ const dmg = join19(out, `${appName}.dmg`);
220466
+ rmSync5(dmg, { force: true });
220467
+ const stage = join19(tmp, "dmg");
220468
+ rmSync5(stage, { recursive: true, force: true });
220469
+ mkdirSync12(stage, { recursive: true });
220470
+ note("Building the disk image…");
220471
+ execFileSync4("cp", ["-R", app, join19(stage, basename6(app))]);
220472
+ symlinkSync("/Applications", join19(stage, "Applications"));
220473
+ const r2 = spawnSync5("hdiutil", [
220474
+ "create",
220475
+ "-volname",
220476
+ appName,
220477
+ "-srcfolder",
220478
+ stage,
220479
+ "-fs",
220480
+ "HFS+",
220481
+ "-format",
220482
+ "UDZO",
220483
+ "-ov",
220484
+ dmg
220485
+ ], { encoding: "utf8" });
220486
+ rmSync5(stage, { recursive: true, force: true });
220487
+ if (r2.status !== 0)
220488
+ throw new CliError(`hdiutil failed:
220489
+ ${(r2.stdout + r2.stderr).trim()}`);
220490
+ if (signing.identity !== "-") {
220491
+ await codesignRetrying(["--force", "--timestamp", "--sign", signing.identity, dmg]);
220492
+ if (signing.notaryProfile)
220493
+ notarize(dmg, signing.notaryProfile, tmp, "disk image");
220494
+ }
220495
+ success(`DMG: ${dmg} (${mb(statSync6(dmg).size)})`);
220357
220496
  };
220358
220497
  var gatekeeperVerdict = (app) => {
220359
220498
  const r2 = spawnSync5("spctl", ["-a", "-t", "exec", "-vv", app], { encoding: "utf8" });
@@ -220454,12 +220593,12 @@ var buildMacApp = async (input) => {
220454
220593
  writeFileSync14(join19(proj, "app.js"), js);
220455
220594
  let assetBytes = 0, assetCount = 0;
220456
220595
  for (const r2 of shipped) {
220457
- if (!r2.absPath || !existsSync18(r2.absPath) || !statSync5(r2.absPath).isFile()) {
220596
+ if (!r2.absPath || !existsSync18(r2.absPath) || !statSync6(r2.absPath).isFile()) {
220458
220597
  warn(`asset "${r2.name}" (${r2.path}) has no local file — skipped`);
220459
220598
  continue;
220460
220599
  }
220461
220600
  cpSync2(r2.absPath, join19(proj, "assets", r2.name));
220462
- assetBytes += statSync5(r2.absPath).size;
220601
+ assetBytes += statSync6(r2.absPath).size;
220463
220602
  assetCount++;
220464
220603
  }
220465
220604
  note(` assets: ${assetCount} files, ${mb(assetBytes)} → Contents/Resources/project/assets/`);
@@ -220487,7 +220626,7 @@ var buildMacApp = async (input) => {
220487
220626
  }, developerIdInKeychain);
220488
220627
  await signApp(app, signing, hostApp, tmp);
220489
220628
  if (signing.notaryProfile) {
220490
- notarizeApp(app, signing.notaryProfile, tmp);
220629
+ notarize(app, signing.notaryProfile, tmp, "app");
220491
220630
  } else if (signing.identity !== "-") {
220492
220631
  warn("Signed with Developer ID but NOT notarized — Gatekeeper refuses a downloaded copy on every other Mac.");
220493
220632
  warn(" Set desktop.macos.notarize.profile in app.json (a `xcrun notarytool store-credentials` profile) or pass --notarize <profile>.");
@@ -220499,15 +220638,17 @@ var buildMacApp = async (input) => {
220499
220638
  }
220500
220639
  writeFileSync14(join19(out, "README.txt"), readmeText(appName, !!signing.notaryProfile, signing.identity !== "-"));
220501
220640
  success(`App: ${app} (${mb(dirSize(app))})`);
220641
+ if (flagBool(args, "dmg"))
220642
+ await buildDmg(app, out, appName, signing, tmp);
220502
220643
  if (flagBool(args, "probe"))
220503
220644
  await probeMacApp(app, out, args);
220504
220645
  if (flagBool(args, "zip")) {
220505
220646
  const zip = join19(out, `${appName}.zip`);
220506
220647
  rmSync5(zip, { force: true });
220507
220648
  execFileSync4("ditto", ["-c", "-k", "--keepParent", app, zip]);
220508
- success(`Zip: ${zip} (${mb(statSync5(zip).size)})`);
220509
- } else {
220510
- note("Tip: --zip packs the .app for sending (ditto, structure-preserving).");
220649
+ success(`Zip: ${zip} (${mb(statSync6(zip).size)})`);
220650
+ } else if (!flagBool(args, "dmg")) {
220651
+ note("Tip: --zip packs the .app for sending; --dmg wraps it in a drag-to-Applications image.");
220511
220652
  }
220512
220653
  } finally {
220513
220654
  rmSync5(tmp, { recursive: true, force: true });
@@ -220529,9 +220670,9 @@ or remove the quarantine flag in Terminal: xattr -dr com.apple.quarantine "` +
220529
220670
 
220530
220671
  // src/commands/appDesktop.ts
220531
220672
  var mb2 = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)} MB`;
220532
- var dirSize2 = (dir) => readdirSync8(dir, { withFileTypes: true }).reduce((sum, e2) => {
220673
+ var dirSize2 = (dir) => readdirSync9(dir, { withFileTypes: true }).reduce((sum, e2) => {
220533
220674
  const p2 = join20(dir, e2.name);
220534
- return sum + (e2.isDirectory() ? dirSize2(p2) : statSync6(p2).size);
220675
+ return sum + (e2.isDirectory() ? dirSize2(p2) : statSync7(p2).size);
220535
220676
  }, 0);
220536
220677
  var initDesktop = async (args) => {
220537
220678
  const root = projectRoot();
@@ -220560,7 +220701,7 @@ var buildDesktop = async (args) => {
220560
220701
  const slug = flagStr(args, "slug") ?? cleanFileName(name);
220561
220702
  const out = resolve7(root, flagStr(args, "o", "out") ?? join20(root, "dist", slug));
220562
220703
  if (existsSync19(out)) {
220563
- const entries = readdirSync8(out);
220704
+ const entries = readdirSync9(out);
220564
220705
  const looksLikeBuild = entries.length === 0 || entries.includes("app.js") || entries.some((e2) => e2.endsWith(".app"));
220565
220706
  if (!looksLikeBuild) {
220566
220707
  throw new CliError(`${out} exists and doesn't look like a previous desktop build — pick another -o or clear it yourself.`);
@@ -220597,7 +220738,7 @@ var buildDesktop = async (args) => {
220597
220738
  const hostProvided = mac ? macHostProvidedNames(exe) : new Set;
220598
220739
  if (!mac) {
220599
220740
  try {
220600
- for (const f3 of readdirSync8(join20(hostDir, "assets")))
220741
+ for (const f3 of readdirSync9(join20(hostDir, "assets")))
220601
220742
  hostProvided.add(f3);
220602
220743
  } catch {}
220603
220744
  }
@@ -220608,10 +220749,12 @@ var buildDesktop = async (args) => {
220608
220749
  }
220609
220750
  if (mac)
220610
220751
  return buildMacApp({ root, config, name, slug, out, js, shipped, hostExe: exe, args });
220752
+ if (flagBool(args, "dmg"))
220753
+ warn("--dmg is a macOS format — ignored on this platform.");
220611
220754
  const version = process.env.LECODES_DESKTOP_EXE ? "local build" : selectedDesktopVersion();
220612
220755
  rmSync6(out, { recursive: true, force: true });
220613
220756
  mkdirSync13(out, { recursive: true });
220614
- for (const entry of readdirSync8(hostDir, { withFileTypes: true })) {
220757
+ for (const entry of readdirSync9(hostDir, { withFileTypes: true })) {
220615
220758
  if (entry.isFile() && entry.name.toLowerCase().endsWith(".dll")) {
220616
220759
  cpSync3(join20(hostDir, entry.name), join20(out, entry.name));
220617
220760
  } else if (entry.isDirectory() && entry.name === "assets") {
@@ -220631,12 +220774,12 @@ var buildDesktop = async (args) => {
220631
220774
  if (shipped.length > 0)
220632
220775
  mkdirSync13(join20(out, ASSETS_DIR), { recursive: true });
220633
220776
  for (const r2 of shipped) {
220634
- if (!r2.absPath || !existsSync19(r2.absPath) || !statSync6(r2.absPath).isFile()) {
220777
+ if (!r2.absPath || !existsSync19(r2.absPath) || !statSync7(r2.absPath).isFile()) {
220635
220778
  warn(`asset "${r2.name}" (${r2.path}) has no local file — skipped`);
220636
220779
  continue;
220637
220780
  }
220638
220781
  cpSync3(r2.absPath, join20(out, ASSETS_DIR, r2.name));
220639
- assetBytes += statSync6(r2.absPath).size;
220782
+ assetBytes += statSync7(r2.absPath).size;
220640
220783
  assetCount++;
220641
220784
  }
220642
220785
  note(` assets: ${assetCount} files, ${mb2(assetBytes)} → ${ASSETS_DIR}/`);
@@ -220645,6 +220788,15 @@ var buildDesktop = async (args) => {
220645
220788
  await writeIco(root, config, join20(out, "app.ico"));
220646
220789
  note(" app.ico rendered from " + config.icon);
220647
220790
  }
220791
+ {
220792
+ const { files } = desktopPluginLibraries(root, { strict: true });
220793
+ if (files.length > 0) {
220794
+ mkdirSync13(join20(out, "plugins"), { recursive: true });
220795
+ for (const f3 of files)
220796
+ cpSync3(f3, join20(out, "plugins", basename7(f3)));
220797
+ note(` plugins: ${files.map((f3) => basename7(f3)).join(", ")} → plugins/`);
220798
+ }
220799
+ }
220648
220800
  writeFileSync15(join20(out, "README.txt"), "\uFEFF" + readmeText2(name, exeName2, version));
220649
220801
  success(`Portable build: ${out} (${mb2(dirSize2(out))})`);
220650
220802
  if (flagBool(args, "probe"))
@@ -220744,7 +220896,7 @@ var zipBuild = (out) => {
220744
220896
  } else {
220745
220897
  execFileSync5("zip", ["-rq", zip, basename7(out)], { cwd: dirname11(out) });
220746
220898
  }
220747
- success(`Zip: ${zip} (${mb2(statSync6(zip).size)})`);
220899
+ success(`Zip: ${zip} (${mb2(statSync7(zip).size)})`);
220748
220900
  };
220749
220901
  var readmeText2 = (name, exeName2, version) => `${name}
220750
220902
  ${"=".repeat(Math.max(name.length, 4))}
@@ -220951,13 +221103,18 @@ var syncIos = async (root, config) => {
220951
221103
  for (const [key, value] of Object.entries(ios.entitlements ?? {})) {
220952
221104
  (entitlementValues[key] ??= []).push(value);
220953
221105
  }
220954
- runtimePlugins.push({ id, module, register });
221106
+ for (const pkg of ios.packages ?? []) {
221107
+ if (!pkg.url || !Array.isArray(pkg.products) || pkg.products.length === 0 || !pkg.exact && !pkg.from || pkg.exact && pkg.from) {
221108
+ throw new CliError(`Plugin "${id}": manifest ios.packages entries need { url, exact | from, products: [...] } — got ${JSON.stringify(pkg)}`);
221109
+ }
221110
+ }
221111
+ runtimePlugins.push({ id, module, register, packages: ios.packages });
220955
221112
  newLock[id] = plugin.lock;
220956
221113
  log(`${c.green("plugin")} ${id}@${plugin.lock.version} (${plugin.lock.tag} → Sources/${module})`);
220957
221114
  }
220958
221115
  const keep = new Set(["LeCodesRuntime", ...runtimePlugins.map((p2) => p2.module)]);
220959
- for (const entry of existsSync20(sourcesDir) ? readdirSync9(sourcesDir) : []) {
220960
- if (!keep.has(entry) && statSync7(join21(sourcesDir, entry)).isDirectory()) {
221116
+ for (const entry of existsSync20(sourcesDir) ? readdirSync10(sourcesDir) : []) {
221117
+ if (!keep.has(entry) && statSync8(join21(sourcesDir, entry)).isDirectory()) {
220961
221118
  rmSync7(join21(sourcesDir, entry), { recursive: true });
220962
221119
  log(`${c.yellow("removed")} ios/LeCodesRuntime/Sources/${entry} (plugin uninstalled)`);
220963
221120
  }
@@ -220985,7 +221142,7 @@ var syncIos = async (root, config) => {
220985
221142
  if (variant === "3d" || variant === "full") {
220986
221143
  const sdkResources = await ensureSdkResourcesBundle(sdkVersion);
220987
221144
  cpSync4(sdkResources, join21(resourcesDir, "LeCodesSDKResources.bundle"), { recursive: true });
220988
- for (const f3 of readdirSync9(sdkResources))
221145
+ for (const f3 of readdirSync10(sdkResources))
220989
221146
  if (f3 !== "Info.plist")
220990
221147
  sdkProvided.add(f3);
220991
221148
  log(`${c.green("resources")} LeCodesSDKResources.bundle (filament) → ios/App/Resources/`);
@@ -221323,7 +221480,7 @@ var doctorIosChecks = async (root, config, check) => {
221323
221480
  check(xcodeVersion !== "", `Xcode CLI (${xcodeVersion || "not found"})`, "install Xcode + command line tools");
221324
221481
  check(!!config.ios.teamId, "Development team set", "lecodes app init wrote app.json — fill ios.teamId", false);
221325
221482
  const iconDir = join21(root, "ios", "App", "Assets.xcassets", "AppIcon.appiconset");
221326
- const hasIcon = existsSync20(iconDir) && readdirSync9(iconDir).some((f3) => f3.endsWith(".png"));
221483
+ const hasIcon = existsSync20(iconDir) && readdirSync10(iconDir).some((f3) => f3.endsWith(".png"));
221327
221484
  check(hasIcon, "App icon present", "drop a 1024×1024 PNG into ios/App/Assets.xcassets/AppIcon.appiconset/ (App Store requires it)", false);
221328
221485
  const bundle = join21(root, "ios", "App", "Resources", "app.js");
221329
221486
  check(existsSync20(bundle), "Bundled app.js", "run `lecodes app sync`");
@@ -221366,7 +221523,7 @@ var doctorIosChecks = async (root, config, check) => {
221366
221523
  const plist = readFileSync17(join21(root, "ios", "App", "Info.plist"), "utf8");
221367
221524
  check(plist.includes("<key>LeCodesUpdateURL</key>"), "OTA update URL in Info.plist", "run `lecodes app sync`", false);
221368
221525
  const appDir = join21(root, "ios", "App");
221369
- const callsUpdater = readdirSync9(appDir).some((f3) => f3.endsWith(".swift") && readFileSync17(join21(appDir, f3), "utf8").includes("LeCodesUpdater.activeBundle"));
221526
+ const callsUpdater = readdirSync10(appDir).some((f3) => f3.endsWith(".swift") && readFileSync17(join21(appDir, f3), "utf8").includes("LeCodesUpdater.activeBundle"));
221370
221527
  check(callsUpdater, "App code launches via LeCodesUpdater", `no App/*.swift calls LeCodesUpdater.activeBundle() — downloaded updates would never run; see "Over-the-air updates" in ios/README.md for the ViewController snippet`, false);
221371
221528
  }
221372
221529
  const asc = readJson(join21(homedir7(), ".lecodes", "asc.json"));
@@ -221519,7 +221676,7 @@ var app = async (args) => {
221519
221676
  init_api();
221520
221677
  init_config();
221521
221678
  init_manifest();
221522
- import { existsSync as existsSync22, mkdirSync as mkdirSync16, readdirSync as readdirSync11 } from "node:fs";
221679
+ import { existsSync as existsSync22, mkdirSync as mkdirSync16, readdirSync as readdirSync12 } from "node:fs";
221523
221680
  import { resolve as resolve9 } from "node:path";
221524
221681
  init_types4();
221525
221682
  init_ignore();
@@ -221542,7 +221699,7 @@ var clone3 = async (args) => {
221542
221699
  const project = await getProject(apiUrl, token, uuid);
221543
221700
  const dir = args._[1] ?? slug(project.name);
221544
221701
  const root = resolve9(dir);
221545
- if (existsSync22(root) && readdirSync11(root).length > 0) {
221702
+ if (existsSync22(root) && readdirSync12(root).length > 0) {
221546
221703
  throw new CliError(`Directory "${dir}" already exists and is not empty.`);
221547
221704
  }
221548
221705
  mkdirSync16(root, { recursive: true });
@@ -221568,7 +221725,7 @@ init_config();
221568
221725
  init_manifest();
221569
221726
  init_types4();
221570
221727
  init_ignore();
221571
- import { existsSync as existsSync23, mkdirSync as mkdirSync17, readdirSync as readdirSync12, writeFileSync as writeFileSync18 } from "node:fs";
221728
+ import { existsSync as existsSync23, mkdirSync as mkdirSync17, readdirSync as readdirSync13, writeFileSync as writeFileSync18 } from "node:fs";
221572
221729
  import { join as join23, resolve as resolve10 } from "node:path";
221573
221730
 
221574
221731
  // src/commands/projectTemplates.ts
@@ -221591,7 +221748,7 @@ lecodes render src/screens/cart.ts # ONE screen module, without booting the ap
221591
221748
  # --state <name> picks the branch). Same flags as above.
221592
221749
  lecodes render --desktop --png shot.png # NATIVE render: real Filament 3D + UI + 2D composited —
221593
221750
  # use this to SEE 3D scenes / procedural geometry (headless
221594
- # renders record 3D as JSON only). Windows/Linux; downloads
221751
+ # renders record 3D as JSON — simulated, not drawn). Windows/Linux; downloads
221595
221752
  # the renderer on first use. --frames <n> for slow loads.
221596
221753
  lecodes render --script <file> # drive the app through a scenario (format below)
221597
221754
  lecodes render --desktop --view iso --png iso.png # 3D: inspection camera fitted to the WHOLE
@@ -221640,8 +221797,9 @@ advance in fixed 1/60 steps driven by \`wait\`/\`frames\`/\`drag\`/\`key\` — n
221640
221797
  so a flow that passes once passes every time. \`Input.key(code)\` reads the keys a flow holds and
221641
221798
  \`Input.on('keydown' | 'keyup', e => e.code)\` fires for each \`key\`/\`keyDown\`/\`keyUp\` step
221642
221799
  (events land before the next loop tick); \`stick\` values are what \`Input.gamepad(0).axis()\` reads.
221643
- Headless draws UI + the 2D scene (sprites, tilemaps, Box2D); 3D is captured as JSON and Jolt does
221644
- not simulate — use \`--desktop\` for 3D pixels. \`console.log\` + \`expect.log\` is the assertion
221800
+ Headless draws UI + the 2D scene (sprites, tilemaps, Box2D); 3D is captured as JSON with REAL Jolt
221801
+ physics (bodies fall, characters collide, triggers fire, ragdolls drop — a character with no ground
221802
+ under it falls, like on device) — use \`--desktop\` for 3D pixels. \`console.log\` + \`expect.log\` is the assertion
221645
221803
  channel for game state that has no UI.
221646
221804
 
221647
221805
  ## Make the app testable
@@ -221660,7 +221818,7 @@ channel for game state that has no UI.
221660
221818
  ## SDK documentation
221661
221819
 
221662
221820
  \`.lecodes/types/\` is the exact API surface (signatures only; generated and gitignored — run
221663
- \`lecodes types\` if it's missing, never commit \`.lecodes/\`). For semantics and examples,
221821
+ \`lecodes update\` if it's missing, never commit \`.lecodes/\`). For semantics and examples,
221664
221822
  fetch the docs — they are served as raw markdown for AI tools:
221665
221823
 
221666
221824
  - https://le.codes/llms.txt — the docs map; fetch it first to find the right page
@@ -221710,7 +221868,7 @@ var create = async (args) => {
221710
221868
  throw new CliError("Usage: lecodes create <name> [directory]");
221711
221869
  const dir = args._[1] ?? slug2(name);
221712
221870
  const root = resolve10(dir);
221713
- if (existsSync23(root) && readdirSync12(root).length > 0) {
221871
+ if (existsSync23(root) && readdirSync13(root).length > 0) {
221714
221872
  throw new CliError(`Directory "${dir}" already exists and is not empty.`);
221715
221873
  }
221716
221874
  mkdirSync17(root, { recursive: true });
@@ -221733,6 +221891,7 @@ var create = async (args) => {
221733
221891
  // src/commands/desktop.ts
221734
221892
  init_util();
221735
221893
  init_manifest();
221894
+ import { basename as basename9 } from "node:path";
221736
221895
  var resolveScale = (flag, cfg) => {
221737
221896
  const raw = flag ?? cfg;
221738
221897
  if (raw === undefined || raw === "system")
@@ -221766,9 +221925,13 @@ var run = async (args) => {
221766
221925
  const renderer = resolveDesktopRenderer(root, args);
221767
221926
  const { js, resources } = await buildBundle(args, undefined, { localAssets: true, desktopRenderer: renderer });
221768
221927
  assertRendererSupportsBundle(renderer, js);
221928
+ const { dirs: pluginDirs, files: pluginFiles } = desktopPluginLibraries(root);
221929
+ if (pluginFiles.length > 0)
221930
+ note(`Native plugins: ${pluginFiles.map((f3) => basename9(f3)).join(", ")}`);
221769
221931
  note(`Launching LeCodes Desktop${renderer === "vulkan" ? " (Vulkan)" : ""}… (close the window to return)`);
221770
221932
  await runDesktopApp(js, resources, {
221771
221933
  renderer,
221934
+ pluginDirs,
221772
221935
  width: Number(flagStr(args, "width")) || win?.width,
221773
221936
  height: Number(flagStr(args, "height")) || win?.height,
221774
221937
  minWidth: win?.minWidth,
@@ -221822,15 +221985,15 @@ init_design();
221822
221985
  // src/commands/dev.ts
221823
221986
  init_manifest();
221824
221987
  init_ignore();
221825
- import { existsSync as existsSync25, mkdirSync as mkdirSync19, readFileSync as readFileSync22, watch, writeFileSync as writeFileSync20 } from "node:fs";
221988
+ import { existsSync as existsSync26, mkdirSync as mkdirSync19, readFileSync as readFileSync22, watch, writeFileSync as writeFileSync20 } from "node:fs";
221826
221989
  import { randomBytes as randomBytes3 } from "node:crypto";
221827
221990
  import { networkInterfaces } from "node:os";
221828
- import { basename as basename11, join as join27 } from "node:path";
221991
+ import { basename as basename12, join as join28 } from "node:path";
221829
221992
 
221830
221993
  // src/dev/devServer.ts
221831
221994
  init_localFiles();
221832
221995
  import { createServer as createServer2 } from "node:http";
221833
- import { createReadStream, statSync as statSync9 } from "node:fs";
221996
+ import { createReadStream, statSync as statSync10 } from "node:fs";
221834
221997
  import { join as join26 } from "node:path";
221835
221998
  init_projectCompile();
221836
221999
  init_fonts();
@@ -222094,7 +222257,12 @@ var MIME = {
222094
222257
  woff2: "font/woff2",
222095
222258
  json: "application/json",
222096
222259
  txt: "text/plain",
222097
- js: "text/javascript"
222260
+ js: "text/javascript",
222261
+ mjs: "text/javascript",
222262
+ css: "text/css",
222263
+ html: "text/html",
222264
+ wasm: "application/wasm",
222265
+ map: "application/json"
222098
222266
  };
222099
222267
  var contentType = (path2) => {
222100
222268
  const i2 = path2.lastIndexOf(".");
@@ -222189,22 +222357,58 @@ var startDevServer = async (options) => {
222189
222357
  else if (m.type === "error")
222190
222358
  log2(`[device:error] ${demangle(m.text ?? "")}`);
222191
222359
  };
222192
- const transportOf = (url) => url.searchParams.get("t") === "poll" ? "poll" : "ws";
222360
+ const transportOf = (url) => {
222361
+ const t2 = url.searchParams.get("t");
222362
+ return t2 === "poll" ? "poll" : t2 === "web" ? "web" : "ws";
222363
+ };
222364
+ const withPrelude = (js, url, v2) => {
222365
+ const transport = transportOf(url);
222366
+ return transport === "web" ? js : js + `
222367
+ ` + renderPrelude({ origin, base, transport, v: v2 });
222368
+ };
222369
+ const webDir = options.webDir ?? null;
222193
222370
  const handle = async (req, res) => {
222194
222371
  const url = new URL(req.url ?? "/", origin);
222195
222372
  const path2 = url.pathname;
222373
+ if (path2 === "/favicon.ico") {
222374
+ res.writeHead(204);
222375
+ res.end();
222376
+ return;
222377
+ }
222196
222378
  if (!path2.startsWith(base + "/"))
222197
222379
  return sendText(res, 404, "text/plain", "Not found");
222198
222380
  const route = path2.slice(base.length);
222199
222381
  if (route === "/dev.js") {
222200
- return sendText(res, 200, "text/javascript", devJs + `
222201
- ` + renderPrelude({ origin, base, transport: transportOf(url), v: -1 }));
222382
+ return sendText(res, 200, "text/javascript", withPrelude(devJs, url, -1));
222202
222383
  }
222203
222384
  if (route === "/bundle.js") {
222204
222385
  if (bundleJs === null)
222205
222386
  return sendText(res, 503, "text/plain", "No successful compile yet — check the terminal.");
222206
- return sendText(res, 200, "text/javascript", bundleJs + `
222207
- ` + renderPrelude({ origin, base, transport: transportOf(url), v }));
222387
+ res.setHeader("x-lecodes-v", String(v));
222388
+ return sendText(res, 200, "text/javascript", withPrelude(bundleJs, url, v));
222389
+ }
222390
+ if (route === "/web") {
222391
+ res.writeHead(302, { location: `${base}/web/${url.search}` });
222392
+ res.end();
222393
+ return;
222394
+ }
222395
+ if (route.startsWith("/web/")) {
222396
+ if (!webDir)
222397
+ return sendText(res, 404, "text/plain", "No Web Lite runner vendored with this CLI (lecodes-viewer-lite dist-embed).");
222398
+ const rel = route === "/web/" ? "embed.html" : route.slice("/web/".length);
222399
+ if (rel.split("/").some((seg) => seg === ".." || seg === "" || seg === "."))
222400
+ return sendText(res, 400, "text/plain", "Bad path");
222401
+ const abs = join26(webDir, ...rel.split("/"));
222402
+ try {
222403
+ if (!statSync10(abs).isFile())
222404
+ throw new Error("not a file");
222405
+ } catch {
222406
+ return sendText(res, 404, "text/plain", "Not found");
222407
+ }
222408
+ const hashed = rel.startsWith("assets/");
222409
+ res.writeHead(200, { "content-type": contentType(rel), "cache-control": hashed ? "max-age=31536000, immutable" : "no-store" });
222410
+ createReadStream(abs).pipe(res);
222411
+ return;
222208
222412
  }
222209
222413
  if (route === "/version") {
222210
222414
  return sendJson(res, 200, { v });
@@ -222219,7 +222423,7 @@ var startDevServer = async (options) => {
222219
222423
  return sendText(res, 400, "text/plain", "Bad path");
222220
222424
  const abs = join26(shadersCacheDir(root), name2);
222221
222425
  try {
222222
- if (!statSync9(abs).isFile())
222426
+ if (!statSync10(abs).isFile())
222223
222427
  throw new Error("not a file");
222224
222428
  } catch {
222225
222429
  return sendText(res, 404, "text/plain", "Not found");
@@ -222236,7 +222440,7 @@ var startDevServer = async (options) => {
222236
222440
  return sendText(res, 400, "text/plain", "Bad path");
222237
222441
  }
222238
222442
  const abs = resolveLocalPath(root, key);
222239
- if (!abs || !statSync9(abs).isFile())
222443
+ if (!abs || !statSync10(abs).isFile())
222240
222444
  return sendText(res, 404, "text/plain", "Not found");
222241
222445
  res.writeHead(200, { "content-type": contentType(key), "cache-control": "no-store" });
222242
222446
  createReadStream(abs).pipe(res);
@@ -222290,6 +222494,7 @@ var startDevServer = async (options) => {
222290
222494
  const boundPort = typeof address === "object" && address !== null ? address.port : options.port;
222291
222495
  return {
222292
222496
  url: `${origin}${base}/dev.js`,
222497
+ webUrl: webDir ? `${origin}${base}/web/` : null,
222293
222498
  base,
222294
222499
  port: boundPort,
222295
222500
  v: () => v,
@@ -222303,7 +222508,25 @@ var startDevServer = async (options) => {
222303
222508
  };
222304
222509
  };
222305
222510
 
222511
+ // src/dev/webRunner.ts
222512
+ init_distRoot();
222513
+ import { existsSync as existsSync25 } from "node:fs";
222514
+ import { createRequire as createRequire5 } from "node:module";
222515
+ import { dirname as dirname14, join as join27 } from "node:path";
222516
+ var resolveWebRunnerDir = () => {
222517
+ const candidates = [join27(distRoot, "runtime", "web")];
222518
+ try {
222519
+ const pkg = createRequire5(import.meta.url).resolve("lecodes-viewer-lite/package.json");
222520
+ candidates.push(join27(dirname14(pkg), "dist-embed"));
222521
+ } catch {}
222522
+ for (const dir of candidates)
222523
+ if (existsSync25(join27(dir, "embed.html")) && existsSync25(join27(dir, "embed.js")))
222524
+ return dir;
222525
+ return null;
222526
+ };
222527
+
222306
222528
  // src/commands/dev.ts
222529
+ init_browserAuth();
222307
222530
  init_util();
222308
222531
  var DEFAULT_PORT2 = 4499;
222309
222532
  var DEBOUNCE_MS = 120;
@@ -222331,14 +222554,14 @@ var lanCandidates = () => {
222331
222554
  return out.sort((x2, y2) => x2.rank - y2.rank).map(({ address, iface }) => ({ address, iface }));
222332
222555
  };
222333
222556
  var loadDevToken = (root) => {
222334
- const path2 = join27(root, LECODES_DIR, "dev.json");
222557
+ const path2 = join28(root, LECODES_DIR, "dev.json");
222335
222558
  try {
222336
222559
  const token2 = JSON.parse(readFileSync22(path2, "utf8")).token;
222337
222560
  if (token2 && /^[a-z0-9]{8,}$/.test(token2))
222338
222561
  return token2;
222339
222562
  } catch {}
222340
222563
  const token = randomBytes3(6).toString("hex");
222341
- mkdirSync19(join27(root, LECODES_DIR), { recursive: true });
222564
+ mkdirSync19(join28(root, LECODES_DIR), { recursive: true });
222342
222565
  writeFileSync20(path2, JSON.stringify({ token }, null, 2) + `
222343
222566
  `);
222344
222567
  return token;
@@ -222364,7 +222587,7 @@ var dev = async (args) => {
222364
222587
  root = process.cwd();
222365
222588
  manifest = null;
222366
222589
  }
222367
- const name = manifest?.name ?? basename11(root);
222590
+ const name = manifest?.name ?? basename12(root);
222368
222591
  if (typeof globalThis.Bun !== "undefined") {
222369
222592
  warnErr("Running under Bun: the websocket reload channel doesn't work here — run `lecodes dev` with Node (or use ?t=poll on the device URL).");
222370
222593
  }
@@ -222376,6 +222599,10 @@ var dev = async (args) => {
222376
222599
  const alternatives = candidates.filter((c4) => c4.address !== host);
222377
222600
  const origin = `http://${host}:${port}`;
222378
222601
  const token = loadDevToken(root);
222602
+ const webDir = resolveWebRunnerDir();
222603
+ if (flagBool(args, "web") && !webDir) {
222604
+ throw new CliError("No Web Lite runner found for --web: this CLI has no runtime/web (a monorepo checkout needs `bun run build:embed` in packages/viewer-lite).");
222605
+ }
222379
222606
  const server2 = await startDevServer({
222380
222607
  root,
222381
222608
  manifest,
@@ -222384,6 +222611,7 @@ var dev = async (args) => {
222384
222611
  port,
222385
222612
  token,
222386
222613
  entryOverride: flagStr(args, "entry"),
222614
+ webDir,
222387
222615
  log: note
222388
222616
  }).catch((e2) => {
222389
222617
  const msg = e2 instanceof Error ? e2.message : String(e2);
@@ -222452,14 +222680,21 @@ ${result.error}`);
222452
222680
  if (!flagBool(args, "no-qr"))
222453
222681
  await printQr(server2.url);
222454
222682
  log(`Scan the QR with the LeCodes app, or open: ${c.bold(server2.url)}`);
222683
+ const webLocal = server2.webUrl ? server2.webUrl.replace(`http://${host}:`, "http://localhost:") : null;
222684
+ if (server2.webUrl && webLocal)
222685
+ log(`In the browser (Web Lite): ${c.bold(webLocal)} (LAN: ${server2.webUrl})`);
222455
222686
  note(`Older desktop builds (no websocket bridge): ${server2.url}?t=poll`);
222456
222687
  if (alternatives.length > 0) {
222457
222688
  note(`Device can't reach it? This machine also has ${alternatives.map((a2) => `${a2.address} (${a2.iface})`).join(", ")} — retry with --host <ip>.`);
222458
222689
  }
222459
- if (existsSync25(join27(root, LECODES_DIR, "manifest.json")))
222690
+ if (existsSync26(join28(root, LECODES_DIR, "manifest.json")))
222460
222691
  note(`Token stored in ${LECODES_DIR}/dev.json (delete it to rotate the URL).`);
222461
222692
  log("");
222462
222693
  log(`Watching ${root} — save a file to hot-reload. Ctrl+C to stop.`);
222694
+ if (flagBool(args, "web") && webLocal) {
222695
+ note("Opening the browser…");
222696
+ openBrowser(webLocal);
222697
+ }
222463
222698
  if (flagBool(args, "desktop")) {
222464
222699
  const appCfg = readAppConfigAt(root);
222465
222700
  const win = appCfg?.desktop?.window;
@@ -222486,7 +222721,7 @@ init_localFiles();
222486
222721
  // src/serverDiff.ts
222487
222722
  init_localFiles();
222488
222723
  import { readFileSync as readFileSync23 } from "node:fs";
222489
- import { basename as basename12 } from "node:path";
222724
+ import { basename as basename13 } from "node:path";
222490
222725
  var isTextAsset = (t2) => t2 === "text" || t2 === "shader" || t2 === "scene";
222491
222726
  var computeServerDiff = (project, scan2) => {
222492
222727
  const pathById = buildPaths(project.assets);
@@ -222503,7 +222738,7 @@ var computeServerDiff = (project, scan2) => {
222503
222738
  const server2 = serverByPath.get(path2);
222504
222739
  const local = localByPath.get(path2);
222505
222740
  if (local && !server2) {
222506
- const text = isTextKind(classifyKind(basename12(path2)));
222741
+ const text = isTextKind(classifyKind(basename13(path2)));
222507
222742
  entries.push({ path: path2, status: "added", binary: !text, serverText: "", localText: text ? readFileSync23(local.absPath, "utf8") : undefined });
222508
222743
  continue;
222509
222744
  }
@@ -222678,8 +222913,8 @@ var statusLabel = (e2) => e2.status === "added" ? c.green(" added ") : e2.st
222678
222913
  init_manifest();
222679
222914
  init_types4();
222680
222915
  init_ignore();
222681
- import { existsSync as existsSync26, mkdirSync as mkdirSync20, writeFileSync as writeFileSync21 } from "node:fs";
222682
- import { basename as basename13, dirname as dirname14, join as join28, resolve as resolve11 } from "node:path";
222916
+ import { existsSync as existsSync27, mkdirSync as mkdirSync20, writeFileSync as writeFileSync21 } from "node:fs";
222917
+ import { basename as basename14, dirname as dirname15, join as join29, resolve as resolve11 } from "node:path";
222683
222918
  init_util();
222684
222919
  var STARTER2 = `// Welcome to LeCodes — the whole SDK is available as globals, no imports needed.
222685
222920
  // Run it live on a device with \`lecodes dev\`, or bundle it with \`lecodes compile -o app.js\`.
@@ -222777,9 +223012,9 @@ lecodes app open # open it in Xcode, build & run
222777
223012
  - \`CLAUDE.md\` — guidance for AI assistants (the headless feedback loop + testability conventions)
222778
223013
  - \`assets/\` — images/fonts the app references (created on demand)
222779
223014
  - \`tsconfig.json\` + \`.lecodes/types/\` — generated IDE types (rewritten by the CLI; don't edit).
222780
- They come from the SDK bundled in the CLI; \`lecodes types\` refreshes them from the platform.
223015
+ They come from the SDK bundled in the CLI; \`lecodes update\` refreshes them from the platform.
222781
223016
  - \`.lecodes/\` is gitignored — per-checkout state (those types + \`manifest.json\`, this machine's
222782
- sync cursor once linked). After a fresh git clone: \`lecodes types\`, then
223017
+ sync cursor once linked). After a fresh git clone: \`lecodes update\`, then
222783
223018
  \`lecodes link --project <uuid>\` to re-attach.
222784
223019
  - \`.lecodesignore\` — files a future \`lecodes push\` would skip
222785
223020
 
@@ -222788,16 +223023,16 @@ Full command list: \`lecodes --help\`.
222788
223023
  var init2 = async (args) => {
222789
223024
  const dirArg = (args._[0] ?? ".").replace(/[/\\]+$/, "") || ".";
222790
223025
  const root = resolve11(dirArg);
222791
- const name = basename13(root);
223026
+ const name = basename14(root);
222792
223027
  mkdirSync20(root, { recursive: true });
222793
223028
  let created = 0;
222794
223029
  const write = (rel, content) => {
222795
- const abs = join28(root, rel);
222796
- if (existsSync26(abs)) {
223030
+ const abs = join29(root, rel);
223031
+ if (existsSync27(abs)) {
222797
223032
  note(`kept ${rel}`);
222798
223033
  return;
222799
223034
  }
222800
- mkdirSync20(dirname14(abs), { recursive: true });
223035
+ mkdirSync20(dirname15(abs), { recursive: true });
222801
223036
  writeFileSync21(abs, content);
222802
223037
  log(`${c.green("created")} ${rel}`);
222803
223038
  created++;
@@ -222845,8 +223080,8 @@ init_config();
222845
223080
  init_ignore();
222846
223081
  init_localFiles();
222847
223082
  init_manifest();
222848
- import { existsSync as existsSync27 } from "node:fs";
222849
- import { basename as basename15, join as join29, resolve as resolve12 } from "node:path";
223083
+ import { existsSync as existsSync28 } from "node:fs";
223084
+ import { basename as basename16, join as join30, resolve as resolve12 } from "node:path";
222850
223085
  init_types4();
222851
223086
  init_util();
222852
223087
 
@@ -222855,7 +223090,7 @@ init_api();
222855
223090
  init_config();
222856
223091
  init_manifest();
222857
223092
  init_localFiles();
222858
- import { basename as basename14 } from "node:path";
223093
+ import { basename as basename15 } from "node:path";
222859
223094
  import { readFileSync as readFileSync24 } from "node:fs";
222860
223095
  init_util();
222861
223096
  var push = async (args) => {
@@ -222912,14 +223147,14 @@ var push = async (args) => {
222912
223147
  let uploaded = 0;
222913
223148
  for (const file of scan2.files) {
222914
223149
  const source = sourceFor(file.path);
222915
- const kind = source?.type ?? classifyKind(basename14(file.path));
223150
+ const kind = source?.type ?? classifyKind(basename15(file.path));
222916
223151
  const entry = { path: file.path, type: kind };
222917
223152
  if (source)
222918
223153
  entry.assetId = source.id;
222919
223154
  if (isTextKind(kind)) {
222920
223155
  entry.text = readFileSync24(file.absPath, "utf8");
222921
223156
  } else if (!source || source.sha !== file.sha) {
222922
- const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename14(file.path), readFileSync24(file.absPath));
223157
+ const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename15(file.path), readFileSync24(file.absPath));
222923
223158
  entry.blobFileId = blob.id;
222924
223159
  uploaded++;
222925
223160
  }
@@ -223027,9 +223262,9 @@ var link = async (args) => {
223027
223262
  const token = requireToken(config);
223028
223263
  const dirArg = args._[0];
223029
223264
  const root = dirArg ? resolve12(dirArg) : process.cwd();
223030
- if (!existsSync27(root))
223265
+ if (!existsSync28(root))
223031
223266
  throw new CliError(`No such directory: ${root}`);
223032
- if (existsSync27(join29(root, LECODES_DIR, "manifest.json")) && !flagBool(args, "force")) {
223267
+ if (existsSync28(join30(root, LECODES_DIR, "manifest.json")) && !flagBool(args, "force")) {
223033
223268
  throw new CliError("This folder is already linked (.lecodes/manifest.json exists) — use `lecodes push`, or `lecodes link --force` to re-link it to another project.");
223034
223269
  }
223035
223270
  let enclosing = null;
@@ -223056,7 +223291,7 @@ var link = async (args) => {
223056
223291
  }
223057
223292
  name = project.name;
223058
223293
  } else {
223059
- name = flagStr(args, "name") ?? basename15(root);
223294
+ name = flagStr(args, "name") ?? basename16(root);
223060
223295
  info(c.dim(`Creating project ${c.bold(name)}…`));
223061
223296
  uuid = (await createProject(apiUrl, token, name, flagStr(args, "team"))).uuid;
223062
223297
  files = {};
@@ -223093,10 +223328,10 @@ var link = async (args) => {
223093
223328
 
223094
223329
  // src/commands/lightmap.ts
223095
223330
  init_manifest();
223096
- import { existsSync as existsSync28, mkdirSync as mkdirSync21, mkdtempSync as mkdtempSync4, rmSync as rmSync9, statSync as statSync10, unlinkSync, writeFileSync as writeFileSync22 } from "node:fs";
223097
- import { spawnSync as spawnSync6 } from "node:child_process";
223331
+ import { existsSync as existsSync29, mkdirSync as mkdirSync21, mkdtempSync as mkdtempSync4, readFileSync as readFileSync25, renameSync as renameSync3, rmSync as rmSync9, statSync as statSync11, unlinkSync, writeFileSync as writeFileSync22 } from "node:fs";
223332
+ import { spawn as spawn6, spawnSync as spawnSync6 } from "node:child_process";
223098
223333
  import { tmpdir as tmpdir4 } from "node:os";
223099
- import { join as join30, relative as relative4, resolve as resolve13 } from "node:path";
223334
+ import { join as join31, relative as relative4, resolve as resolve13 } from "node:path";
223100
223335
  init_util();
223101
223336
  var num = (args, key, fallback, min, max) => {
223102
223337
  const raw = flagStr(args, key);
@@ -223109,13 +223344,13 @@ var num = (args, key, fallback, min, max) => {
223109
223344
  };
223110
223345
  var findToktx = () => {
223111
223346
  const env = process.env.KTX_TOKTX;
223112
- if (env && existsSync28(env))
223347
+ if (env && existsSync29(env))
223113
223348
  return env;
223114
- for (const base of [process.env.LOCALAPPDATA && join30(process.env.LOCALAPPDATA, "Programs"), process.env.ProgramFiles]) {
223349
+ for (const base of [process.env.LOCALAPPDATA && join31(process.env.LOCALAPPDATA, "Programs"), process.env.ProgramFiles]) {
223115
223350
  if (!base)
223116
223351
  continue;
223117
- const guess = join30(base, "KTX-Software", "bin", "toktx.exe");
223118
- if (existsSync28(guess))
223352
+ const guess = join31(base, "KTX-Software", "bin", "toktx.exe");
223353
+ if (existsSync29(guess))
223119
223354
  return guess;
223120
223355
  }
223121
223356
  return "toktx";
@@ -223136,14 +223371,26 @@ var requireToktx = () => {
223136
223371
  }
223137
223372
  return toktx;
223138
223373
  };
223139
- var encodeKtx2 = (toktx, png2, ktx2) => {
223374
+ var encodeKtx2 = (toktx, png2, ktx2) => new Promise((resolve14, reject) => {
223140
223375
  const args = ["--t2", "--encode", "uastc", "--uastc_quality", "2", "--zcmp", "18", "--genmipmap", "--assign_oetf", "srgb", ktx2, png2];
223141
- const p2 = spawnSync6(toktx, args, { encoding: "utf8" });
223142
- if (p2.status !== 0) {
223143
- throw new CliError(`toktx failed (${p2.status ?? p2.error?.message ?? "?"}) while encoding ${ktx2}:
223144
- ${(p2.stderr || p2.stdout || "").trim()}`);
223145
- }
223146
- };
223376
+ const p2 = spawn6(toktx, args, { stdio: ["ignore", "pipe", "pipe"] });
223377
+ let out = "";
223378
+ p2.stdout.on("data", (d) => {
223379
+ out += String(d);
223380
+ });
223381
+ p2.stderr.on("data", (d) => {
223382
+ out += String(d);
223383
+ });
223384
+ p2.on("error", (e2) => reject(new CliError(`toktx failed (${e2.message}) while encoding ${ktx2}`)));
223385
+ p2.on("close", (status2) => {
223386
+ if (status2 === 0)
223387
+ resolve14();
223388
+ else
223389
+ reject(new CliError(`toktx failed (${status2 ?? "?"}) while encoding ${ktx2}:
223390
+ ${out.trim()}`));
223391
+ });
223392
+ });
223393
+ var pageFile = (stem, page, light, ext2 = ".ktx2") => `${stem}${light ? "-light" : ""}${page > 0 ? `_${page}` : ""}${ext2}`;
223147
223394
  var bake = async (args) => {
223148
223395
  let root;
223149
223396
  try {
@@ -223153,20 +223400,31 @@ var bake = async (args) => {
223153
223400
  }
223154
223401
  const toktx = requireToktx();
223155
223402
  const outDir = resolve13(root, flagStr(args, "out") ?? "assets/lightmap");
223403
+ const stem = flagStr(args, "name") ?? "lightmap";
223404
+ if (!/^[A-Za-z0-9_-]+$/.test(stem))
223405
+ throw new CliError(`--name wants a plain file stem (letters, digits, - and _), got "${stem}".`);
223156
223406
  const entryFlag = flagStr(args, "entry");
223157
223407
  const entryFile = entryFlag ?? "main.ts";
223158
- if (!existsSync28(resolve13(root, entryFile))) {
223408
+ if (!existsSync29(resolve13(root, entryFile))) {
223159
223409
  throw new CliError(`No ${entryFile} in ${root} — pass --entry <file> (the file that builds the scene and calls Lightmap.load).`);
223160
223410
  }
223161
223411
  const cfg = {
223162
223412
  outDir: outDir.replace(/\\/g, "/"),
223163
223413
  size: num(args, "size", 4096, 256, 16384),
223164
223414
  texel: num(args, "texel", 0.02, 0.001, 2),
223415
+ pages: num(args, "pages", 4, 1, 16),
223165
223416
  sunRays: num(args, "sun-rays", 32, 1, 1024),
223166
223417
  aoRays: num(args, "ao-rays", 64, 1, 4096),
223167
223418
  aoDistance: num(args, "ao-distance", 2, 0.01, 1000),
223168
223419
  bias: num(args, "bias", 0.02, 0.0001, 1),
223169
223420
  sunAngle: num(args, "sun-angle", 0.5, 0.01, 30),
223421
+ lightRays: num(args, "light-rays", 16, 1, 256),
223422
+ lightRadius: num(args, "light-radius", 0.1, 0.001, 5),
223423
+ bounce: num(args, "bounce", 0, 0, 1),
223424
+ bounceDistance: num(args, "bounce-distance", 8, 0.5, 100),
223425
+ bounceRays: num(args, "bounce-rays", 1, 1, 32),
223426
+ coplanarSkip: num(args, "coplanar-skip", 0.05, 0, 1),
223427
+ denoise: num(args, "denoise", 2, 0, 4),
223170
223428
  volumeCell: flagBool(args, "no-volume") ? 0 : num(args, "cell", 0.5, 0.05, 20)
223171
223429
  };
223172
223430
  const frames = num(args, "frames", 900, 30, 1e5);
@@ -223180,7 +223438,7 @@ var bake = async (args) => {
223180
223438
  import ${JSON.stringify(importPath)}
223181
223439
  `;
223182
223440
  mkdirSync21(outDir, { recursive: true });
223183
- ensurePlaceholders(outDir);
223441
+ ensurePlaceholders(outDir, stem);
223184
223442
  const desktopRenderer = resolveDesktopRenderer(root, args);
223185
223443
  const { js, resources } = await buildBundle(args, undefined, {
223186
223444
  localAssets: true,
@@ -223188,12 +223446,12 @@ import ${JSON.stringify(importPath)}
223188
223446
  extraEntries: [{ path: entryPath, type: "text", text: entryText }],
223189
223447
  entryOverride: entryPath
223190
223448
  });
223191
- const scratch = mkdtempSync4(join30(tmpdir4(), "lecodes-lightmap-"));
223192
- const outPng = join30(scratch, "frame.png");
223193
- note(`Baking ${cfg.size}² at ${(cfg.texel * 100).toFixed(1)} cm texels (${cfg.sunRays} sun + ${cfg.aoRays} AO rays)${cfg.volumeCell ? ` + a ${cfg.volumeCell} m light volume` : ""} → ${relative4(root, outDir) || "."}…`);
223449
+ const scratch = mkdtempSync4(join31(tmpdir4(), "lecodes-lightmap-"));
223450
+ const outPng = join31(scratch, "frame.png");
223451
+ note(`Baking up to ${cfg.pages} × ${cfg.size}² at ${(cfg.texel * 100).toFixed(1)} cm texels (${cfg.sunRays} sun + ${cfg.aoRays} AO + ${cfg.lightRays} light rays)${cfg.volumeCell ? ` + a ${cfg.volumeCell} m light volume` : ""} → ${relative4(root, outDir) || "."}/${stem}.*…`);
223194
223452
  let output = "";
223195
223453
  try {
223196
- const timeoutMs = Number(flagStr(args, "timeout")) || 600000;
223454
+ const timeoutMs = Number(flagStr(args, "timeout")) || 24 * 3600 * 1000;
223197
223455
  const res = await runDesktopRender(js, resources, {
223198
223456
  renderer: desktopRenderer,
223199
223457
  width,
@@ -223211,28 +223469,76 @@ import ${JSON.stringify(importPath)}
223211
223469
  throw e2;
223212
223470
  output = msg;
223213
223471
  } finally {
223214
- rmSync9(scratch, { recursive: true, force: true });
223472
+ try {
223473
+ rmSync9(scratch, { recursive: true, force: true });
223474
+ } catch {}
223215
223475
  }
223216
223476
  const lines = output.split(/\r?\n/).filter((l2) => l2.startsWith("[lightmap]"));
223217
223477
  for (const l2 of lines) {
223218
223478
  const text = l2.slice("[lightmap]".length).trim();
223219
223479
  if (text === "done")
223220
223480
  continue;
223221
- logErr(text.startsWith("error") ? c.red(`✗ ${text}`) : c.dim(` ${text}`));
223481
+ logErr(text.startsWith("error") ? c.red(`✗ ${text}`) : text.startsWith("warning") ? c.yellow(` ${text}`) : c.dim(` ${text}`));
223222
223482
  }
223223
223483
  if (/\[lightmap\] done/.test(output)) {
223224
- const png2 = join30(outDir, "lightmap.png");
223225
- const ktx2 = join30(outDir, "lightmap.ktx2");
223226
- if (!existsSync28(png2))
223227
- throw new CliError(`The host reported a bake but ${png2} is missing.`);
223484
+ const bakePath = join31(outDir, "lightmap.bake");
223485
+ if (!existsSync29(bakePath))
223486
+ throw new CliError(`The host reported a bake but ${bakePath} is missing.`);
223487
+ const data = JSON.parse(readFileSync25(bakePath, "utf8"));
223488
+ const pages = Math.max(1, data.pages ?? 1);
223489
+ const hasLight = !!data.lightTextures?.length;
223228
223490
  const t0 = Date.now();
223229
- encodeKtx2(toktx, png2, ktx2);
223230
- const pngKb = Math.round(statSync10(png2).size / 1024), ktxKb = Math.round(statSync10(ktx2).size / 1024);
223231
- unlinkSync(png2);
223232
- logErr(c.dim(` encoded lightmap.ktx2 (UASTC + mips, ${ktxKb} KB from a ${pngKb} KB raster) in ${((Date.now() - t0) / 1000).toFixed(1)} s`));
223233
- const hasVolume = cfg.volumeCell > 0 && existsSync28(join30(outDir, "lightmap.volume"));
223234
- logErr(c.green(`✓ baked → ${relative4(root, outDir) || "."}/lightmap.ktx2 + lightmap.bake${hasVolume ? " + lightmap.volume" : ""}`));
223235
- logErr(c.dim(` apply it with Lightmap.load(scene, { data: asset('<rel>/${relative4(root, join30(outDir, "lightmap.bake")).replace(/\\/g, "/")}'), texture: asset('<rel>/${relative4(root, ktx2).replace(/\\/g, "/")}')${hasVolume ? `, volume: asset('<rel>/${relative4(root, join30(outDir, "lightmap.volume")).replace(/\\/g, "/")}')` : ""} }) — asset() paths are relative to the calling file`));
223491
+ let pngKb = 0, ktxKb = 0;
223492
+ const encodePage = async (page, light) => {
223493
+ const png2 = join31(outDir, pageFile("lightmap", page, light, ".png"));
223494
+ const ktx2 = join31(outDir, pageFile(stem, page, light));
223495
+ if (!existsSync29(png2))
223496
+ throw new CliError(`The host reported a bake but ${png2} is missing.`);
223497
+ await encodeKtx2(toktx, png2, ktx2);
223498
+ pngKb += statSync11(png2).size / 1024;
223499
+ ktxKb += statSync11(ktx2).size / 1024;
223500
+ unlinkSync(png2);
223501
+ };
223502
+ const encodes = [];
223503
+ for (let p2 = 0;p2 < pages; p2++) {
223504
+ encodes.push(encodePage(p2, false));
223505
+ if (hasLight)
223506
+ encodes.push(encodePage(p2, true));
223507
+ }
223508
+ await Promise.all(encodes);
223509
+ for (let p2 = pages;p2 < 16; p2++) {
223510
+ for (const light of [false, true]) {
223511
+ const stale = join31(outDir, pageFile(stem, p2, light));
223512
+ if (existsSync29(stale) && statSync11(stale).size !== PLACEHOLDER_KTX2.length)
223513
+ writeFileSync22(stale, PLACEHOLDER_KTX2);
223514
+ }
223515
+ }
223516
+ if (!hasLight) {
223517
+ const light = join31(outDir, pageFile(stem, 0, true));
223518
+ if (existsSync29(light) && statSync11(light).size !== PLACEHOLDER_KTX2.length)
223519
+ writeFileSync22(light, PLACEHOLDER_KTX2);
223520
+ }
223521
+ data.texture = pageFile(stem, 0, false);
223522
+ data.textures = Array.from({ length: pages }, (_, p2) => pageFile(stem, p2, false));
223523
+ if (hasLight)
223524
+ data.lightTextures = Array.from({ length: pages }, (_, p2) => pageFile(stem, p2, true));
223525
+ if (data.volume)
223526
+ data.volume.file = `${stem}.volume`;
223527
+ writeFileSync22(join31(outDir, `${stem}.bake`), JSON.stringify(data, null, 1) + `
223528
+ `);
223529
+ if (stem !== "lightmap")
223530
+ unlinkSync(bakePath);
223531
+ const volumeSrc = join31(outDir, "lightmap.volume");
223532
+ const hasVolume = cfg.volumeCell > 0 && existsSync29(volumeSrc);
223533
+ if (hasVolume && stem !== "lightmap") {
223534
+ rmSync9(join31(outDir, `${stem}.volume`), { force: true });
223535
+ renameSync3(volumeSrc, join31(outDir, `${stem}.volume`));
223536
+ }
223537
+ logErr(c.dim(` encoded ${pages} page(s)${hasLight ? " + the light atlas" : ""} (UASTC + mips, ${Math.round(ktxKb)} KB from ${Math.round(pngKb)} KB of rasters) in ${((Date.now() - t0) / 1000).toFixed(1)} s`));
223538
+ logErr(c.green(`✓ baked → ${relative4(root, outDir) || "."}/${stem}.bake + ${data.textures.join(", ")}${hasLight ? ` + ${data.lightTextures.join(", ")}` : ""}${hasVolume ? ` + ${stem}.volume` : ""}`));
223539
+ const rel = (f3) => relative4(root, join31(outDir, f3)).replace(/\\/g, "/");
223540
+ const assets = (files) => files.length === 1 ? `asset('<rel>/${rel(files[0])}')` : `[${files.map((f3) => `asset('<rel>/${rel(f3)}')`).join(", ")}]`;
223541
+ logErr(c.dim(` scene file: env.lightmap: { data: asset('<rel>/${rel(`${stem}.bake`)}'), texture: ${assets(data.textures)}${hasLight ? `, light: ${assets(data.lightTextures)}` : ""}${hasVolume ? `, volume: asset('<rel>/${rel(`${stem}.volume`)}')` : ""} } — asset() paths are relative to the scene file`));
223236
223542
  return;
223237
223543
  }
223238
223544
  if (lines.some((l2) => /no bake module/.test(l2))) {
@@ -223259,30 +223565,34 @@ var placeholderVolume = () => {
223259
223565
  b[37] = 255;
223260
223566
  return b;
223261
223567
  };
223262
- var ensurePlaceholders = (outDir) => {
223263
- const bake2 = join30(outDir, "lightmap.bake");
223264
- const ktx2 = join30(outDir, "lightmap.ktx2");
223265
- const volume = join30(outDir, "lightmap.volume");
223266
- if (!existsSync28(bake2))
223267
- writeFileSync22(bake2, JSON.stringify({ version: 1, placeholder: true, size: 1, texel: 0, instances: [] }) + `
223568
+ var ensurePlaceholders = (outDir, stem) => {
223569
+ const missing = (f3) => !existsSync29(f3) || statSync11(f3).size === 0;
223570
+ const bake2 = join31(outDir, `${stem}.bake`);
223571
+ const ktx2 = join31(outDir, pageFile(stem, 0, false));
223572
+ const light = join31(outDir, pageFile(stem, 0, true));
223573
+ const volume = join31(outDir, `${stem}.volume`);
223574
+ if (missing(bake2))
223575
+ writeFileSync22(bake2, JSON.stringify({ version: 2, placeholder: true, size: 1, texel: 0, pages: 1, instances: [] }) + `
223268
223576
  `);
223269
- if (!existsSync28(ktx2))
223577
+ if (missing(ktx2))
223270
223578
  writeFileSync22(ktx2, PLACEHOLDER_KTX2);
223271
- if (!existsSync28(volume))
223579
+ if (missing(light))
223580
+ writeFileSync22(light, PLACEHOLDER_KTX2);
223581
+ if (missing(volume))
223272
223582
  writeFileSync22(volume, placeholderVolume());
223273
223583
  };
223274
223584
  var lightmap = async (args) => {
223275
223585
  const sub = args._[0];
223276
223586
  if (sub === "bake")
223277
223587
  return bake({ ...args, _: args._.slice(1) });
223278
- throw new CliError("Usage: lecodes lightmap bake [--out assets/lightmap] [--size 4096] [--texel 0.02] [--sun-rays 32] [--ao-rays 64] [--ao-distance 2] [--bias 0.02] [--sun-angle 0.5] [--cell 0.5 | --no-volume] [--frames 900] [--entry main.ts] [--logs] (needs KTX-Software's toktx)");
223588
+ throw new CliError("Usage: lecodes lightmap bake [--out assets/lightmap] [--name lightmap] [--size 4096] [--pages 4] [--texel 0.02] [--sun-rays 32] [--ao-rays 64] [--ao-distance 2] [--bias 0.02] [--sun-angle 0.5] [--light-rays 16] [--light-radius 0.1] [--bounce 0..1] [--denoise 2] [--cell 0.5 | --no-volume] [--frames 900] [--timeout <ms>] [--entry main.ts] [--logs] (needs KTX-Software's toktx)");
223279
223589
  };
223280
223590
 
223281
223591
  // src/commands/navmesh.ts
223282
223592
  init_manifest();
223283
- import { existsSync as existsSync29, mkdirSync as mkdirSync22, mkdtempSync as mkdtempSync5, readdirSync as readdirSync14, readFileSync as readFileSync25, rmSync as rmSync10, statSync as statSync11, unlinkSync as unlinkSync2, writeFileSync as writeFileSync23 } from "node:fs";
223593
+ import { existsSync as existsSync30, mkdirSync as mkdirSync22, mkdtempSync as mkdtempSync5, readdirSync as readdirSync15, readFileSync as readFileSync26, rmSync as rmSync10, statSync as statSync12, unlinkSync as unlinkSync2, writeFileSync as writeFileSync23 } from "node:fs";
223284
223594
  import { tmpdir as tmpdir5 } from "node:os";
223285
- import { basename as basename16, join as join31, relative as relative5, resolve as resolve14 } from "node:path";
223595
+ import { basename as basename17, join as join32, relative as relative5, resolve as resolve14 } from "node:path";
223286
223596
  init_util();
223287
223597
 
223288
223598
  // ../creator-nav/dist/creatorNav.js
@@ -223299,8 +223609,8 @@ var createCreatorNav = (() => {
223299
223609
  var ENVIRONMENT_IS_WORKER = typeof WorkerGlobalScope != "undefined";
223300
223610
  var ENVIRONMENT_IS_NODE = typeof process == "object" && process.versions?.node && process.type != "renderer";
223301
223611
  if (ENVIRONMENT_IS_NODE) {
223302
- const { createRequire: createRequire4 } = await import("module");
223303
- var require2 = createRequire4(import.meta.url);
223612
+ const { createRequire: createRequire6 } = await import("module");
223613
+ var require2 = createRequire6(import.meta.url);
223304
223614
  }
223305
223615
  var arguments_ = [];
223306
223616
  var thisProgram = "./this.program";
@@ -224079,13 +224389,13 @@ var num2 = (args, key, fallback, min, max) => {
224079
224389
  return n2;
224080
224390
  };
224081
224391
  var guessName = (root) => {
224082
- const dirs = [join31(root, "src"), root];
224392
+ const dirs = [join32(root, "src"), root];
224083
224393
  for (const dir of dirs) {
224084
- if (!existsSync29(dir))
224394
+ if (!existsSync30(dir))
224085
224395
  continue;
224086
224396
  let entries;
224087
224397
  try {
224088
- entries = readdirSync14(dir);
224398
+ entries = readdirSync15(dir);
224089
224399
  } catch {
224090
224400
  continue;
224091
224401
  }
@@ -224093,7 +224403,7 @@ var guessName = (root) => {
224093
224403
  if (!f3.endsWith(".scene.ts"))
224094
224404
  continue;
224095
224405
  try {
224096
- if (/\bnavmesh\s*:/.test(readFileSync25(join31(dir, f3), "utf8")))
224406
+ if (/\bnavmesh\s*:/.test(readFileSync26(join32(dir, f3), "utf8")))
224097
224407
  return f3.slice(0, -".scene.ts".length);
224098
224408
  } catch {}
224099
224409
  }
@@ -224136,7 +224446,7 @@ var bake2 = async (args) => {
224136
224446
  const outDir = resolve14(root, flagStr(args, "out") ?? "assets/nav");
224137
224447
  const entryFlag = flagStr(args, "entry");
224138
224448
  const entryFile = entryFlag ?? "main.ts";
224139
- if (!existsSync29(resolve14(root, entryFile))) {
224449
+ if (!existsSync30(resolve14(root, entryFile))) {
224140
224450
  throw new CliError(`No ${entryFile} in ${root} — pass --entry <file> (the file that builds the level: a scene file with env.navmesh, or code calling NavMesh.load).`);
224141
224451
  }
224142
224452
  const name = flagStr(args, "name") ?? guessName(root) ?? "scene";
@@ -224148,8 +224458,8 @@ var bake2 = async (args) => {
224148
224458
  const keepGeometry = flagBool(args, "keep-geometry");
224149
224459
  const obj = flagBool(args, "obj");
224150
224460
  mkdirSync22(outDir, { recursive: true });
224151
- const navmeshPath = join31(outDir, `${name}.navmesh`);
224152
- if (!existsSync29(navmeshPath))
224461
+ const navmeshPath = join32(outDir, `${name}.navmesh`);
224462
+ if (!existsSync30(navmeshPath))
224153
224463
  writeFileSync23(navmeshPath, placeholderLnav());
224154
224464
  const importPath = "./" + entryFile.replace(/\\/g, "/").replace(/\.tsx?$/, "");
224155
224465
  const entryPath = "/.navmesh-entry.ts";
@@ -224164,8 +224474,8 @@ import ${JSON.stringify(importPath)}
224164
224474
  extraEntries: [{ path: entryPath, type: "text", text: entryText }],
224165
224475
  entryOverride: entryPath
224166
224476
  });
224167
- const scratch = mkdtempSync5(join31(tmpdir5(), "lecodes-navmesh-"));
224168
- const outPng = join31(scratch, "frame.png");
224477
+ const scratch = mkdtempSync5(join32(tmpdir5(), "lecodes-navmesh-"));
224478
+ const outPng = join32(scratch, "frame.png");
224169
224479
  note(`Dumping the level's static collision geometry through the desktop host → ${relative5(root, outDir) || "."}/${name}.navgeo…`);
224170
224480
  let output = "";
224171
224481
  try {
@@ -224218,10 +224528,10 @@ import ${JSON.stringify(importPath)}
224218
224528
  }
224219
224529
  if (!config)
224220
224530
  throw new CliError("The host reported a dump but no `[navmesh] config` line — SDK/host mismatch?");
224221
- const geoPath = join31(outDir, `${name}.navgeo`);
224222
- if (!existsSync29(geoPath))
224531
+ const geoPath = join32(outDir, `${name}.navgeo`);
224532
+ if (!existsSync30(geoPath))
224223
224533
  throw new CliError(`The host reported a dump but ${geoPath} is missing.`);
224224
- const geo = readNgeo(new Uint8Array(readFileSync25(geoPath)));
224534
+ const geo = readNgeo(new Uint8Array(readFileSync26(geoPath)));
224225
224535
  const params = {
224226
224536
  agentRadius: num2(args, "radius", config.agent.radius, 0.05, 10),
224227
224537
  agentHeight: num2(args, "height", config.agent.height, 0.1, 20),
@@ -224269,18 +224579,18 @@ import ${JSON.stringify(importPath)}
224269
224579
  }
224270
224580
  writeFileSync23(navmeshPath, result.data);
224271
224581
  if (obj) {
224272
- const objPath = join31(outDir, `${name}.navmesh.obj`);
224582
+ const objPath = join32(outDir, `${name}.navmesh.obj`);
224273
224583
  writeObj(objPath, navMeshTriangles(mod, result.data));
224274
224584
  logErr(c.dim(` wrote ${relative5(root, objPath)} (the polygon mesh, for inspection)`));
224275
224585
  }
224276
224586
  if (!keepGeometry)
224277
224587
  unlinkSync2(geoPath);
224278
- const kb = Math.round(statSync11(navmeshPath).size / 1024);
224588
+ const kb = Math.round(statSync12(navmeshPath).size / 1024);
224279
224589
  const b = result.bounds;
224280
224590
  logErr(c.dim(` ${result.tiles} tiles, ${result.polys} polygons, ${(b.max[0] - b.min[0]).toFixed(0)} × ${(b.max[2] - b.min[2]).toFixed(0)} m, ${result.time.toFixed(1)} s`));
224281
224591
  logErr(c.green(`✓ baked → ${relative5(root, navmeshPath).replace(/\\/g, "/")} (${kb} KB)`));
224282
224592
  if (!config.data) {
224283
- logErr(c.dim(` load it with env.navmesh: { data: asset('<rel>/${basename16(navmeshPath)}') } in the scene file, or NavMesh.load(scene, { data: asset(...) })`));
224593
+ logErr(c.dim(` load it with env.navmesh: { data: asset('<rel>/${basename17(navmeshPath)}') } in the scene file, or NavMesh.load(scene, { data: asset(...) })`));
224284
224594
  }
224285
224595
  };
224286
224596
  var navmesh = async (args) => {
@@ -224307,7 +224617,7 @@ var login = async (args) => {
224307
224617
 
224308
224618
  // src/commands/shaders.ts
224309
224619
  init_manifest();
224310
- import { readFileSync as readFileSync26 } from "node:fs";
224620
+ import { readFileSync as readFileSync27 } from "node:fs";
224311
224621
  init_util();
224312
224622
  init_compile();
224313
224623
  var collectShaders = () => {
@@ -224339,7 +224649,7 @@ var status2 = async () => {
224339
224649
  if (matcId === null)
224340
224650
  state = c.dim("unknown (matc not downloaded)");
224341
224651
  else {
224342
- const text = readFileSync26(shader.absPath, "utf8");
224652
+ const text = readFileSync27(shader.absPath, "utf8");
224343
224653
  if (shaderIsCompiled(root, text, matcId))
224344
224654
  state = "compiled";
224345
224655
  else {
@@ -224348,7 +224658,7 @@ var status2 = async () => {
224348
224658
  }
224349
224659
  }
224350
224660
  log(` ${shader.path.padEnd(width)} ${state}`);
224351
- const schema = parseShaderSchema(readFileSync26(shader.absPath, "utf8"));
224661
+ const schema = parseShaderSchema(readFileSync27(shader.absPath, "utf8"));
224352
224662
  for (const p2 of schema.params) {
224353
224663
  const range = p2.min !== undefined ? ` [${p2.min}..${p2.max}]` : "";
224354
224664
  const def = p2.default !== null && p2.default !== undefined ? ` = ${JSON.stringify(p2.default)}` : "";
@@ -224391,7 +224701,7 @@ var compile3 = async (args) => {
224391
224701
  }
224392
224702
  const matcExe = await resolveMatcExe();
224393
224703
  const matcId = matcIdentity(matcExe);
224394
- const wasCompiled = new Map(shaders.map((s2) => [s2.path, shaderIsCompiled(root, readFileSync26(s2.absPath, "utf8"), matcId)]));
224704
+ const wasCompiled = new Map(shaders.map((s2) => [s2.path, shaderIsCompiled(root, readFileSync27(s2.absPath, "utf8"), matcId)]));
224395
224705
  const errors2 = [];
224396
224706
  for (const shader of shaders) {
224397
224707
  try {
@@ -224425,8 +224735,8 @@ var shaders = async (args) => {
224425
224735
  };
224426
224736
 
224427
224737
  // src/commands/render.ts
224428
- import { mkdirSync as mkdirSync23, readFileSync as readFileSync28, writeFileSync as writeFileSync24 } from "node:fs";
224429
- import { dirname as dirname15, resolve as resolve17 } from "node:path";
224738
+ import { mkdirSync as mkdirSync23, readFileSync as readFileSync29, writeFileSync as writeFileSync24 } from "node:fs";
224739
+ import { dirname as dirname16, resolve as resolve17 } from "node:path";
224430
224740
 
224431
224741
  // src/compile/headlessBundle.ts
224432
224742
  init_api();
@@ -224510,8 +224820,8 @@ var compileHeadlessBundle = async (args, opts = {}) => {
224510
224820
  // src/compile/screenEntry.ts
224511
224821
  init_designMeta();
224512
224822
  init_util();
224513
- import { existsSync as existsSync30, readFileSync as readFileSync27, statSync as statSync12 } from "node:fs";
224514
- import { isAbsolute, join as join32, relative as relative6, resolve as resolve15 } from "node:path";
224823
+ import { existsSync as existsSync31, readFileSync as readFileSync28, statSync as statSync13 } from "node:fs";
224824
+ import { isAbsolute, join as join33, relative as relative6, resolve as resolve15 } from "node:path";
224515
224825
  var SCREEN_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs"];
224516
224826
  var SCREEN_ENTRY_PATH = "/.screen-entry.ts";
224517
224827
  var DEFAULT_DESIGN_DIR = "design";
@@ -224520,13 +224830,13 @@ var hasSeparator = (file) => file.includes("/") || file.includes("\\");
224520
224830
  var findScreenFile = (root, file, dirFlag) => {
224521
224831
  const candidates = [resolve15(process.cwd(), file)];
224522
224832
  if (!hasSeparator(file)) {
224523
- candidates.push(join32(root, (dirFlag ?? DEFAULT_DESIGN_DIR).replace(/[/\\]+$/, ""), "screens", file));
224833
+ candidates.push(join33(root, (dirFlag ?? DEFAULT_DESIGN_DIR).replace(/[/\\]+$/, ""), "screens", file));
224524
224834
  }
224525
224835
  for (const base of candidates) {
224526
- if (existsSync30(base) && statSync12(base).isFile())
224836
+ if (existsSync31(base) && statSync13(base).isFile())
224527
224837
  return base;
224528
224838
  for (const ext2 of SCREEN_EXTS)
224529
- if (existsSync30(base + ext2))
224839
+ if (existsSync31(base + ext2))
224530
224840
  return base + ext2;
224531
224841
  }
224532
224842
  throw new CliError(`No screen file at "${file}".` + (hasSeparator(file) ? "" : ` Pass a path, or a screen id that exists in ${dirFlag ?? DEFAULT_DESIGN_DIR}/screens/.`));
@@ -224535,8 +224845,8 @@ var designDirOf = (root, key) => {
224535
224845
  const m = /^\/(.*)screens\/([^/]+)\.[^./]+$/.exec(key);
224536
224846
  if (!m)
224537
224847
  return null;
224538
- const dir = join32(root, m[1]);
224539
- return existsSync30(join32(dir, "meta.json")) ? { dir, id: m[2] } : null;
224848
+ const dir = join33(root, m[1]);
224849
+ return existsSync31(join33(dir, "meta.json")) ? { dir, id: m[2] } : null;
224540
224850
  };
224541
224851
  var resolveScreenTarget = (root, file, opts = {}) => {
224542
224852
  const abs = findScreenFile(root, file, opts.dir);
@@ -224545,7 +224855,7 @@ var resolveScreenTarget = (root, file, opts = {}) => {
224545
224855
  throw new CliError(`"${file}" is outside the project (${root}) — a screen renders as part of its project, so its ` + `imports and asset() files resolve from the same tree.`);
224546
224856
  }
224547
224857
  const key = toKey(rel);
224548
- if (!/^\s*export\s+default\b/m.test(readFileSync27(abs, "utf8"))) {
224858
+ if (!/^\s*export\s+default\b/m.test(readFileSync28(abs, "utf8"))) {
224549
224859
  throw new CliError(`${key.slice(1)} has no default export. A screen file ends with ` + `\`export default UIScreen([...])\` — or \`export default (state) => UIScreen([...])\` for screen states.`);
224550
224860
  }
224551
224861
  const invoke = opts.state !== undefined ? `screen(${JSON.stringify(opts.state)})` : "screen()";
@@ -224852,7 +225162,7 @@ var evaluateDesktopRun = (plan, output) => {
224852
225162
  if (!error && st.expectLog !== undefined) {
224853
225163
  const seen = consoleLines.slice(0, cutoff);
224854
225164
  if (!seen.some((l2) => l2.includes(st.expectLog))) {
224855
- const tail = seen.filter((l2) => l2.startsWith("[DEBUG]") || l2.startsWith("[INFO]") || l2.startsWith("[WARN]") || l2.startsWith("[ERROR]")).slice(-3);
225165
+ const tail = seen.filter((l2) => l2.startsWith("[DEBUG]") || l2.startsWith("[INFO]") || l2.startsWith("[WARN]") || l2.startsWith("[ERROR]")).slice(-8);
224856
225166
  error = `expected a console line containing "${st.expectLog}" — ${tail.length === 0 ? "the app logged nothing" : `last lines: ${tail.map((l2) => `"${l2}"`).join(", ")}`}`;
224857
225167
  }
224858
225168
  }
@@ -224895,7 +225205,7 @@ var requireRenderer = async () => {
224895
225205
  var fileScenarioIO = (dir) => ({
224896
225206
  write: (relPath, data) => {
224897
225207
  const abs = resolve17(dir, relPath);
224898
- mkdirSync23(dirname15(abs), { recursive: true });
225208
+ mkdirSync23(dirname16(abs), { recursive: true });
224899
225209
  writeFileSync24(abs, data);
224900
225210
  }
224901
225211
  });
@@ -224906,7 +225216,7 @@ var resolveFixtures = (value, baseDir) => {
224906
225216
  if (typeof value === "string") {
224907
225217
  const abs = resolve17(baseDir, value);
224908
225218
  try {
224909
- map = JSON.parse(readFileSync28(abs, "utf8"));
225219
+ map = JSON.parse(readFileSync29(abs, "utf8"));
224910
225220
  } catch (e2) {
224911
225221
  throw new CliError(`Can't read fixtures "${value}": ${e2 instanceof Error ? e2.message : String(e2)}`);
224912
225222
  }
@@ -224919,7 +225229,7 @@ var resolveFixtures = (value, baseDir) => {
224919
225229
  var readScenarioFile = (path2) => {
224920
225230
  let raw;
224921
225231
  try {
224922
- raw = JSON.parse(readFileSync28(resolve17(path2), "utf8"));
225232
+ raw = JSON.parse(readFileSync29(resolve17(path2), "utf8"));
224923
225233
  } catch (e2) {
224924
225234
  throw new CliError(`Can't read scenario "${path2}": ${e2 instanceof Error ? e2.message : String(e2)}`);
224925
225235
  }
@@ -224970,23 +225280,25 @@ var renderDesktop = async (args) => {
224970
225280
  if ((camera || view) && scenario)
224971
225281
  throw new CliError('--camera/--view apply to a plain render; in a --script run use a { "view": "iso" } or { "camera": { "at", "lookAt" } } step before the screenshot.');
224972
225282
  const desktopRenderer = resolveDesktopRenderer(root, args);
225283
+ const { dirs: pluginDirs } = desktopPluginLibraries(root);
224973
225284
  const { js, resources } = await buildBundle(args, undefined, {
224974
225285
  localAssets: true,
224975
225286
  desktopRenderer,
224976
225287
  extraEntries: screen2 ? [screen2.entry] : undefined,
224977
225288
  entryOverride: screen2?.entryPath
224978
225289
  });
224979
- mkdirSync23(dirname15(resolve17(pngFlag)), { recursive: true });
225290
+ mkdirSync23(dirname16(resolve17(pngFlag)), { recursive: true });
224980
225291
  if (scenario) {
224981
225292
  const outDir = flagStr(args, "out-dir") ?? ".";
224982
225293
  const plan = translateScenario(scenario.steps, { outDir, headroomFrames: frames });
224983
225294
  for (const st of plan.steps)
224984
225295
  if (st.screenshot)
224985
- mkdirSync23(dirname15(st.screenshot), { recursive: true });
225296
+ mkdirSync23(dirname16(st.screenshot), { recursive: true });
224986
225297
  const timeoutMs2 = Number(flagStr(args, "timeout")) || 60000 + plan.steps.length * 5000;
224987
225298
  note(`Running ${plan.steps.length} step${plan.steps.length === 1 ? "" : "s"} (native)…`);
224988
225299
  const { output } = await runDesktopRender(js, resources, {
224989
225300
  renderer: desktopRenderer,
225301
+ pluginDirs,
224990
225302
  width,
224991
225303
  height,
224992
225304
  frames: plan.frameCap,
@@ -225019,6 +225331,7 @@ var renderDesktop = async (args) => {
225019
225331
  note(screen2 ? `Rendering ${screen2.label} (native)…` : "Rendering (native)…");
225020
225332
  await runDesktopRender(js, resources, {
225021
225333
  renderer: desktopRenderer,
225334
+ pluginDirs,
225022
225335
  width,
225023
225336
  height,
225024
225337
  frames,
@@ -225086,7 +225399,7 @@ var render = async (args) => {
225086
225399
  const settleStr = flagStr(args, "settle") ?? (scenario?.settle !== undefined ? String(scenario.settle) : undefined);
225087
225400
  const timeStr = flagStr(args, "time");
225088
225401
  const onConsole = flagBool(args, "logs") ? (level, text2) => logErr(`${c.dim(`[${level}]`)} ${text2}`) : undefined;
225089
- const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname15(resolve17(scriptFlag)) : ".");
225402
+ const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname16(resolve17(scriptFlag)) : ".");
225090
225403
  const flagFixtures = resolveFixtures(flagStr(args, "fixtures"), ".");
225091
225404
  const fixtures = envelopeFixtures || flagFixtures ? { ...envelopeFixtures, ...flagFixtures } : undefined;
225092
225405
  if (fixtures) {
@@ -225133,6 +225446,26 @@ var render = async (args) => {
225133
225446
  session.dispose();
225134
225447
  }
225135
225448
  }
225449
+ const mapFlag = flagStr(args, "map") ?? (flagBool(args, "map") ? "map.png" : undefined);
225450
+ if (mapFlag !== undefined) {
225451
+ note(`Rendering map…`);
225452
+ const title = root.split(/[\\/]/).pop();
225453
+ const { png: png2, data, warnings: mapWarnings } = await renderer.renderToMap(js, {
225454
+ ...timing,
225455
+ maxPx: Number(flagStr(args, "max-px")) || undefined,
225456
+ title,
225457
+ labelAll: flagBool(args, "label-all")
225458
+ });
225459
+ for (const w of [...warnings, ...mapWarnings])
225460
+ warnErr(w);
225461
+ const pngPath = mapFlag.endsWith(".png") ? mapFlag : mapFlag + ".png";
225462
+ mkdirSync23(dirname16(resolve17(pngPath)), { recursive: true });
225463
+ writeFileSync24(resolve17(pngPath), png2);
225464
+ const jsonPath = pngPath.slice(0, -4) + ".json";
225465
+ writeFileSync24(resolve17(jsonPath), JSON.stringify(data, null, 2));
225466
+ note(`Wrote ${pngPath} + ${jsonPath}`);
225467
+ return done((cb) => cb());
225468
+ }
225136
225469
  const pngFlag = flagStr(args, "png") ?? (flagBool(args, "png") ? "screenshot.png" : undefined);
225137
225470
  if (pngFlag !== undefined) {
225138
225471
  note(screen2 ? `Rendering ${screen2.label} (PNG)…` : "Rendering PNG…");
@@ -225149,7 +225482,7 @@ var render = async (args) => {
225149
225482
  if (isNullSink(pngFlag)) {
225150
225483
  note(`Discarded PNG (${pngFlag})`);
225151
225484
  } else {
225152
- mkdirSync23(dirname15(resolve17(pngFlag)), { recursive: true });
225485
+ mkdirSync23(dirname16(resolve17(pngFlag)), { recursive: true });
225153
225486
  writeFileSync24(resolve17(pngFlag), png2);
225154
225487
  note(`Wrote ${pngFlag} (${png2.length} bytes, ${outW}x${outH}${clip !== undefined ? ", clipped" : ""})`);
225155
225488
  }
@@ -225180,8 +225513,8 @@ init_api();
225180
225513
  init_config();
225181
225514
  init_manifest();
225182
225515
  init_util();
225183
- import { appendFileSync as appendFileSync2, existsSync as existsSync31, readFileSync as readFileSync29, writeFileSync as writeFileSync25 } from "node:fs";
225184
- import { join as join33 } from "node:path";
225516
+ import { appendFileSync as appendFileSync2, existsSync as existsSync32, readFileSync as readFileSync30, writeFileSync as writeFileSync25 } from "node:fs";
225517
+ import { join as join34 } from "node:path";
225185
225518
  var ENV_VAR = "LECODES_PUSH_KEY";
225186
225519
  var context = () => {
225187
225520
  const root = findProjectRoot(process.cwd());
@@ -225325,9 +225658,9 @@ var pnKeysList = async (ctx) => {
225325
225658
  };
225326
225659
  var writeEnv = (root, key) => {
225327
225660
  const touched = [];
225328
- const envPath = join33(root, ".env");
225661
+ const envPath = join34(root, ".env");
225329
225662
  const line = `${ENV_VAR}=${key}`;
225330
- const existing = existsSync31(envPath) ? readFileSync29(envPath, "utf8") : "";
225663
+ const existing = existsSync32(envPath) ? readFileSync30(envPath, "utf8") : "";
225331
225664
  if (new RegExp(`^${ENV_VAR}=`, "m").test(existing)) {
225332
225665
  writeFileSync25(envPath, existing.replace(new RegExp(`^${ENV_VAR}=.*$`, "m"), line));
225333
225666
  } else {
@@ -225338,8 +225671,8 @@ var writeEnv = (root, key) => {
225338
225671
  `);
225339
225672
  }
225340
225673
  touched.push(".env");
225341
- const ignorePath = join33(root, ".gitignore");
225342
- const ignore = existsSync31(ignorePath) ? readFileSync29(ignorePath, "utf8") : "";
225674
+ const ignorePath = join34(root, ".gitignore");
225675
+ const ignore = existsSync32(ignorePath) ? readFileSync30(ignorePath, "utf8") : "";
225343
225676
  if (!ignore.split(`
225344
225677
  `).some((l2) => l2.trim() === ".env")) {
225345
225678
  const separator = !ignore || ignore.endsWith(`
@@ -225438,8 +225771,8 @@ var pn = async (args) => {
225438
225771
  init_api();
225439
225772
  init_config();
225440
225773
  init_manifest();
225441
- import { existsSync as existsSync32, mkdirSync as mkdirSync24, rmSync as rmSync11, writeFileSync as writeFileSync26 } from "node:fs";
225442
- import { dirname as dirname16, join as join34 } from "node:path";
225774
+ import { existsSync as existsSync33, mkdirSync as mkdirSync24, rmSync as rmSync11, writeFileSync as writeFileSync26 } from "node:fs";
225775
+ import { dirname as dirname17, join as join35 } from "node:path";
225443
225776
  init_types4();
225444
225777
  init_localFiles();
225445
225778
  init_util();
@@ -225462,8 +225795,8 @@ var pull = async (args) => {
225462
225795
  const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a2) => a2.id === id)?.type !== "folder").map(([, path2]) => path2));
225463
225796
  for (const path2 of Object.keys(manifest.files)) {
225464
225797
  if (!remotePaths.has(path2)) {
225465
- const abs = join34(root, ...path2.replace(/^\//, "").split("/"));
225466
- if (existsSync32(abs))
225798
+ const abs = join35(root, ...path2.replace(/^\//, "").split("/"));
225799
+ if (existsSync33(abs))
225467
225800
  rmSync11(abs);
225468
225801
  }
225469
225802
  }
@@ -225501,8 +225834,8 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
225501
225834
  const files = { ...manifest.files };
225502
225835
  for (const asset of targets) {
225503
225836
  const key = pathById.get(asset.id);
225504
- const abs = join34(root, ...key.replace(/^\//, "").split("/"));
225505
- mkdirSync24(dirname16(abs), { recursive: true });
225837
+ const abs = join35(root, ...key.replace(/^\//, "").split("/"));
225838
+ mkdirSync24(dirname17(abs), { recursive: true });
225506
225839
  let sha;
225507
225840
  if (asset.type === "file") {
225508
225841
  if (!asset.file?.src)
@@ -225525,8 +225858,8 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
225525
225858
 
225526
225859
  // src/commands/scene.ts
225527
225860
  init_manifest();
225528
- import { existsSync as existsSync34, readFileSync as readFileSync31, readdirSync as readdirSync15, statSync as statSync13, writeFileSync as writeFileSync28 } from "node:fs";
225529
- import { basename as basename17, dirname as dirname17, join as join36, resolve as resolve18 } from "node:path";
225861
+ import { existsSync as existsSync35, readFileSync as readFileSync32, readdirSync as readdirSync16, statSync as statSync14, writeFileSync as writeFileSync28 } from "node:fs";
225862
+ import { basename as basename18, dirname as dirname18, join as join37, resolve as resolve18 } from "node:path";
225530
225863
 
225531
225864
  // src/compile/sceneCompile.ts
225532
225865
  init_localFiles();
@@ -225535,18 +225868,18 @@ init_distRoot();
225535
225868
  init_util();
225536
225869
  init_projectCompile();
225537
225870
  import { createHash as createHash8 } from "node:crypto";
225538
- import { existsSync as existsSync33, mkdirSync as mkdirSync25, readFileSync as readFileSync30, writeFileSync as writeFileSync27 } from "node:fs";
225539
- import { join as join35 } from "node:path";
225871
+ import { existsSync as existsSync34, mkdirSync as mkdirSync25, readFileSync as readFileSync31, writeFileSync as writeFileSync27 } from "node:fs";
225872
+ import { join as join36 } from "node:path";
225540
225873
  var TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i;
225541
225874
  var rel = (key) => key.replace(/^\//, "");
225542
225875
  var resolveHarness = (loadSceneHarness2) => {
225543
- const inRepo = join35(distRoot, "..", "projects", "src", "scene-editor");
225876
+ const inRepo = join36(distRoot, "..", "projects", "src", "scene-editor");
225544
225877
  try {
225545
- return loadSceneHarness2(existsSync33(inRepo) ? inRepo : undefined);
225878
+ return loadSceneHarness2(existsSync34(inRepo) ? inRepo : undefined);
225546
225879
  } catch {
225547
- const vendored = join35(distRoot, "runtime", "scene-harness.json");
225548
- if (existsSync33(vendored))
225549
- return JSON.parse(readFileSync30(vendored, "utf8"));
225880
+ const vendored = join36(distRoot, "runtime", "scene-harness.json");
225881
+ if (existsSync34(vendored))
225882
+ return JSON.parse(readFileSync31(vendored, "utf8"));
225550
225883
  throw new CliError("scene-editor harness sources not found — reinstall the CLI or set LECODES_SCENE_HARNESS_DIR.");
225551
225884
  }
225552
225885
  };
@@ -225558,16 +225891,16 @@ var compileSceneEditor = async (opts) => {
225558
225891
  for (const file of scan2.files) {
225559
225892
  const path2 = "./" + rel(file.path);
225560
225893
  if (TEXT_EXT.test(path2)) {
225561
- entries.push({ path: path2, type: "text", text: readFileSync30(file.absPath, "utf8") });
225894
+ entries.push({ path: path2, type: "text", text: readFileSync31(file.absPath, "utf8") });
225562
225895
  } else if (/\.mat$/i.test(path2)) {
225563
- entries.push({ path: path2, type: "shader", text: readFileSync30(file.absPath, "utf8"), absPath: file.absPath });
225896
+ entries.push({ path: path2, type: "shader", text: readFileSync31(file.absPath, "utf8"), absPath: file.absPath });
225564
225897
  } else {
225565
225898
  entries.push({ path: path2, type: "resource", fileSrc: "/project/" + rel(file.path) });
225566
225899
  }
225567
225900
  }
225568
225901
  const shaderEntries = entries.filter((e2) => e2.type === "shader");
225569
225902
  if (shaderEntries.length > 0) {
225570
- const { warnings } = await applyLocalShaders(shaderEntries, { root, srcPrefix: "/project/.lecodes/shaders", platforms: ["webgl", "webgl-lite"] });
225903
+ const { warnings } = await applyLocalShaders(shaderEntries, { root, srcPrefix: "/project/.lecodes/shaders", platforms: ["opengl", "webgl-lite"] });
225571
225904
  for (const w of warnings)
225572
225905
  warnErr(w);
225573
225906
  const { buildLiteMaterialArtifact: buildLiteMaterialArtifact2 } = await Promise.resolve().then(() => (init_compile(), exports_compile));
@@ -225578,10 +225911,10 @@ var compileSceneEditor = async (opts) => {
225578
225911
  const dir = shadersCacheDir(root);
225579
225912
  mkdirSync25(dir, { recursive: true });
225580
225913
  const liteName = `${hash}_webgl-lite.filamat`;
225581
- if (!existsSync33(join35(dir, liteName)))
225582
- writeFileSync27(join35(dir, liteName), JSON.stringify(buildLiteMaterialArtifact2(e2.text ?? "")));
225914
+ if (!existsSync34(join36(dir, liteName)))
225915
+ writeFileSync27(join36(dir, liteName), JSON.stringify(buildLiteMaterialArtifact2(e2.text ?? "")));
225583
225916
  const src = `/project/.lecodes/shaders/${hash}_opengl.filamat`;
225584
- e2.shaders = ["opengl", "webgl", "webgl-lite"].map((platform) => ({ platform, src }));
225917
+ e2.shaders = ["opengl", "webgl-lite"].map((platform) => ({ platform, src }));
225585
225918
  }
225586
225919
  }
225587
225920
  const scene = scenePath.startsWith("./") ? scenePath : "./" + rel(scenePath);
@@ -225613,14 +225946,21 @@ var DEFAULT_PORT3 = 4488;
225613
225946
  var findAppRoot = (start) => {
225614
225947
  let dir = start;
225615
225948
  while (true) {
225616
- if (existsSync34(join36(dir, "app.json")))
225949
+ if (existsSync35(join37(dir, "app.json")))
225617
225950
  return dir;
225618
- const parent = dirname17(dir);
225951
+ const parent = dirname18(dir);
225619
225952
  if (parent === dir)
225620
225953
  return null;
225621
225954
  dir = parent;
225622
225955
  }
225623
225956
  };
225957
+ var projectRootOf = (dir) => {
225958
+ try {
225959
+ return findProjectRoot(dir);
225960
+ } catch {
225961
+ return findAppRoot(dir) ?? dir;
225962
+ }
225963
+ };
225624
225964
  var getContext2 = (args) => {
225625
225965
  let root;
225626
225966
  let manifest;
@@ -225633,25 +225973,25 @@ var getContext2 = (args) => {
225633
225973
  const pathArg = args._[0];
225634
225974
  if (pathArg) {
225635
225975
  const abs = resolve18(process.cwd(), pathArg);
225636
- if (existsSync34(abs) && statSync13(abs).isDirectory())
225976
+ if (existsSync35(abs) && statSync14(abs).isDirectory())
225637
225977
  root = abs;
225638
- else if (abs.endsWith(".scene.ts") && existsSync34(abs))
225639
- root = findAppRoot(dirname17(abs)) ?? dirname17(abs);
225978
+ else if (abs.endsWith(".scene.ts") && existsSync35(abs))
225979
+ root = findAppRoot(dirname18(abs)) ?? dirname18(abs);
225640
225980
  else
225641
225981
  throw new CliError(`No such folder or .scene.ts: ${pathArg}`);
225642
225982
  } else {
225643
225983
  root = findAppRoot(root) ?? root;
225644
225984
  }
225645
225985
  }
225646
- return { root, manifest, name: manifest?.name ?? basename17(root) };
225986
+ return { root, manifest, name: manifest?.name ?? basename18(root) };
225647
225987
  };
225648
225988
  var writeMcpConfig2 = (root, port) => {
225649
- const path2 = join36(root, ".mcp.json");
225989
+ const path2 = join37(root, ".mcp.json");
225650
225990
  const entry = { type: "http", url: `http://localhost:${port}/mcp` };
225651
225991
  let config = {};
225652
- if (existsSync34(path2)) {
225992
+ if (existsSync35(path2)) {
225653
225993
  try {
225654
- config = JSON.parse(readFileSync31(path2, "utf8"));
225994
+ config = JSON.parse(readFileSync32(path2, "utf8"));
225655
225995
  } catch {
225656
225996
  warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-scene server by hand.");
225657
225997
  return;
@@ -225670,11 +226010,11 @@ var writeMcpConfig2 = (root, port) => {
225670
226010
  };
225671
226011
  var countScenes = (dir) => {
225672
226012
  let n2 = 0;
225673
- for (const name of readdirSync15(dir)) {
226013
+ for (const name of readdirSync16(dir)) {
225674
226014
  if (name === "node_modules" || name.startsWith("."))
225675
226015
  continue;
225676
- const full = join36(dir, name);
225677
- if (statSync13(full).isDirectory())
226016
+ const full = join37(dir, name);
226017
+ if (statSync14(full).isDirectory())
225678
226018
  n2 += countScenes(full);
225679
226019
  else if (name.endsWith(".scene.ts"))
225680
226020
  n2++;
@@ -225715,13 +226055,170 @@ var scene = async (args) => {
225715
226055
  openBrowser(server2.url);
225716
226056
  };
225717
226057
 
226058
+ // src/commands/thumbs.ts
226059
+ import { createRequire as createRequire6 } from "node:module";
226060
+ import { execFileSync as execFileSync7 } from "node:child_process";
226061
+ import { existsSync as existsSync36, mkdtempSync as mkdtempSync6, readdirSync as readdirSync17, rmSync as rmSync12, statSync as statSync15 } from "node:fs";
226062
+ import { tmpdir as tmpdir6 } from "node:os";
226063
+ import { extname, join as join38, relative as relative7, resolve as resolve19 } from "node:path";
226064
+ init_util();
226065
+ var DEFAULT_SIZE = 256;
226066
+ var IMAGE_EXT = /\.(png|jpe?g|webp|bmp|gif|tiff?|ktx2)$/i;
226067
+ var RENDERED_EXT = /\.(glb|gltf|material\.ts|scene\.ts)$/i;
226068
+ var findKtx = () => {
226069
+ const exe = process.platform === "win32" ? "ktx.exe" : "ktx";
226070
+ const candidates = [
226071
+ process.env.LECODES_KTX,
226072
+ process.env.KTX_CLI,
226073
+ process.env.KTX_TOKTX?.replace(/toktx(\.exe)?$/i, exe),
226074
+ process.platform === "win32" ? join38(process.env.LOCALAPPDATA ?? "", "Programs", "KTX-Software", "bin", exe) : null,
226075
+ process.platform === "win32" ? join38(process.env.ProgramFiles ?? "", "KTX-Software", "bin", exe) : null,
226076
+ "/usr/local/bin/ktx",
226077
+ "/opt/homebrew/bin/ktx"
226078
+ ].filter((p2) => !!p2);
226079
+ for (const path2 of candidates) {
226080
+ if (existsSync36(path2))
226081
+ return path2;
226082
+ }
226083
+ try {
226084
+ execFileSync7(exe, ["--version"], { stdio: "ignore" });
226085
+ return exe;
226086
+ } catch {
226087
+ return null;
226088
+ }
226089
+ };
226090
+ var sharpResizer = () => {
226091
+ try {
226092
+ const req = createRequire6(import.meta.url);
226093
+ const sharp = createRequire6(req.resolve("lecodes-assets/cli"))("sharp");
226094
+ return async (file, size) => sharp(file).resize({ width: size, height: size, fit: "inside", withoutEnlargement: true }).png().toBuffer();
226095
+ } catch {
226096
+ return null;
226097
+ }
226098
+ };
226099
+ var jimpResizer = () => async (file, size) => {
226100
+ const { Jimp: Jimp2 } = await Promise.resolve().then(() => (init_esm30(), exports_esm2));
226101
+ const img = await Jimp2.read(file);
226102
+ if (Math.max(img.width, img.height) > size)
226103
+ img.scaleToFit({ w: size, h: size });
226104
+ return await img.getBuffer("image/png");
226105
+ };
226106
+ var ktxToPng = (ktx, file, out) => {
226107
+ const run2 = (args) => execFileSync7(ktx, args, { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
226108
+ try {
226109
+ run2(["extract", "--transcode", "rgba8", "--level", "0", file, out]);
226110
+ } catch (e2) {
226111
+ run2(["extract", "--level", "0", file, out]);
226112
+ if (!existsSync36(out))
226113
+ throw e2;
226114
+ }
226115
+ };
226116
+ var collect = (dir) => {
226117
+ const out = [];
226118
+ for (const name of readdirSync17(dir)) {
226119
+ if (name === "node_modules" || name.startsWith("."))
226120
+ continue;
226121
+ const full = join38(dir, name);
226122
+ if (statSync15(full).isDirectory())
226123
+ out.push(...collect(full));
226124
+ else if (IMAGE_EXT.test(name))
226125
+ out.push(full);
226126
+ }
226127
+ return out;
226128
+ };
226129
+ var thumbs = async (args) => {
226130
+ const root = projectRootOf(process.cwd());
226131
+ let sceneServer;
226132
+ try {
226133
+ sceneServer = await import("lecodes-3d-editor/server");
226134
+ } catch {
226135
+ throw new CliError("The scene editor isn't installed (it owns the preview cache). Run: npm install -g lecodes-3d-editor");
226136
+ }
226137
+ const size = Number(flagStr(args, "size")) || DEFAULT_SIZE;
226138
+ const force = flagBool(args, "force");
226139
+ let files = [];
226140
+ const rendered = [];
226141
+ if (args._.length > 0) {
226142
+ for (const arg of args._) {
226143
+ const abs = resolve19(process.cwd(), arg);
226144
+ if (!existsSync36(abs))
226145
+ throw new CliError(`No such file or folder: ${arg}`);
226146
+ if (statSync15(abs).isDirectory())
226147
+ files.push(...collect(abs));
226148
+ else if (IMAGE_EXT.test(abs))
226149
+ files.push(abs);
226150
+ else if (RENDERED_EXT.test(abs))
226151
+ rendered.push(arg);
226152
+ else
226153
+ throw new CliError(`Not a previewable asset: ${arg}`);
226154
+ }
226155
+ } else {
226156
+ files = collect(root);
226157
+ }
226158
+ files = [...new Set(files)];
226159
+ if (files.length === 0 && rendered.length === 0) {
226160
+ log("No images to thumbnail.");
226161
+ return;
226162
+ }
226163
+ const ktx = files.some((f3) => f3.toLowerCase().endsWith(".ktx2")) ? findKtx() : null;
226164
+ const sharp = sharpResizer();
226165
+ const resize = sharp ?? jimpResizer();
226166
+ const tmp = mkdtempSync6(join38(tmpdir6(), "lecodes-thumbs-"));
226167
+ let made = 0, fresh = 0;
226168
+ const skipped = [];
226169
+ try {
226170
+ for (const [i2, file] of files.entries()) {
226171
+ const asset = "./" + toKey(relative7(root, file));
226172
+ progress(i2, files.length, asset);
226173
+ const hash = sceneServer.hashAsset(root, asset);
226174
+ if (!hash)
226175
+ continue;
226176
+ if (!force && sceneServer.previewIsCurrent(root, asset, hash)) {
226177
+ fresh++;
226178
+ continue;
226179
+ }
226180
+ const ext2 = extname(file).toLowerCase();
226181
+ if (ext2 === ".webp" && !sharp) {
226182
+ skipped.push(`${asset} (webp needs sharp — npm install -g lecodes-assets)`);
226183
+ continue;
226184
+ }
226185
+ try {
226186
+ let source = file;
226187
+ if (ext2 === ".ktx2") {
226188
+ if (!ktx) {
226189
+ skipped.push(`${asset} (KTX2 needs KTX-Software's \`ktx\` — set LECODES_KTX or put it on PATH)`);
226190
+ continue;
226191
+ }
226192
+ source = join38(tmp, `${i2}.png`);
226193
+ ktxToPng(ktx, file, source);
226194
+ }
226195
+ sceneServer.writePreviewFile(root, asset, await resize(source, size));
226196
+ made++;
226197
+ } catch (e2) {
226198
+ skipped.push(`${asset} (${(e2?.message ?? String(e2)).split(`
226199
+ `)[0]})`);
226200
+ }
226201
+ }
226202
+ } finally {
226203
+ endProgress();
226204
+ rmSync12(tmp, { recursive: true, force: true });
226205
+ }
226206
+ log(`${c.bold(String(made))} generated · ${fresh} already current${skipped.length > 0 ? ` · ${skipped.length} skipped` : ""}`);
226207
+ for (const s2 of skipped)
226208
+ warnErr(s2);
226209
+ if (rendered.length > 0) {
226210
+ warnErr(`Models, materials and prefabs are RENDERED, not decoded — the scene editor does those: ${rendered.join(", ")}`);
226211
+ }
226212
+ note(`Cache: ${join38(root, ".lecodes", "preview")} (re-runs skip everything unchanged)`);
226213
+ };
226214
+
225718
226215
  // src/commands/assets.ts
225719
226216
  init_util();
225720
226217
 
225721
226218
  // src/cmgenTool.ts
225722
- import { existsSync as existsSync35, readdirSync as readdirSync16 } from "node:fs";
226219
+ import { existsSync as existsSync37, readdirSync as readdirSync18 } from "node:fs";
225723
226220
  import { homedir as homedir8 } from "node:os";
225724
- import { join as join37, resolve as resolve19 } from "node:path";
226221
+ import { join as join39, resolve as resolve20 } from "node:path";
225725
226222
  init_util();
225726
226223
  var exeName2 = () => process.platform === "win32" ? "cmgen.exe" : "cmgen";
225727
226224
  var platformKey3 = () => {
@@ -225731,10 +226228,10 @@ var platformKey3 = () => {
225731
226228
  return "linux-x64";
225732
226229
  throw new CliError(process.platform === "darwin" ? "cmgen isn't published for macOS yet — build filament's host tools locally and set LECODES_CMGEN." : `cmgen isn't published for ${process.platform}.`);
225733
226230
  };
225734
- var cacheRoot3 = () => join37(homedir8(), ".lecodes", "cmgen");
226231
+ var cacheRoot3 = () => join39(homedir8(), ".lecodes", "cmgen");
225735
226232
  var findCmgenIn = (dir) => {
225736
- for (const entry of readdirSync16(dir, { withFileTypes: true })) {
225737
- const p2 = join37(dir, entry.name);
226233
+ for (const entry of readdirSync18(dir, { withFileTypes: true })) {
226234
+ const p2 = join39(dir, entry.name);
225738
226235
  if (entry.isFile() && entry.name === exeName2())
225739
226236
  return p2;
225740
226237
  if (entry.isDirectory()) {
@@ -225747,7 +226244,7 @@ var findCmgenIn = (dir) => {
225747
226244
  };
225748
226245
  var installedCmgenVersions = () => {
225749
226246
  try {
225750
- return readdirSync16(cacheRoot3(), { withFileTypes: true }).filter((e2) => e2.isDirectory() && e2.name.startsWith("cmgen-")).map((e2) => e2.name.slice("cmgen-".length)).filter((v) => findCmgenIn(join37(cacheRoot3(), `cmgen-${v}`)) !== null).sort(semverNewer);
226247
+ return readdirSync18(cacheRoot3(), { withFileTypes: true }).filter((e2) => e2.isDirectory() && e2.name.startsWith("cmgen-")).map((e2) => e2.name.slice("cmgen-".length)).filter((v) => findCmgenIn(join39(cacheRoot3(), `cmgen-${v}`)) !== null).sort(semverNewer);
225751
226248
  } catch {
225752
226249
  return [];
225753
226250
  }
@@ -225769,19 +226266,19 @@ var downloadCmgen = async (version, versions, dest) => {
225769
226266
  var resolveCmgenExeCached = () => {
225770
226267
  const override = process.env.LECODES_CMGEN;
225771
226268
  if (override) {
225772
- if (!existsSync35(override))
226269
+ if (!existsSync37(override))
225773
226270
  throw new CliError(`LECODES_CMGEN points at "${override}" which doesn't exist.`);
225774
- return resolve19(override);
226271
+ return resolve20(override);
225775
226272
  }
225776
226273
  const cached = installedCmgenVersions()[0];
225777
- return cached ? findCmgenIn(join37(cacheRoot3(), `cmgen-${cached}`)) : null;
226274
+ return cached ? findCmgenIn(join39(cacheRoot3(), `cmgen-${cached}`)) : null;
225778
226275
  };
225779
226276
  var resolveCmgenExe = async () => {
225780
226277
  const cached = resolveCmgenExeCached();
225781
226278
  if (cached)
225782
226279
  return cached;
225783
226280
  const { latest, versions } = await fetchCmgenManifest();
225784
- const dir = join37(cacheRoot3(), `cmgen-${latest}`);
226281
+ const dir = join39(cacheRoot3(), `cmgen-${latest}`);
225785
226282
  await downloadCmgen(latest, versions, dir);
225786
226283
  const exe = findCmgenIn(dir);
225787
226284
  if (!exe)
@@ -225839,8 +226336,8 @@ var status3 = async (_args) => {
225839
226336
  };
225840
226337
 
225841
226338
  // src/commands/test.ts
225842
- import { existsSync as existsSync36, mkdirSync as mkdirSync26, readFileSync as readFileSync32, readdirSync as readdirSync17 } from "node:fs";
225843
- import { dirname as dirname18, resolve as resolve20 } from "node:path";
226339
+ import { existsSync as existsSync38, mkdirSync as mkdirSync26, readFileSync as readFileSync33, readdirSync as readdirSync19 } from "node:fs";
226340
+ import { dirname as dirname19, resolve as resolve21 } from "node:path";
225844
226341
  init_manifest();
225845
226342
  init_util();
225846
226343
  var deriveDesignFlows = (meta, entryScreen) => {
@@ -225903,7 +226400,7 @@ var slug3 = (s2) => s2.replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "").
225903
226400
  var readFlowFile = (abs, displayName) => {
225904
226401
  let raw;
225905
226402
  try {
225906
- raw = JSON.parse(readFileSync32(abs, "utf8"));
226403
+ raw = JSON.parse(readFileSync33(abs, "utf8"));
225907
226404
  } catch (e2) {
225908
226405
  throw new CliError(`Can't read "${displayName}": ${e2 instanceof Error ? e2.message : String(e2)}`);
225909
226406
  }
@@ -225917,7 +226414,7 @@ var readFlowFile = (abs, displayName) => {
225917
226414
  name: file.name ?? displayName,
225918
226415
  kind: "file",
225919
226416
  steps: file.steps,
225920
- fixtures: resolveFixtures(file.fixtures, dirname18(abs)),
226417
+ fixtures: resolveFixtures(file.fixtures, dirname19(abs)),
225921
226418
  width: file.width,
225922
226419
  height: file.height,
225923
226420
  settle: file.settle,
@@ -225958,40 +226455,40 @@ var test = async (args) => {
225958
226455
  if (!Number.isFinite(nativeFixedDt) || nativeFixedDt < 0 || nativeFixedDt > 1000)
225959
226456
  throw new CliError(`--fixed-dt wants a number of ms (0 = wall clock, max 1000), got "${fixedDtFlag}".`);
225960
226457
  const projectViewport = desktopDefaultViewport(root);
225961
- const testsDir = resolve20(root, flagStr(args, "dir") ?? "tests");
225962
- const designMetaPath = resolve20(root, flagStr(args, "design-dir") ?? "design", "meta.json");
226458
+ const testsDir = resolve21(root, flagStr(args, "dir") ?? "tests");
226459
+ const designMetaPath = resolve21(root, flagStr(args, "design-dir") ?? "design", "meta.json");
225963
226460
  const asJson = flagBool(args, "json");
225964
226461
  const out = asJson ? logErr : log;
225965
226462
  const cases = [];
225966
226463
  if (args._.length > 0) {
225967
226464
  for (const given of args._) {
225968
- const candidates = [resolve20(given), resolve20(testsDir, given), resolve20(testsDir, `${given}.flow.json`)];
225969
- const abs = candidates.find((p2) => existsSync36(p2));
226465
+ const candidates = [resolve21(given), resolve21(testsDir, given), resolve21(testsDir, `${given}.flow.json`)];
226466
+ const abs = candidates.find((p2) => existsSync38(p2));
225970
226467
  if (!abs)
225971
226468
  throw new CliError(`No such test: "${given}" (looked in ${testsDir}).`);
225972
226469
  cases.push(readFlowFile(abs, given));
225973
226470
  }
225974
- } else if (existsSync36(testsDir)) {
225975
- for (const f3 of readdirSync17(testsDir).filter((f4) => f4.endsWith(".flow.json")).sort()) {
225976
- cases.push(readFlowFile(resolve20(testsDir, f3), f3));
226471
+ } else if (existsSync38(testsDir)) {
226472
+ for (const f3 of readdirSync19(testsDir).filter((f4) => f4.endsWith(".flow.json")).sort()) {
226473
+ cases.push(readFlowFile(resolve21(testsDir, f3), f3));
225977
226474
  }
225978
226475
  }
225979
226476
  const fixturesFlag = flagStr(args, "fixtures");
225980
- const defaultPack = resolve20(testsDir, "fixtures.json");
225981
- const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") : existsSync36(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
226477
+ const defaultPack = resolve21(testsDir, "fixtures.json");
226478
+ const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") : existsSync38(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
225982
226479
  const flowsOnly = flagBool(args, "flows");
225983
226480
  const wantFlows = !flagBool(args, "no-flows") && args._.length === 0;
225984
226481
  if (flowsOnly)
225985
226482
  cases.length = 0;
225986
226483
  let skipped = [];
225987
226484
  let entryScreen = null;
225988
- if (desktop2 && wantFlows && existsSync36(designMetaPath)) {
226485
+ if (desktop2 && wantFlows && existsSync38(designMetaPath)) {
225989
226486
  note("Design-derived flows skipped with --desktop (they tap by name; run `lecodes test` headless for them).");
225990
226487
  }
225991
- if (!desktop2 && (wantFlows || flowsOnly) && existsSync36(designMetaPath)) {
226488
+ if (!desktop2 && (wantFlows || flowsOnly) && existsSync38(designMetaPath)) {
225992
226489
  let meta;
225993
226490
  try {
225994
- meta = JSON.parse(readFileSync32(designMetaPath, "utf8"));
226491
+ meta = JSON.parse(readFileSync33(designMetaPath, "utf8"));
225995
226492
  } catch (e2) {
225996
226493
  throw new CliError(`Can't read ${designMetaPath}: ${e2 instanceof Error ? e2.message : String(e2)}`);
225997
226494
  }
@@ -226023,18 +226520,19 @@ var test = async (args) => {
226023
226520
  }, timeoutMs);
226024
226521
  killer.unref?.();
226025
226522
  const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
226026
- const artifactsRoot = resolve20(root, flagStr(args, "artifacts") ?? resolve20(testsDir, ".artifacts"));
226523
+ const artifactsRoot = resolve21(root, flagStr(args, "artifacts") ?? resolve21(testsDir, ".artifacts"));
226027
226524
  const results = [];
226028
226525
  const runNativeCase = async (kase) => {
226029
- const caseDir = resolve20(artifactsRoot, slug3(kase.name));
226526
+ const caseDir = resolve21(artifactsRoot, slug3(kase.name));
226030
226527
  const plan = translateScenario(kase.steps, { outDir: caseDir, headroomFrames: nativeFrames });
226031
226528
  for (const st of plan.steps)
226032
226529
  if (st.screenshot)
226033
- mkdirSync26(dirname18(st.screenshot), { recursive: true });
226530
+ mkdirSync26(dirname19(st.screenshot), { recursive: true });
226034
226531
  mkdirSync26(caseDir, { recursive: true });
226035
- const finalPng = resolve20(caseDir, "final.png");
226532
+ const finalPng = resolve21(caseDir, "final.png");
226036
226533
  const { output } = await runDesktopRender(native.js, native.resources, {
226037
226534
  renderer: desktopRenderer,
226535
+ pluginDirs: desktopPluginLibraries(root).dirs,
226038
226536
  width: Number(flagStr(args, "width")) || kase.width || projectViewport?.width || 390,
226039
226537
  height: Number(flagStr(args, "height")) || kase.height || projectViewport?.height || 844,
226040
226538
  frames: plan.frameCap,
@@ -226084,14 +226582,14 @@ var test = async (args) => {
226084
226582
  onConsole
226085
226583
  });
226086
226584
  try {
226087
- const io = fileScenarioIO(resolve20(artifactsRoot, slug3(kase.name)));
226585
+ const io = fileScenarioIO(resolve21(artifactsRoot, slug3(kase.name)));
226088
226586
  const outcome = await renderer.runScenario(session, steps, io);
226089
226587
  result = {
226090
226588
  name: kase.name,
226091
226589
  kind: kase.kind,
226092
226590
  ok: outcome.ok,
226093
226591
  steps: outcome.steps.map((s2) => ({ desc: s2.desc, ok: s2.ok, error: s2.error })),
226094
- artifacts: outcome.failureArtifacts.map((p2) => resolve20(artifactsRoot, slug3(kase.name), p2))
226592
+ artifacts: outcome.failureArtifacts.map((p2) => resolve21(artifactsRoot, slug3(kase.name), p2))
226095
226593
  };
226096
226594
  } finally {
226097
226595
  session.dispose();
@@ -226153,17 +226651,54 @@ var test = async (args) => {
226153
226651
  process.exit(failed > 0 ? 1 : 0);
226154
226652
  };
226155
226653
 
226654
+ // src/commands/update.ts
226655
+ import { spawnSync as spawnSync7 } from "node:child_process";
226656
+ import { existsSync as existsSync40, readdirSync as readdirSync20 } from "node:fs";
226657
+ import { homedir as homedir9 } from "node:os";
226658
+ import { dirname as dirname21, join as join41, resolve as resolve23 } from "node:path";
226659
+ init_util();
226660
+
226661
+ // src/peers.ts
226662
+ import { existsSync as existsSync39, readFileSync as readFileSync34 } from "node:fs";
226663
+ import { createRequire as createRequire7 } from "node:module";
226664
+ import { dirname as dirname20, join as join40 } from "node:path";
226665
+ var OPTIONAL_PEERS = [
226666
+ ["lecodes-renderer", "lecodes-renderer/headless"],
226667
+ ["lecodes-design", "lecodes-design/server"],
226668
+ ["lecodes-3d-editor", "lecodes-3d-editor/server"],
226669
+ ["lecodes-assets", "lecodes-assets/cli"]
226670
+ ];
226671
+ var peerVersion = (name, probe) => {
226672
+ try {
226673
+ const require2 = createRequire7(import.meta.url);
226674
+ let dir = dirname20(require2.resolve(probe));
226675
+ for (let depth = 0;depth < 16; depth++) {
226676
+ const manifest = join40(dir, "package.json");
226677
+ if (existsSync39(manifest)) {
226678
+ const parsed = JSON.parse(readFileSync34(manifest, "utf8"));
226679
+ if (parsed.name === name)
226680
+ return parsed.version ?? "unknown";
226681
+ }
226682
+ const parent = dirname20(dir);
226683
+ if (parent === dir)
226684
+ break;
226685
+ dir = parent;
226686
+ }
226687
+ } catch {}
226688
+ return null;
226689
+ };
226690
+
226156
226691
  // src/commands/types.ts
226157
226692
  init_api();
226158
226693
  init_config();
226159
226694
  init_manifest();
226160
226695
  init_types4();
226161
226696
  init_util();
226162
- import { resolve as resolve21 } from "node:path";
226697
+ import { resolve as resolve22 } from "node:path";
226163
226698
  var label = (bundle) => `${Object.keys(bundle).length} file(s)`;
226164
226699
  var types4 = async (args) => {
226165
226700
  const dirArg = args._[0];
226166
- const root = dirArg ? resolve21(dirArg) : (() => {
226701
+ const root = dirArg ? resolve22(dirArg) : (() => {
226167
226702
  try {
226168
226703
  return findProjectRoot(process.cwd());
226169
226704
  } catch {
@@ -226198,9 +226733,9 @@ var types4 = async (args) => {
226198
226733
  const same = bundleHash(onDisk) === bundleHash(bundle);
226199
226734
  if (checkOnly) {
226200
226735
  if (Object.keys(onDisk).length === 0)
226201
- throw new CliError(`No types in ${LECODES_DIR}/types/ — run \`lecodes types\`.`);
226736
+ throw new CliError(`No types in ${LECODES_DIR}/types/ — run \`lecodes update\`.`);
226202
226737
  if (!same)
226203
- throw new CliError(`Types are out of date (${label(onDisk)} on disk) — run \`lecodes types\`.`);
226738
+ throw new CliError(`Types are out of date (${label(onDisk)} on disk) — run \`lecodes update\`.`);
226204
226739
  success(`Types are up to date (${label(onDisk)}).`);
226205
226740
  return;
226206
226741
  }
@@ -226221,29 +226756,166 @@ var types4 = async (args) => {
226221
226756
  note("Reload the TypeScript server in your editor to pick them up.");
226222
226757
  };
226223
226758
 
226759
+ // src/commands/update.ts
226760
+ var findProjectDir = (start) => {
226761
+ const home = resolve23(homedir9());
226762
+ let dir = resolve23(start);
226763
+ while (true) {
226764
+ if (dir !== home && (existsSync40(join41(dir, "app.json")) || existsSync40(join41(dir, ".lecodes", "manifest.json")) || existsSync40(join41(dir, ".lecodes", "types"))))
226765
+ return dir;
226766
+ const parent = dirname21(dir);
226767
+ if (parent === dir)
226768
+ return null;
226769
+ dir = parent;
226770
+ }
226771
+ };
226772
+ var NPM = "https://registry.npmjs.org";
226773
+ var npmLatest = async (name) => {
226774
+ try {
226775
+ const res = await fetch(`${NPM}/${name}/latest`, { signal: AbortSignal.timeout(4000) });
226776
+ if (!res.ok)
226777
+ return null;
226778
+ const json = await res.json();
226779
+ return json.version ?? null;
226780
+ } catch {
226781
+ return null;
226782
+ }
226783
+ };
226784
+ var semverNewer2 = (a2, b) => {
226785
+ const pa = a2.split(/[.-]/).map(Number), pb = b.split(/[.-]/).map(Number);
226786
+ for (let i2 = 0;i2 < 3; i2++) {
226787
+ const d = (pa[i2] ?? 0) - (pb[i2] ?? 0);
226788
+ if (d)
226789
+ return d;
226790
+ }
226791
+ return 0;
226792
+ };
226793
+ var FROM_SOURCE = /[\\/]src[\\/]commands[\\/]/.test(import.meta.url);
226794
+ var STANDALONE = existsSync40(join41(dirname21(process.execPath), "runtime")) && existsSync40(join41(dirname21(process.execPath), process.platform === "win32" ? "chisel.exe" : "chisel"));
226795
+ var update = async (args) => {
226796
+ const steps = [];
226797
+ const heading = (name, what) => log(`
226798
+ ${c.bold(name)} ${c.dim(what)}`);
226799
+ const run2 = async (name, what, fn) => {
226800
+ heading(name, what);
226801
+ try {
226802
+ const r2 = await fn();
226803
+ const [outcome, detail] = Array.isArray(r2) ? r2 : [r2, undefined];
226804
+ steps.push({ name, outcome, detail });
226805
+ if (outcome === "skipped" && detail)
226806
+ note(`skipped — ${detail}`);
226807
+ } catch (e2) {
226808
+ const msg = e2 instanceof CliError ? e2.message : e2 instanceof Error ? e2.message : String(e2);
226809
+ log(c.red("✗ ") + msg);
226810
+ steps.push({ name, outcome: "failed", detail: msg });
226811
+ }
226812
+ };
226813
+ const root = findProjectDir(process.cwd());
226814
+ await run2("desktop", "LeCodes Desktop host (desktop run / render --desktop)", async () => {
226815
+ if (process.env.LECODES_DESKTOP_EXE)
226816
+ return ["skipped", `LECODES_DESKTOP_EXE=${process.env.LECODES_DESKTOP_EXE} (a local build; \`lecodes desktop update\` refreshes the cache anyway)`];
226817
+ await updateDesktop();
226818
+ return "ok";
226819
+ });
226820
+ await run2("matc", "Filament material compiler for local shader compiles", async () => {
226821
+ if (process.env.LECODES_MATC)
226822
+ return ["skipped", `LECODES_MATC=${process.env.LECODES_MATC} (a local matc)`];
226823
+ await updateMatc();
226824
+ return "ok";
226825
+ });
226826
+ await run2("shaders", "recompile the project's .mat shaders with the current matc", async () => {
226827
+ if (!root)
226828
+ return ["skipped", "not inside a project"];
226829
+ await shaders({ _: ["compile"], flags: {} });
226830
+ return "ok";
226831
+ });
226832
+ await run2("types", flagBool(args, "local") ? "IDE types from the SDK bundled in this CLI" : "IDE types from the server", async () => {
226833
+ if (!root)
226834
+ return ["skipped", "not inside a project"];
226835
+ await types4({ _: [], flags: flagBool(args, "local") ? { local: true } : {} });
226836
+ return "ok";
226837
+ });
226838
+ await run2("shells", "ios/ + android/ pins → newest SDK release + plugin tags, then re-sync", async () => {
226839
+ if (!root)
226840
+ return ["skipped", "not inside a project"];
226841
+ if (flagBool(args, "no-shells"))
226842
+ return ["skipped", "--no-shells"];
226843
+ const present = ["ios", "android"].filter((d) => existsSync40(join41(root, d)) && readdirSync20(join41(root, d)).length > 0);
226844
+ if (present.length === 0)
226845
+ return ["skipped", "no ios/ or android/ shell in this project (`lecodes app init ios|android`)"];
226846
+ await app({ _: ["update"], flags: {} });
226847
+ return "ok";
226848
+ });
226849
+ await run2("cli", "lecodes-cli + its optional peers (global npm installs)", async () => {
226850
+ if (flagBool(args, "no-cli"))
226851
+ return ["skipped", "--no-cli"];
226852
+ if (FROM_SOURCE)
226853
+ return ["skipped", "running from the repo checkout — the CLI and its peers are the repo's own"];
226854
+ const rows = [];
226855
+ for (const [name, probe] of OPTIONAL_PEERS) {
226856
+ const installed = peerVersion(name, probe);
226857
+ rows.push({ name, installed, latest: installed ? await npmLatest(name) : null });
226858
+ }
226859
+ rows.push({ name: "lecodes-cli", installed: package_default.version, latest: await npmLatest("lecodes-cli") });
226860
+ if (rows.every((r3) => r3.latest === null))
226861
+ return ["skipped", "npm unreachable"];
226862
+ const behind = rows.filter((r3) => r3.installed && r3.latest && semverNewer2(r3.latest, r3.installed) > 0);
226863
+ for (const r3 of rows) {
226864
+ const state = !r3.installed ? c.dim("not installed (optional — npm install -g " + r3.name + ")") : !r3.latest ? c.dim("(npm: unknown)") : behind.includes(r3) ? c.yellow("→ " + r3.latest) : c.dim("(up to date)");
226865
+ log(" " + r3.name.padEnd(18) + " " + (r3.installed ?? "").padEnd(8) + " " + state);
226866
+ }
226867
+ if (behind.length === 0)
226868
+ return "ok";
226869
+ const specs = behind.map((r3) => r3.name + "@latest");
226870
+ if (flagBool(args, "check"))
226871
+ return ["updated", behind.length + " behind — run: npm install -g " + specs.join(" ")];
226872
+ if (STANDALONE && behind.some((r3) => r3.name === "lecodes-cli")) {
226873
+ note("this is the standalone build — the CLI updates by re-installing the release archive, not through npm; peers still go through npm");
226874
+ }
226875
+ const toInstall = STANDALONE ? specs.filter((s2) => !s2.startsWith("lecodes-cli@")) : specs;
226876
+ if (toInstall.length === 0)
226877
+ return ["skipped", "only the standalone CLI itself is behind — download the release"];
226878
+ log(" " + c.dim("$") + " npm install -g " + toInstall.join(" "));
226879
+ const r2 = spawnSync7("npm", ["install", "-g", ...toInstall], { stdio: "inherit", shell: true });
226880
+ if (r2.status !== 0)
226881
+ throw new CliError("npm install -g exited with " + (r2.status ?? "a signal"));
226882
+ const names3 = toInstall.map((s2) => s2.replace("@latest", "")).join(", ");
226883
+ const cliToo = toInstall.some((s2) => s2.startsWith("lecodes-cli@"));
226884
+ return ["updated", names3 + " → latest" + (cliToo ? " (the new CLI runs from your next command)" : "")];
226885
+ });
226886
+ log(`
226887
+ ${c.bold("Summary")}`);
226888
+ const width = Math.max(...steps.map((s2) => s2.name.length));
226889
+ for (const s2 of steps) {
226890
+ const mark = s2.outcome === "failed" ? c.red("✗") : s2.outcome === "skipped" ? c.dim("–") : c.green("✓");
226891
+ log(` ${mark} ${s2.name.padEnd(width)} ${s2.outcome === "failed" ? c.red(s2.detail ?? "failed") : s2.outcome === "skipped" ? c.dim(s2.detail ?? "skipped") : s2.detail ?? s2.outcome}`);
226892
+ }
226893
+ if (steps.some((s2) => s2.outcome === "failed"))
226894
+ process.exitCode = 1;
226895
+ };
226896
+
226224
226897
  // src/index.ts
226225
- import { existsSync as existsSync39, readFileSync as readFileSync35, realpathSync as realpathSync2 } from "node:fs";
226226
- import { createRequire as createRequire4 } from "node:module";
226227
- import { delimiter as delimiter2, dirname as dirname21, join as join40 } from "node:path";
226898
+ import { existsSync as existsSync43, realpathSync as realpathSync2 } from "node:fs";
226899
+ import { delimiter as delimiter3, dirname as dirname24, join as join44 } from "node:path";
226228
226900
 
226229
226901
  // src/commands/install.ts
226230
226902
  init_util();
226231
- import { execFileSync as execFileSync7 } from "node:child_process";
226232
- import { appendFileSync as appendFileSync3, chmodSync as chmodSync2, cpSync as cpSync5, existsSync as existsSync37, mkdirSync as mkdirSync27, readFileSync as readFileSync33, realpathSync, rmSync as rmSync12, symlinkSync } from "node:fs";
226233
- import { homedir as homedir9 } from "node:os";
226234
- import { basename as basename18, delimiter, dirname as dirname19, join as join38, resolve as resolve22, sep as sep2 } from "node:path";
226903
+ import { execFileSync as execFileSync8 } from "node:child_process";
226904
+ import { appendFileSync as appendFileSync3, chmodSync as chmodSync2, cpSync as cpSync5, existsSync as existsSync41, mkdirSync as mkdirSync27, readFileSync as readFileSync35, realpathSync, rmSync as rmSync13, symlinkSync as symlinkSync2 } from "node:fs";
226905
+ import { homedir as homedir10 } from "node:os";
226906
+ import { basename as basename19, delimiter as delimiter2, dirname as dirname22, join as join42, resolve as resolve24, sep as sep2 } from "node:path";
226235
226907
  var isWin = process.platform === "win32";
226236
- var HOME = homedir9();
226908
+ var HOME = homedir10();
226237
226909
  var safeRealpath = (p2) => {
226238
226910
  try {
226239
226911
  return realpathSync(p2);
226240
226912
  } catch {
226241
- return resolve22(p2);
226913
+ return resolve24(p2);
226242
226914
  }
226243
226915
  };
226244
226916
  var onPath = (dir) => {
226245
226917
  const want = safeRealpath(dir);
226246
- return (process.env.PATH ?? "").split(delimiter).some((p2) => {
226918
+ return (process.env.PATH ?? "").split(delimiter2).some((p2) => {
226247
226919
  if (!p2)
226248
226920
  return false;
226249
226921
  try {
@@ -226254,13 +226926,13 @@ var onPath = (dir) => {
226254
226926
  });
226255
226927
  };
226256
226928
  var install = async (args) => {
226257
- const bundle = dirname19(process.execPath);
226258
- const exeName3 = basename18(process.execPath);
226929
+ const bundle = dirname22(process.execPath);
226930
+ const exeName3 = basename19(process.execPath);
226259
226931
  const chiselName = isWin ? "chisel.exe" : "chisel";
226260
- if (!existsSync37(join38(bundle, "runtime")) || !existsSync37(join38(bundle, chiselName))) {
226932
+ if (!existsSync41(join42(bundle, "runtime")) || !existsSync41(join42(bundle, chiselName))) {
226261
226933
  throw new CliError("`lecodes install` only works from the packaged release build (the archive containing lecodes + " + "chisel + runtime/). You appear to be running from source or an npm install — install globally " + "with your package manager there instead.");
226262
226934
  }
226263
- const companions = [exeName3, chiselName, "runtime", "README.txt"].filter((f3) => existsSync37(join38(bundle, f3)));
226935
+ const companions = [exeName3, chiselName, "runtime", "README.txt"].filter((f3) => existsSync41(join42(bundle, f3)));
226264
226936
  if (isWin)
226265
226937
  return installWindows(args, bundle, companions);
226266
226938
  return installUnix(args, bundle, companions);
@@ -226270,9 +226942,9 @@ var copyBundle = (bundle, target, companions) => {
226270
226942
  if (safeRealpath(target) === safeRealpath(bundle))
226271
226943
  return;
226272
226944
  for (const f3 of companions) {
226273
- const dest = join38(target, f3);
226274
- rmSync12(dest, { recursive: true, force: true });
226275
- cpSync5(join38(bundle, f3), dest, { recursive: true });
226945
+ const dest = join42(target, f3);
226946
+ rmSync13(dest, { recursive: true, force: true });
226947
+ cpSync5(join42(bundle, f3), dest, { recursive: true });
226276
226948
  }
226277
226949
  };
226278
226950
  var permError = (e2, dir, system) => {
@@ -226284,41 +226956,41 @@ var permError = (e2, dir, system) => {
226284
226956
  };
226285
226957
  var installUnix = (args, bundle, companions) => {
226286
226958
  const system = flagBool(args, "system");
226287
- const libDir = resolve22(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join38(HOME, ".local", "lib", "lecodes-cli")));
226288
- const binDir = resolve22(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join38(HOME, ".local", "bin")));
226289
- const link2 = join38(binDir, "lecodes");
226959
+ const libDir = resolve24(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join42(HOME, ".local", "lib", "lecodes-cli")));
226960
+ const binDir = resolve24(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join42(HOME, ".local", "bin")));
226961
+ const link2 = join42(binDir, "lecodes");
226290
226962
  try {
226291
226963
  copyBundle(bundle, libDir, companions);
226292
- chmodSync2(join38(libDir, "lecodes"), 493);
226293
- chmodSync2(join38(libDir, "chisel"), 493);
226964
+ chmodSync2(join42(libDir, "lecodes"), 493);
226965
+ chmodSync2(join42(libDir, "chisel"), 493);
226294
226966
  } catch (e2) {
226295
226967
  throw permError(e2, libDir, system);
226296
226968
  }
226297
226969
  try {
226298
226970
  mkdirSync27(binDir, { recursive: true });
226299
- rmSync12(link2, { force: true });
226300
- symlinkSync(join38(libDir, "lecodes"), link2);
226971
+ rmSync13(link2, { force: true });
226972
+ symlinkSync2(join42(libDir, "lecodes"), link2);
226301
226973
  } catch (e2) {
226302
226974
  throw permError(e2, binDir, system);
226303
226975
  }
226304
226976
  success(`Installed to ${c.bold(libDir)}`);
226305
- log(` ${c.dim(link2 + " → " + join38(libDir, "lecodes"))}`);
226977
+ log(` ${c.dim(link2 + " → " + join42(libDir, "lecodes"))}`);
226306
226978
  finishPathUnix(args, binDir);
226307
226979
  };
226308
226980
  var shellRc = () => {
226309
- const shell = basename18(process.env.SHELL ?? "");
226981
+ const shell = basename19(process.env.SHELL ?? "");
226310
226982
  if (shell === "zsh")
226311
- return { file: join38(HOME, ".zshrc"), name: "~/.zshrc" };
226983
+ return { file: join42(HOME, ".zshrc"), name: "~/.zshrc" };
226312
226984
  if (shell === "bash")
226313
- return { file: join38(HOME, ".bashrc"), name: "~/.bashrc" };
226314
- return { file: join38(HOME, ".profile"), name: "~/.profile" };
226985
+ return { file: join42(HOME, ".bashrc"), name: "~/.bashrc" };
226986
+ return { file: join42(HOME, ".profile"), name: "~/.profile" };
226315
226987
  };
226316
226988
  var PATH_MARKER = "# Added by `lecodes install`";
226317
226989
  var appendPathToShellRc = (binDir) => {
226318
226990
  const { file, name } = shellRc();
226319
226991
  const line = `export PATH="${binDir}:$PATH"`;
226320
226992
  try {
226321
- const existing = existsSync37(file) ? readFileSync33(file, "utf8") : "";
226993
+ const existing = existsSync41(file) ? readFileSync35(file, "utf8") : "";
226322
226994
  if (existing.includes(line))
226323
226995
  return name;
226324
226996
  appendFileSync3(file, `${existing && !existing.endsWith(`
@@ -226356,7 +227028,7 @@ var finishPathUnix = (args, binDir) => {
226356
227028
  log(` • or install system-wide (no PATH edit): ${c.bold("sudo lecodes install --system")}`);
226357
227029
  };
226358
227030
  var installWindows = (args, bundle, companions) => {
226359
- const target = resolve22(flagStr(args, "dir") ?? join38(process.env.LOCALAPPDATA ?? join38(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
227031
+ const target = resolve24(flagStr(args, "dir") ?? join42(process.env.LOCALAPPDATA ?? join42(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
226360
227032
  try {
226361
227033
  copyBundle(bundle, target, companions);
226362
227034
  } catch (e2) {
@@ -226384,7 +227056,7 @@ var installWindows = (args, bundle, companions) => {
226384
227056
  var addToUserPathWindows = (dir) => {
226385
227057
  const script = "$d = $env:LECODES_INSTALL_DIR;" + "$p = [Environment]::GetEnvironmentVariable('Path','User'); if (-not $p) { $p = '' };" + "$parts = @($p.Split(';') | Where-Object { $_ -ne '' });" + "if ($parts -notcontains $d) { [Environment]::SetEnvironmentVariable('Path', (@($parts + $d) -join ';'), 'User') }";
226386
227058
  try {
226387
- execFileSync7("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], { stdio: "ignore", env: { ...process.env, LECODES_INSTALL_DIR: dir } });
227059
+ execFileSync8("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], { stdio: "ignore", env: { ...process.env, LECODES_INSTALL_DIR: dir } });
226388
227060
  return true;
226389
227061
  } catch {
226390
227062
  return false;
@@ -226395,9 +227067,9 @@ var addToUserPathWindows = (dir) => {
226395
227067
  init_util();
226396
227068
 
226397
227069
  // src/projectEnv.ts
226398
- import { existsSync as existsSync38, readFileSync as readFileSync34 } from "node:fs";
226399
- import { homedir as homedir10 } from "node:os";
226400
- import { dirname as dirname20, join as join39, resolve as resolve23 } from "node:path";
227070
+ import { existsSync as existsSync42, readFileSync as readFileSync36 } from "node:fs";
227071
+ import { homedir as homedir11 } from "node:os";
227072
+ import { dirname as dirname23, join as join43, resolve as resolve25 } from "node:path";
226401
227073
  var KEY_RE = /^(LECODES|CREATOR)_[A-Z0-9_]+$/;
226402
227074
  var parseDotEnv = (text) => {
226403
227075
  const out = {};
@@ -226432,19 +227104,19 @@ var applyDotEnv = (parsed, dir, env) => {
226432
227104
  continue;
226433
227105
  let value = raw;
226434
227106
  if (value.startsWith("./") || value.startsWith("../"))
226435
- value = resolve23(dir, value);
227107
+ value = resolve25(dir, value);
226436
227108
  else if (value === "~" || value.startsWith("~/"))
226437
- value = join39(homedir10(), value.slice(1));
227109
+ value = join43(homedir11(), value.slice(1));
226438
227110
  out[key] = value;
226439
227111
  }
226440
227112
  return out;
226441
227113
  };
226442
227114
  var findEnvRoot = (start) => {
226443
- let dir = resolve23(start);
227115
+ let dir = resolve25(start);
226444
227116
  while (true) {
226445
- if (existsSync38(join39(dir, "app.json")) || existsSync38(join39(dir, ".lecodes")))
227117
+ if (existsSync42(join43(dir, "app.json")) || existsSync42(join43(dir, ".lecodes")))
226446
227118
  return dir;
226447
- const parent = dirname20(dir);
227119
+ const parent = dirname23(dir);
226448
227120
  if (parent === dir)
226449
227121
  return null;
226450
227122
  dir = parent;
@@ -226455,10 +227127,10 @@ var loadProjectEnv = (cwd = process.cwd()) => {
226455
227127
  const root = findEnvRoot(cwd);
226456
227128
  if (!root)
226457
227129
  return [];
226458
- const file = join39(root, ".env");
226459
- if (!existsSync38(file))
227130
+ const file = join43(root, ".env");
227131
+ if (!existsSync42(file))
226460
227132
  return [];
226461
- const added = applyDotEnv(parseDotEnv(readFileSync34(file, "utf8")), root, process.env);
227133
+ const added = applyDotEnv(parseDotEnv(readFileSync36(file, "utf8")), root, process.env);
226462
227134
  for (const [k, v] of Object.entries(added))
226463
227135
  process.env[k] = v;
226464
227136
  return Object.keys(added);
@@ -226468,12 +227140,12 @@ var loadProjectEnv = (cwd = process.cwd()) => {
226468
227140
  };
226469
227141
 
226470
227142
  // src/index.ts
226471
- var STANDALONE = (() => {
226472
- const dir = dirname21(process.execPath);
227143
+ var STANDALONE2 = (() => {
227144
+ const dir = dirname24(process.execPath);
226473
227145
  const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
226474
- return existsSync39(join40(dir, "runtime")) && existsSync39(join40(dir, chisel));
227146
+ return existsSync43(join44(dir, "runtime")) && existsSync43(join44(dir, chisel));
226475
227147
  })();
226476
- var INSTALLED = STANDALONE && (() => {
227148
+ var INSTALLED = STANDALONE2 && (() => {
226477
227149
  const exe = process.platform === "win32" ? "lecodes.exe" : "lecodes";
226478
227150
  const self2 = (() => {
226479
227151
  try {
@@ -226482,18 +227154,18 @@ var INSTALLED = STANDALONE && (() => {
226482
227154
  return process.execPath;
226483
227155
  }
226484
227156
  })();
226485
- return (process.env.PATH ?? "").split(delimiter2).some((d) => {
227157
+ return (process.env.PATH ?? "").split(delimiter3).some((d) => {
226486
227158
  if (!d)
226487
227159
  return false;
226488
227160
  try {
226489
- const cand = join40(d, exe);
226490
- return existsSync39(cand) && realpathSync2(cand) === self2;
227161
+ const cand = join44(d, exe);
227162
+ return existsSync43(cand) && realpathSync2(cand) === self2;
226491
227163
  } catch {
226492
227164
  return false;
226493
227165
  }
226494
227166
  });
226495
227167
  })();
226496
- var INSTALL_HELP = STANDALONE && !INSTALLED ? `
227168
+ var INSTALL_HELP = STANDALONE2 && !INSTALLED ? `
226497
227169
  install Install this standalone build so 'lecodes' is on your PATH
226498
227170
  (copies the bundle to a library folder + a symlink/PATH entry).
226499
227171
  --modify-path also add it to PATH (edits your shell rc / user PATH)
@@ -226555,6 +227227,9 @@ ${c.bold("Commands:")}
226555
227227
  --host <ip> LAN address to advertise (default: auto-detect)
226556
227228
  --entry <path> override the entrypoint
226557
227229
  --no-qr don't render the QR code
227230
+ --web also open the app in the browser (Web Lite host:
227231
+ same bundle + hot reload, camera/map plugins
227232
+ have web twins; the /web/ URL is served always)
226558
227233
  --desktop also open the app in the native desktop host
226559
227234
  (same hot reload; window from app.json's
226560
227235
  desktop block; closing it keeps the server)
@@ -226580,6 +227255,11 @@ ${c.bold("Commands:")}
226580
227255
  -o, --out <file> write JSON to a file instead of stdout
226581
227256
  --png [file] rasterize a PNG (default screenshot.png;
226582
227257
  needs @napi-rs/canvas)
227258
+ --map [file] draw the 3D scene's annotated top-down map
227259
+ (grid + XZ footprints + named markers) as
227260
+ <file>.png + <file>.json world-space data
227261
+ (default map.png; --label-all, --max-px N;
227262
+ also a { "map": "f.png" } script step)
226583
227263
  --script <file> drive the app through a step list (tap/type/
226584
227264
  scroll/wait/expect/screenshot/json steps;
226585
227265
  games: frames/drag/hold/key/keyDown/keyUp,
@@ -226643,13 +227323,22 @@ ${c.bold("Commands:")}
226643
227323
  render --desktop
226644
227324
  (+ the render timing flags: --width/--height, --settle,
226645
227325
  --device, --safe-area, --wait-network, --logs, --entry)
226646
- types [dir] Refresh the IDE types (.lecodes/types/ + tsconfig.json) — by
226647
- default from the server, so a project scaffolded offline picks
226648
- up the SDK the platform actually runs
226649
- --local use the SDK bundled in this CLI instead
226650
- --check report drift only, don't write (non-zero exit)
226651
- --api <url> server to fetch from (default: the logged-in
226652
- one, else https://le.codes)
227326
+ update Move everything a project depends on to its newest published
227327
+ state, in dependency order: the desktop host ('desktop
227328
+ update'), matc ('shaders update'), the project's shader
227329
+ compiles, the IDE types (.lecodes/types/ + tsconfig.json,
227330
+ from the server — the former 'lecodes types'), the
227331
+ ios/android shell pins ('app update'), then the GLOBAL npm
227332
+ installs: every installed optional peer (lecodes-renderer,
227333
+ lecodes-design, lecodes-3d-editor, lecodes-assets) and this
227334
+ CLI that is behind npm's latest gets 'npm install -g'.
227335
+ Pinned tools are skipped, not overridden: LECODES_DESKTOP_EXE
227336
+ / LECODES_MATC mean a local build. Keeps going when a step
227337
+ fails; the summary says which.
227338
+ --local types from the SDK bundled in this CLI
227339
+ --no-shells leave ios/android pins alone (no re-sync)
227340
+ --check npm step: report what's behind, install nothing
227341
+ --no-cli skip the npm step (offline)
226653
227342
  design [serve] LeCodes Design — a live canvas of screens for AI-driven prototyping
226654
227343
  (needs the optional 'lecodes-design' package; works inside a
226655
227344
  cloned project or standalone in any folder)
@@ -226706,6 +227395,8 @@ ${c.bold("Commands:")}
226706
227395
  --probe headless-render the staged folder to PNG
226707
227396
  (macOS: a brief windowed run + snapshot)
226708
227397
  --zip also pack the result for sending
227398
+ --dmg macOS: also build a drag-to-Applications
227399
+ disk image (signed + notarized too)
226709
227400
  --sign <identity|adhoc> macOS codesign identity
226710
227401
  --notarize <profile> / --no-notarize macOS
226711
227402
  --zip <out>.zip, UTF-8/forward-slash entries
@@ -226743,6 +227434,13 @@ ${c.bold("Commands:")}
226743
227434
  --port <n> editor-server port (default 4488)
226744
227435
  --no-open don't open the browser
226745
227436
  --no-mcp don't register the MCP server in .mcp.json
227437
+ thumbs [path…] Asset thumbnails for the scene editor, WITHOUT a browser: decodes
227438
+ the project's images (KTX2 too, via KTX-Software's ktx tool) into the
227439
+ editor's preview cache. Incremental — the cached name carries the
227440
+ asset's content hash, so unchanged files are skipped. Models,
227441
+ materials and prefabs are rendered by the editor, not here
227442
+ --force redo even the up-to-date ones
227443
+ --size <px> longest edge (default 256)
226746
227444
  assets convert <model.fbx|.glb> [--clips a.fbx b.fbx …] [-o out.glb] [--drop-model-clips]
226747
227445
  [--fix] [--ktx2|--ktx2-hq]
226748
227446
  FBX → LeCodes-ready GLB: meters/Y-up, textures embedded, clips from
@@ -226841,35 +227539,10 @@ Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKE
226841
227539
  A .env next to app.json is read at startup for LECODES_* / CREATOR_* keys (the shell's own
226842
227540
  environment wins; ./ paths resolve against the project; the file is never pushed) — e.g.
226843
227541
  LECODES_DESKTOP_EXE, LECODES_MATC, LECODES_NOTARY_PROFILE, LECODES_MACOS_SIGN_ID, LECODES_TOKEN.`;
226844
- var commands = { init: init2, login, create, clone: clone3, link, status: status3, diff: diff2, pull, push, compile: compile2, preview: previewLegacy, render, test, types: types4, design, scene, dev, app, pn, desktop, shaders, assets, lightmap, navmesh };
226845
- if (STANDALONE)
227542
+ var commands = { init: init2, login, create, clone: clone3, link, status: status3, diff: diff2, pull, push, compile: compile2, preview: previewLegacy, render, test, design, scene, thumbs, dev, app, pn, desktop, shaders, assets, lightmap, navmesh, update };
227543
+ if (STANDALONE2)
226846
227544
  commands.install = install;
226847
227545
  var readVersion = () => package_default.version ?? "unknown";
226848
- var OPTIONAL_PEERS = [
226849
- ["lecodes-renderer", "lecodes-renderer/headless"],
226850
- ["lecodes-design", "lecodes-design/server"],
226851
- ["lecodes-3d-editor", "lecodes-3d-editor/server"],
226852
- ["lecodes-assets", "lecodes-assets/cli"]
226853
- ];
226854
- var peerVersion = (name, probe) => {
226855
- try {
226856
- const require2 = createRequire4(import.meta.url);
226857
- let dir = dirname21(require2.resolve(probe));
226858
- for (let depth = 0;depth < 16; depth++) {
226859
- const manifest = join40(dir, "package.json");
226860
- if (existsSync39(manifest)) {
226861
- const parsed = JSON.parse(readFileSync35(manifest, "utf8"));
226862
- if (parsed.name === name)
226863
- return parsed.version ?? "unknown";
226864
- }
226865
- const parent = dirname21(dir);
226866
- if (parent === dir)
226867
- break;
226868
- dir = parent;
226869
- }
226870
- } catch {}
226871
- return null;
226872
- };
226873
227546
  var printVersion = () => {
226874
227547
  const rows = [["lecodes-cli", readVersion()]];
226875
227548
  for (const [name, probe] of OPTIONAL_PEERS) {