lecodes-cli 0.19.0 → 0.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/dist/index.js +730 -235
  2. package/package.json +14 -13
  3. package/runtime/scene-harness.json +1 -1
  4. package/runtime/sdk-types.json +1 -1
  5. package/src/api.ts +302 -0
  6. package/src/browserAuth.ts +87 -0
  7. package/src/cmgenTool.ts +104 -0
  8. package/src/commands/app.ts +891 -0
  9. package/src/commands/appAndroid.ts +603 -0
  10. package/src/commands/appDesktop.ts +326 -0
  11. package/src/commands/appDesktopMac.ts +470 -0
  12. package/src/commands/appIcon.ts +187 -0
  13. package/src/commands/appShared.ts +448 -0
  14. package/src/commands/appTemplates.ts +883 -0
  15. package/src/commands/appTemplatesAndroid.ts +599 -0
  16. package/src/commands/appTemplatesGradlew.ts +9 -0
  17. package/src/commands/assets.ts +28 -0
  18. package/src/commands/clone.ts +60 -0
  19. package/src/commands/compile.ts +253 -0
  20. package/src/commands/create.ts +59 -0
  21. package/src/commands/design.ts +863 -0
  22. package/src/commands/designTemplates.ts +11 -0
  23. package/src/commands/desktop.ts +122 -0
  24. package/src/commands/dev.ts +214 -0
  25. package/src/commands/diff.ts +59 -0
  26. package/src/commands/init.ts +191 -0
  27. package/src/commands/install.ts +198 -0
  28. package/src/commands/lightmap.ts +290 -0
  29. package/src/commands/link.ts +147 -0
  30. package/src/commands/login.ts +24 -0
  31. package/src/commands/navmesh.ts +225 -0
  32. package/src/commands/pn.ts +305 -0
  33. package/src/commands/projectTemplates.ts +129 -0
  34. package/src/commands/pull.ts +109 -0
  35. package/src/commands/push.ts +173 -0
  36. package/src/commands/render.ts +414 -0
  37. package/src/commands/scene.ts +148 -0
  38. package/src/commands/shaders.ts +157 -0
  39. package/src/commands/status.ts +29 -0
  40. package/src/commands/test.ts +370 -0
  41. package/src/commands/thumbs.ts +178 -0
  42. package/src/commands/types.ts +87 -0
  43. package/src/commands/update.ts +190 -0
  44. package/src/compile/assetIcons.ts +214 -0
  45. package/src/compile/collect.ts +46 -0
  46. package/src/compile/collectLocal.ts +37 -0
  47. package/src/compile/designCompile.ts +104 -0
  48. package/src/compile/fonts.ts +156 -0
  49. package/src/compile/headlessBundle.ts +129 -0
  50. package/src/compile/nativeStack.ts +21 -0
  51. package/src/compile/projectCompile.ts +109 -0
  52. package/src/compile/sceneCompile.ts +113 -0
  53. package/src/compile/screenEntry.ts +127 -0
  54. package/src/compile/shaders.ts +245 -0
  55. package/src/config.ts +42 -0
  56. package/src/designMeta.ts +35 -0
  57. package/src/desktopRenderer.ts +532 -0
  58. package/src/desktopScript.ts +276 -0
  59. package/src/dev/clientTemplates.ts +160 -0
  60. package/src/dev/devServer.ts +292 -0
  61. package/src/dev/wsServer.ts +144 -0
  62. package/src/distRoot.ts +20 -0
  63. package/src/ignore.ts +163 -0
  64. package/src/index.ts +491 -0
  65. package/src/lecodes-3d-editor.d.ts +41 -0
  66. package/src/lecodes-assets.d.ts +7 -0
  67. package/src/lecodes-design.d.ts +191 -0
  68. package/src/lecodes-renderer.d.ts +131 -0
  69. package/src/localFiles.ts +144 -0
  70. package/src/manifest.ts +46 -0
  71. package/src/matcTool.ts +137 -0
  72. package/src/peers.ts +40 -0
  73. package/src/project.ts +61 -0
  74. package/src/projectEnv.ts +94 -0
  75. package/src/qrcode-terminal.d.ts +9 -0
  76. package/src/releases.ts +125 -0
  77. package/src/serverDiff.ts +78 -0
  78. package/src/textDiff.ts +103 -0
  79. package/src/types.ts +0 -0
  80. package/src/util.ts +146 -0
  81. package/runtime/sdk/animate/animate.ts +0 -238
  82. package/runtime/sdk/animate/bezier.ts +0 -138
  83. package/runtime/sdk/animate/easings.ts +0 -126
  84. package/runtime/sdk/canvas/Canvas.ts +0 -305
  85. package/runtime/sdk/compile/aspectMacro.ts +0 -86
  86. package/runtime/sdk/compile/assetIconMacro.ts +0 -384
  87. package/runtime/sdk/compile/assetMacro.ts +0 -146
  88. package/runtime/sdk/compile/assetName.ts +0 -50
  89. package/runtime/sdk/compile/bundler.ts +0 -287
  90. package/runtime/sdk/compile/compileProject.ts +0 -129
  91. package/runtime/sdk/compile/detectEntry.ts +0 -128
  92. package/runtime/sdk/compile/fontMacro.ts +0 -459
  93. package/runtime/sdk/compile/fontRegistry.ts +0 -78
  94. package/runtime/sdk/compile/header.ts +0 -67
  95. package/runtime/sdk/compile/index.ts +0 -100
  96. package/runtime/sdk/compile/libraryImports.ts +0 -52
  97. package/runtime/sdk/compile/liteMaterial.ts +0 -247
  98. package/runtime/sdk/compile/sceneEditor.ts +0 -88
  99. package/runtime/sdk/compile/serverSplit.ts +0 -233
  100. package/runtime/sdk/compile/serverTypes.ts +0 -227
  101. package/runtime/sdk/compile/sfnt.ts +0 -98
  102. package/runtime/sdk/compile/shaderSchema.ts +0 -202
  103. package/runtime/sdk/compile/shaderTargets.ts +0 -81
  104. package/runtime/sdk/compile/sourcemap.ts +0 -25
  105. package/runtime/sdk/core/Aspect.ts +0 -568
  106. package/runtime/sdk/core/InspectorUI.ts +0 -212
  107. package/runtime/sdk/core/StateMachine.ts +0 -308
  108. package/runtime/sdk/core/color.ts +0 -66
  109. package/runtime/sdk/core/compWrite.ts +0 -42
  110. package/runtime/sdk/core/events.ts +0 -38
  111. package/runtime/sdk/core/fields.ts +0 -120
  112. package/runtime/sdk/core/registry.ts +0 -23
  113. package/runtime/sdk/core/signals.ts +0 -277
  114. package/runtime/sdk/core/time.ts +0 -84
  115. package/runtime/sdk/g2/Camera2D.ts +0 -40
  116. package/runtime/sdk/g2/CharacterController2D.ts +0 -276
  117. package/runtime/sdk/g2/Node2D.ts +0 -267
  118. package/runtime/sdk/g2/OneWay2D.ts +0 -66
  119. package/runtime/sdk/g2/Physics2D.ts +0 -346
  120. package/runtime/sdk/g2/Scene2D.ts +0 -209
  121. package/runtime/sdk/g2/Shape2D.ts +0 -259
  122. package/runtime/sdk/g2/Sprite.ts +0 -89
  123. package/runtime/sdk/g2/SpriteAnimation.ts +0 -171
  124. package/runtime/sdk/g2/SpriteSheet.ts +0 -166
  125. package/runtime/sdk/g2/Texture2D.ts +0 -47
  126. package/runtime/sdk/g2/Tilemap.ts +0 -41
  127. package/runtime/sdk/g2/Tileset.ts +0 -71
  128. package/runtime/sdk/g2/Trigger2D.ts +0 -77
  129. package/runtime/sdk/g2/autotile.ts +0 -433
  130. package/runtime/sdk/g2/cells.ts +0 -91
  131. package/runtime/sdk/g2/defineScene2d.ts +0 -381
  132. package/runtime/sdk/g2/groups2d.ts +0 -106
  133. package/runtime/sdk/g2/loop.ts +0 -50
  134. package/runtime/sdk/g2/scenarios2d.ts +0 -69
  135. package/runtime/sdk/g2/touch.ts +0 -83
  136. package/runtime/sdk/gl/Camera.ts +0 -160
  137. package/runtime/sdk/gl/CameraPlace.ts +0 -52
  138. package/runtime/sdk/gl/CharacterController.ts +0 -238
  139. package/runtime/sdk/gl/Gearbox.ts +0 -212
  140. package/runtime/sdk/gl/Geometry.ts +0 -279
  141. package/runtime/sdk/gl/IK.ts +0 -193
  142. package/runtime/sdk/gl/InstancedMesh.ts +0 -132
  143. package/runtime/sdk/gl/Light.ts +0 -107
  144. package/runtime/sdk/gl/Lightmap.ts +0 -256
  145. package/runtime/sdk/gl/Material.ts +0 -260
  146. package/runtime/sdk/gl/Mesh.ts +0 -83
  147. package/runtime/sdk/gl/Model.ts +0 -124
  148. package/runtime/sdk/gl/NavAgent.ts +0 -337
  149. package/runtime/sdk/gl/NavMesh.ts +0 -397
  150. package/runtime/sdk/gl/Node.ts +0 -350
  151. package/runtime/sdk/gl/Noise.ts +0 -30
  152. package/runtime/sdk/gl/Particles.ts +0 -676
  153. package/runtime/sdk/gl/Physics.ts +0 -222
  154. package/runtime/sdk/gl/Plane.ts +0 -53
  155. package/runtime/sdk/gl/Ragdoll.ts +0 -270
  156. package/runtime/sdk/gl/Ray.ts +0 -16
  157. package/runtime/sdk/gl/Scene.ts +0 -486
  158. package/runtime/sdk/gl/Shape.ts +0 -409
  159. package/runtime/sdk/gl/Terrain.ts +0 -1092
  160. package/runtime/sdk/gl/Texture.ts +0 -63
  161. package/runtime/sdk/gl/Trigger.ts +0 -45
  162. package/runtime/sdk/gl/Vehicle.ts +0 -473
  163. package/runtime/sdk/gl/Wheel.ts +0 -240
  164. package/runtime/sdk/gl/animation/AnimationClip.ts +0 -204
  165. package/runtime/sdk/gl/animation/Animator.ts +0 -120
  166. package/runtime/sdk/gl/animation/Layer.ts +0 -31
  167. package/runtime/sdk/gl/animation/Loop.ts +0 -30
  168. package/runtime/sdk/gl/animation/Playback.ts +0 -44
  169. package/runtime/sdk/gl/animation/core.ts +0 -336
  170. package/runtime/sdk/gl/controls.ts +0 -95
  171. package/runtime/sdk/gl/physicsEvents.ts +0 -20
  172. package/runtime/sdk/gl/scenarios.ts +0 -291
  173. package/runtime/sdk/gl/state.ts +0 -6
  174. package/runtime/sdk/gl/terrainMesh.ts +0 -219
  175. package/runtime/sdk/gl/touch.ts +0 -68
  176. package/runtime/sdk/inject.ts +0 -197
  177. package/runtime/sdk/math/Mathf.ts +0 -118
  178. package/runtime/sdk/math/mat4.ts +0 -278
  179. package/runtime/sdk/math/quat.ts +0 -232
  180. package/runtime/sdk/math/vec.ts +0 -255
  181. package/runtime/sdk/net/codec.ts +0 -119
  182. package/runtime/sdk/net/core.ts +0 -384
  183. package/runtime/sdk/net/index.ts +0 -181
  184. package/runtime/sdk/net/replication.ts +0 -622
  185. package/runtime/sdk/plugins/camera.ts +0 -81
  186. package/runtime/sdk/plugins/geolocation.ts +0 -123
  187. package/runtime/sdk/plugins/oauth.ts +0 -61
  188. package/runtime/sdk/plugins/permission.ts +0 -7
  189. package/runtime/sdk/plugins/push.ts +0 -132
  190. package/runtime/sdk/plugins/qr.ts +0 -73
  191. package/runtime/sdk/plugins/service.ts +0 -47
  192. package/runtime/sdk/runtime/app.ts +0 -101
  193. package/runtime/sdk/runtime/appEvents.ts +0 -54
  194. package/runtime/sdk/runtime/channel.ts +0 -61
  195. package/runtime/sdk/runtime/clipboard.ts +0 -20
  196. package/runtime/sdk/runtime/datetime.ts +0 -329
  197. package/runtime/sdk/runtime/device.ts +0 -293
  198. package/runtime/sdk/runtime/fetch.ts +0 -77
  199. package/runtime/sdk/runtime/files.ts +0 -23
  200. package/runtime/sdk/runtime/input.ts +0 -175
  201. package/runtime/sdk/runtime/media.ts +0 -111
  202. package/runtime/sdk/runtime/misc.ts +0 -16
  203. package/runtime/sdk/runtime/net.ts +0 -36
  204. package/runtime/sdk/runtime/rpc.ts +0 -218
  205. package/runtime/sdk/runtime/service.ts +0 -83
  206. package/runtime/sdk/runtime/share.ts +0 -9
  207. package/runtime/sdk/runtime/storage.ts +0 -13
  208. package/runtime/sdk/runtime/touch.ts +0 -76
  209. package/runtime/sdk/scene/defineScene.ts +0 -1353
  210. package/runtime/sdk/scene/editorPlugins.ts +0 -110
  211. package/runtime/sdk/scene/gizmos.ts +0 -148
  212. package/runtime/sdk/scene/grammar.ts +0 -120
  213. package/runtime/sdk/scene/material.ts +0 -188
  214. package/runtime/sdk/server/auth/appConfig.ts +0 -12
  215. package/runtime/sdk/server/auth/global.ts +0 -80
  216. package/runtime/sdk/server/auth/host.ts +0 -318
  217. package/runtime/sdk/server/auth/models.ts +0 -83
  218. package/runtime/sdk/server/auth/types.ts +0 -50
  219. package/runtime/sdk/server/channel.ts +0 -56
  220. package/runtime/sdk/server/context.ts +0 -36
  221. package/runtime/sdk/server/db/defineDb.ts +0 -237
  222. package/runtime/sdk/server/db/fields.ts +0 -132
  223. package/runtime/sdk/server/db/httpTransport.ts +0 -93
  224. package/runtime/sdk/server/db/index.ts +0 -7
  225. package/runtime/sdk/server/db/marci/query.ts +0 -412
  226. package/runtime/sdk/server/db/types.ts +0 -202
  227. package/runtime/sdk/server/errors.ts +0 -12
  228. package/runtime/sdk/server/host.ts +0 -74
  229. package/runtime/sdk/server/inject.ts +0 -13
  230. package/runtime/sdk/server/runtime.ts +0 -133
  231. package/runtime/sdk/server/validate.ts +0 -87
  232. package/runtime/sdk/ui/NativeView.ts +0 -142
  233. package/runtime/sdk/ui/UI.ts +0 -39
  234. package/runtime/sdk/ui/UIBottomSheet.ts +0 -139
  235. package/runtime/sdk/ui/UIButton.ts +0 -101
  236. package/runtime/sdk/ui/UIContainer.ts +0 -60
  237. package/runtime/sdk/ui/UIImage.ts +0 -83
  238. package/runtime/sdk/ui/UIInput.ts +0 -185
  239. package/runtime/sdk/ui/UIModal.ts +0 -139
  240. package/runtime/sdk/ui/UINode.ts +0 -830
  241. package/runtime/sdk/ui/UIPager.ts +0 -362
  242. package/runtime/sdk/ui/UIPopover.ts +0 -100
  243. package/runtime/sdk/ui/UIScreen.ts +0 -123
  244. package/runtime/sdk/ui/UIScrollable.ts +0 -87
  245. package/runtime/sdk/ui/UISpacer.ts +0 -14
  246. package/runtime/sdk/ui/UITabs.ts +0 -236
  247. package/runtime/sdk/ui/UIText.ts +0 -51
  248. package/runtime/sdk/ui/UIVideo.ts +0 -88
  249. package/runtime/sdk/ui/UIVirtualizedList.ts +0 -241
  250. package/runtime/sdk/ui/UIWidget.ts +0 -127
  251. package/runtime/sdk/ui/fonts.ts +0 -13
  252. package/runtime/sdk/ui/presentable.ts +0 -117
  253. package/runtime/sdk/ui/router.ts +0 -132
  254. package/runtime/sdk/ui/theme.ts +0 -84
@@ -0,0 +1,448 @@
1
+ import { execFileSync } from "node:child_process"
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
3
+ import { homedir } from "node:os"
4
+ import { dirname, join, resolve } from "node:path"
5
+ import { findProjectRoot } from "../manifest"
6
+ import { DEFAULT_IGNORE_FILE } from "../ignore"
7
+ import { CliError, c, flagStr, log, note, warn, type Args } from "../util"
8
+
9
+ /* Shared plumbing for the `lecodes app` platform arms (app.ts = iOS + dispatch,
10
+ * appAndroid.ts = Android). Everything here moved verbatim out of app.ts when the Android
11
+ * arm landed — behavior for iOS is unchanged; the SDK helpers just grew a repo parameter
12
+ * (lecodes-ios-sdk vs lecodes-android-sdk) and resolvePlugin a platform one. */
13
+
14
+ /** First-party plugin registry: id → GitHub repo holding plugins/<id>/. Third-party plugins
15
+ * use a `github:owner/repo[@tag]` spec in app.json instead (manifest at the repo root). */
16
+ export const FIRST_PARTY_PLUGINS: Record<string, string> = {
17
+ "camera": "letary/lecodes-plugins",
18
+ "qr-scanner": "letary/lecodes-plugins",
19
+ "geolocation": "letary/lecodes-plugins",
20
+ "push": "letary/lecodes-plugins",
21
+ }
22
+
23
+ export interface AppConfig {
24
+ name?: string
25
+ version?: string
26
+ /** Project-root-relative path of a square ≥1024px PNG/JPEG master. When set, every sync
27
+ * renders the platform icons from it (iOS AppIcon + Android adaptive/legacy mipmaps) and
28
+ * those files become CLI-owned like assets/. Absent = icons stay hand-managed; removing
29
+ * the key stops regeneration but deletes nothing. */
30
+ icon?: string
31
+ /** Icon backdrop ("#RGB"/"#RRGGBB", default white): the Android adaptive-icon background
32
+ * layer, and what a transparent master is flattened onto for the opacity-required iOS icon. */
33
+ iconBackground?: string
34
+ plugins?: Record<string, string>
35
+ /** `remote`: lecodesignore-style globs of resources to STREAM from the server instead of
36
+ * embedding in the app (server-backed projects only — a streamed asset needs a server URL). */
37
+ resources?: { remote?: string[] }
38
+ /** Over-the-air JS updates: where the shell checks for newer compiled code at launch
39
+ * (natively, before the JS engine — offline is a no-op, new code applies next launch).
40
+ * `true` = this project's live bundle on the platform (server-backed only); or an https
41
+ * URL to a compiled bundle on any hosting (string or `{ "url": … }`). Absent = no OTA.
42
+ * iOS-only for now — the Android runtime has no native updater yet. */
43
+ update?: boolean | string | { url?: string }
44
+ /** `variant`: SDK product — "auto" (default: sync picks the smallest one covering the engines
45
+ * the compiled bundle actually uses) or core|2d|3d|full to force one.
46
+ * `sdk`: native SDK version — an exact lecodes-ios-sdk release (init pins the newest one,
47
+ * `lecodes app update` moves it), or "latest" to re-resolve on every sync. */
48
+ ios?: { bundleId?: string, teamId?: string, variant?: string, buildNumber?: number, sdk?: string }
49
+ /** `applicationId`: the Android package name (init default: com.example.<slug>).
50
+ * `versionCode`: Play's monotonically increasing build number (versionName comes from the
51
+ * shared `version`). `sdk`: an exact lecodes-android-sdk release or "latest", like ios.sdk.
52
+ * `variant`: SDK artifact — "auto" (default: sync picks the smallest one covering the engines
53
+ * the compiled bundle actually uses) or core|2d|3d|full to force one, like ios.variant
54
+ * (needs lecodes-android-sdk >= 1.3.0 — older releases ship only the full AAR).
55
+ * `signing`: keystore METADATA only — the store/key passwords live in android/
56
+ * keystore.properties (gitignored) or LECODES_ANDROID_* env vars, never in app.json. */
57
+ android?: {
58
+ applicationId?: string, versionCode?: number, sdk?: string, variant?: string,
59
+ signing?: { storeFile?: string, keyAlias?: string },
60
+ }
61
+ /** Desktop host (packages/desktop). `window` sizes are LOGICAL units — the host multiplies by
62
+ * the display scale. `entry` picks a per-target entry file (default: the detected project
63
+ * entry) — see docs/desktop-target-plan.md. */
64
+ desktop?: {
65
+ entry?: string,
66
+ window?: DesktopWindowConfig,
67
+ /** Which desktop host binary to run: "gl" (default) or "vulkan" (Windows only, host >= 1.4.0).
68
+ * A BUILD/DEV-TIME host selection, not a runtime switch — the two are separate binaries with
69
+ * their own shaders and different features (the Vulkan one has no 2D engine and no native-view
70
+ * plugins). Overridable per run with `--renderer` / LECODES_DESKTOP_RENDERER. */
71
+ renderer?: "gl" | "vulkan",
72
+ /** 3D buffer scale vs the window (0.25–1, host ≥1.4.0). The UI always renders at native
73
+ * pixels; only the 3D scene is rendered smaller and upscaled by the compositor. Multiplies
74
+ * the scene's own `renderScale` (SceneOptions). Headless render/test ignore it. */
75
+ renderScale?: number,
76
+ /** 3D buffer cap in px, e.g. [1920, 1080]: on a bigger window/monitor the 3D renders at most
77
+ * this size (aspect kept) — the per-machine answer to "fullscreen on a 4K display is slow". */
78
+ maxRenderSize?: [number, number],
79
+ /** The macOS .app build (`lecodes app build desktop` on a Mac) — identity + signing +
80
+ * notarization. See appDesktopMac.ts for the whole story. */
81
+ macos?: MacDesktopConfig,
82
+ }
83
+ }
84
+
85
+ /** app.json `desktop.macos`. Only NAMES live here (identity, keychain profile) — the certificate
86
+ * and the notarization password stay in the keychain. */
87
+ export interface MacDesktopConfig {
88
+ /** CFBundleIdentifier (default codes.le.app.<slug>). Reverse-DNS, stable across releases. */
89
+ bundleId?: string
90
+ /** CFBundleVersion (default 1). Bump per upload; the top-level `version` is the marketing one. */
91
+ buildNumber?: number
92
+ /** LSApplicationCategoryType, e.g. "public.app-category.games". */
93
+ category?: string
94
+ /** `identity`: a "Developer ID Application: Name (TEAM)" codesign identity, or "adhoc" (local
95
+ * only — refused on every other Mac). Absent/"auto": the keychain's Developer ID cert, else
96
+ * ad-hoc. Overridden by --sign / LECODES_MACOS_SIGN_ID. */
97
+ signing?: { identity?: string }
98
+ /** `profile`: a `xcrun notarytool store-credentials <name>` keychain profile. With it the build
99
+ * is notarized + stapled (needs a Developer ID identity). Overridden by --notarize /
100
+ * LECODES_NOTARY_PROFILE; --no-notarize skips for a local build. */
101
+ notarize?: { profile?: string }
102
+ }
103
+
104
+ export interface DesktopWindowConfig {
105
+ /** Initial client size, logical units (host default: 1280×720). */
106
+ width?: number
107
+ height?: number
108
+ minWidth?: number
109
+ minHeight?: number
110
+ /** false → fixed-size window (default: resizable). */
111
+ resizable?: boolean
112
+ /** Window title (default: the top-level `name`). */
113
+ title?: string
114
+ /** Borderless fullscreen on the primary monitor (kiosks). `width`/`height` stay the
115
+ * design/dev frame; headless render/test always run windowed (the render contract). */
116
+ fullscreen?: boolean
117
+ /** Display scale (host ≥1.3.0). "system" (default): the OS display scale. A number (0.5–4):
118
+ * a fixed density — the window opens at `width×scale` physical px. "fit": derived at launch
119
+ * from the design frame vs the monitor (fullscreen) / work area, so a 1080p-designed app
120
+ * runs density-2 on a 4K display and scales DOWN on smaller screens. Headless render/test
121
+ * always force scale 1 (the render contract). */
122
+ scale?: number | "fit" | "system"
123
+ }
124
+
125
+ /** app.json wherever a command needs the project-level desktop/window config. Tolerant of a
126
+ * missing or broken file — standalone projects (no app.json) must keep working everywhere
127
+ * this is consulted. */
128
+ export const readAppConfigAt = (root: string): AppConfig | null =>
129
+ readJson<AppConfig>(join(root, "app.json"))
130
+
131
+ /** The desktop host's own built-in fallback, mirrored where the CLI must fill a missing side. */
132
+ export const DESKTOP_DEFAULT_WINDOW = { width: 1280, height: 720 }
133
+
134
+ /**
135
+ * The renderer for this run: `--renderer` > LECODES_DESKTOP_RENDERER > app.json `desktop.renderer`
136
+ * > "gl". Resolve it ONCE per command and pass the result to both `resolveDesktopExe` and the
137
+ * shader staging — two independent reads of the config is how a build ends up with a Vulkan exe
138
+ * next to an OpenGL .filamat, which fails at the first material with a Filament abort.
139
+ */
140
+ export const resolveDesktopRenderer = (root: string, args?: Args): "gl" | "vulkan" => {
141
+ const raw = (args ? flagStr(args, "renderer") : undefined)
142
+ ?? process.env.LECODES_DESKTOP_RENDERER
143
+ ?? readAppConfigAt(root)?.desktop?.renderer
144
+ if (raw === undefined) return "gl"
145
+ const value = String(raw).toLowerCase()
146
+ if (value === "gl" || value === "opengl") return "gl"
147
+ if (value === "vulkan" || value === "vk") {
148
+ // The macOS host is Metal, full stop. A project tuned for the Windows Vulkan host must still
149
+ // build/run on a Mac, so the setting is a no-op here rather than an error.
150
+ if (process.platform === "darwin") { note("desktop.renderer \"vulkan\" is Windows-only — the macOS host renders with Metal."); return "gl" }
151
+ return "vulkan"
152
+ }
153
+ throw new CliError(`Unknown desktop renderer "${raw}". Use "gl" or "vulkan".`)
154
+ }
155
+
156
+ /** Project-level default viewport for HEADLESS render/test: the desktop window when the project
157
+ * declares desktop as its ONLY native target (a desktop-only project gets desktop-sized frames
158
+ * and flow tests for free). Mixed or mobile projects keep the phone default — headless frames
159
+ * exercise the phone UI there. */
160
+ export const desktopDefaultViewport = (root: string): { width: number, height: number } | null => {
161
+ const cfg = readAppConfigAt(root)
162
+ if (!cfg?.desktop || cfg.ios || cfg.android) return null
163
+ return {
164
+ width: cfg.desktop.window?.width ?? DESKTOP_DEFAULT_WINDOW.width,
165
+ height: cfg.desktop.window?.height ?? DESKTOP_DEFAULT_WINDOW.height,
166
+ }
167
+ }
168
+
169
+ export interface PluginManifest {
170
+ id: string
171
+ version?: string
172
+ provides?: { views?: string[], services?: string[] }
173
+ ios?: {
174
+ sources?: string, module?: string, register?: string, minSdk?: string,
175
+ infoPlist?: Record<string, string>,
176
+ /** Entitlements the plugin's capability needs (plist-typed JSON values, arrays allowed —
177
+ * e.g. push's `{"aps-environment": "development"}`). Merged into App/App.entitlements
178
+ * by sync; the relay signs sends for Letary-published bundle ids with the shared team
179
+ * key, and the register route fail-fasts for anything outside that namespace. */
180
+ entitlements?: Record<string, unknown>,
181
+ } | null
182
+ android?: {
183
+ /** Dir vendored into the shell (default "android") — a Gradle-module-shaped tree minus
184
+ * build.gradle (sync generates that): src/main/AndroidManifest.xml carries the plugin's
185
+ * permissions/services (AGP's manifest merger is the infoPlist analog), libs/*.aar become
186
+ * local file deps, consumer-rules.pro becomes the module's consumer ProGuard rules. */
187
+ sources?: string,
188
+ /** Gradle module name (kebab-case) → :plugins:<module> in the shell. */
189
+ module?: string,
190
+ /** Fully-qualified Kotlin object with `fun register(engine: LecodesEngine, context: Context)`;
191
+ * its package doubles as the generated module's AGP namespace. */
192
+ register?: string,
193
+ /** Minimum lecodes-android-sdk release (NOT Android's minSdkVersion — the OS floor comes
194
+ * from the SDK itself). Sync hard-errors below it, like ios.minSdk. */
195
+ minSdk?: string,
196
+ /** Maven coordinates for the generated build.gradle. A "platform:" prefix maps to
197
+ * implementation(platform(…)) — BoMs; versionless coords are only valid under one. */
198
+ dependencies?: string[],
199
+ /** App-level Gradle plugin ids the shell must apply (e.g. com.google.gms.google-services).
200
+ * Bare ids resolve through the CLI's known-version table; "id:version" pins explicitly. */
201
+ gradlePlugins?: string[],
202
+ } | null
203
+ }
204
+
205
+ export interface LockEntry { repo: string, tag: string, commit: string, version: string }
206
+
207
+ export const readJson = <T>(path: string): T | null => {
208
+ if (!existsSync(path)) return null
209
+ // BOM-tolerant: Notepad and PowerShell write UTF-8 with a BOM, and app.json is hand-edited.
210
+ try { return JSON.parse(readFileSync(path, "utf8").replace(/^\uFEFF/, "")) as T } catch {
211
+ throw new CliError(`Invalid JSON in ${path}`)
212
+ }
213
+ }
214
+
215
+ export const writeJson = (path: string, value: unknown) =>
216
+ writeFileSync(path, JSON.stringify(value, null, 2) + "\n")
217
+
218
+ export const projectRoot = (): string => {
219
+ try { return findProjectRoot(process.cwd()) } catch { return process.cwd() }
220
+ }
221
+
222
+ export const git = (cwd: string, ...args: string[]): string =>
223
+ execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim()
224
+
225
+ export const slugify = (s: string) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "app"
226
+
227
+ /** Run a tool, capturing output; on failure surface the tail (xcodebuild/gradle logs are huge). */
228
+ export const runTool = (cwd: string, cmd: string, args: string[]): string => {
229
+ try {
230
+ return execFileSync(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 128 * 1024 * 1024 })
231
+ } catch (e) {
232
+ const err = e as { stdout?: string, stderr?: string }
233
+ const tail = ((err.stdout ?? "") + "\n" + (err.stderr ?? "")).split("\n").filter(l => l.trim()).slice(-25).join("\n")
234
+ throw new CliError(`${cmd} ${args[0] ?? ""} failed:\n${tail}`)
235
+ }
236
+ }
237
+
238
+ /** Add a shell dir (ios/, android/) to .lecodesignore — shells are never pushed to LeCodes. */
239
+ export const ensureShellIgnored = (root: string, dir: string) => {
240
+ const ignorePath = join(root, ".lecodesignore")
241
+ const ignore = existsSync(ignorePath) ? readFileSync(ignorePath, "utf8") : DEFAULT_IGNORE_FILE
242
+ if (new RegExp(`^${dir}\\/$`, "m").test(ignore)) return
243
+ const comment = ignore.includes("# Native app shells")
244
+ ? ""
245
+ : "# Native app shells — generated by `lecodes app init`, never pushed to LeCodes.\n"
246
+ writeFileSync(ignorePath, ignore.replace(/\n*$/, "\n") + "\n" + comment + `${dir}/\n`)
247
+ log(`${c.green("updated")} .lecodesignore (+ ${dir}/)`)
248
+ }
249
+
250
+ /* ----------------------------------- SDK variants ----------------------------------- */
251
+
252
+ // Both native SDKs ship the same variant matrix (full / 3d / 2d / core), so the detection and
253
+ // resolution logic is shared between the iOS (app.ts) and Android (appAndroid.ts) syncs.
254
+ export const SDK_VARIANTS = ["core", "2d", "3d", "full"] as const
255
+ type SdkVariantShared = (typeof SDK_VARIANTS)[number]
256
+
257
+ /** Engine signals from a compiled bundle's header — the same lines buildHeader emits.
258
+ * No `// gl: disable` line → the 3D engine is used (eagerly, or lazily via `gl: defer`);
259
+ * `// 2d: enable` → the 2D engine is used. */
260
+ export const detectEngines = (bundleJs: string): { gl: boolean, g2: boolean } => {
261
+ const lines: string[] = []
262
+ for (const line of bundleJs.split("\n")) {
263
+ if (!line.startsWith("//")) break
264
+ lines.push(line)
265
+ }
266
+ const header = lines.join("\n")
267
+ return { gl: !/^\/\/ gl: disable$/m.test(header), g2: /^\/\/ 2d: enable$/m.test(header) }
268
+ }
269
+
270
+ /**
271
+ * Refuse a Vulkan desktop build for a project that uses the 2D engine. The Vulkan host is built with
272
+ * CREATOR_PKG_2D=OFF (creator-2d is sokol-on-GL, with no Vulkan twin), so those scenes don't render
273
+ * — a blank window at runtime with nothing in the log to explain it. Fail at build time instead,
274
+ * where the fix ("use the default renderer") is one line away.
275
+ *
276
+ * Detection is the same `// 2d: enable` bundle header `lecodes app sync` picks its SDK variant from,
277
+ * so it can't drift from what the engine actually needs.
278
+ */
279
+ export const assertRendererSupportsBundle = (renderer: "gl" | "vulkan", bundleJs: string): void => {
280
+ if (renderer !== "vulkan") return
281
+ if (!detectEngines(bundleJs).g2) return
282
+ throw new CliError(
283
+ "This project uses the 2D engine, which the Vulkan desktop host doesn't include " +
284
+ "(creator-2d renders through OpenGL and has no Vulkan path yet).\n" +
285
+ " Build with the default renderer: drop `desktop.renderer` from app.json, or pass `--renderer gl`.",
286
+ )
287
+ }
288
+
289
+ /** The SDK product for a shell: the smallest variant covering the engines the compiled bundle
290
+ * actually uses, unless <configKey> (ios.variant / android.variant) forces one — then warn when
291
+ * the forced product can't run the project (missing engine = those scenes silently no-op on
292
+ * device). */
293
+ export const resolveVariant = (requested: string, bundleJs: string, configKey = "ios.variant"): { variant: SdkVariantShared, detected: SdkVariantShared } => {
294
+ const uses = detectEngines(bundleJs)
295
+ const detected: SdkVariantShared = uses.gl && uses.g2 ? "full" : uses.gl ? "3d" : uses.g2 ? "2d" : "core"
296
+ if (requested === "auto") return { variant: detected, detected }
297
+ const variant = requested as SdkVariantShared
298
+ const missing = [
299
+ uses.gl && variant !== "3d" && variant !== "full" ? "3D" : null,
300
+ uses.g2 && variant !== "2d" && variant !== "full" ? "2D" : null,
301
+ ].filter(Boolean)
302
+ if (missing.length > 0) {
303
+ warn(`project uses the ${missing.join(" + ")} engine but ${configKey} forces "${variant}" — those scenes won't run on device; remove ${configKey} to auto-select "${detected}".`)
304
+ }
305
+ return { variant, detected }
306
+ }
307
+
308
+ /* ----------------------------------- SDK version ------------------------------------ */
309
+
310
+ export const VERSION_RE = /^\d+\.\d+\.\d+$/
311
+
312
+ /** Numeric semver-ish compare (only x.y.z tags are published). */
313
+ export const cmpVersion = (a: string, b: string): number => {
314
+ const pa = a.split(".").map(Number)
315
+ const pb = b.split(".").map(Number)
316
+ for (let i = 0; i < 3; i++) {
317
+ if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0)
318
+ }
319
+ return 0
320
+ }
321
+
322
+ /** The newest published release of an SDK dist repo (prereleases excluded). Network — throws. */
323
+ export const fetchLatestSdkVersion = async (repo: string): Promise<string> => {
324
+ const url = `https://api.github.com/repos/${repo}/releases/latest`
325
+ const res = await fetch(url, { headers: { "accept": "application/vnd.github+json", "user-agent": "lecodes-cli" } })
326
+ if (!res.ok) throw new CliError(`Could not read the latest ${repo} release (HTTP ${res.status}) — ${url}`)
327
+ const tag = ((await res.json()) as { tag_name?: string }).tag_name?.replace(/^v/, "") ?? ""
328
+ if (!VERSION_RE.test(tag)) throw new CliError(`${repo} latest release has no x.y.z tag (got "${tag}")`)
329
+ return tag
330
+ }
331
+
332
+ /** Resolve an app.json sdk value ("latest" or an exact release) to the concrete release the
333
+ * shell links; `fallback` is what "latest" degrades to when the network is unavailable. */
334
+ export const resolveSdkVersion = async (
335
+ pin: string, fallback: string | null,
336
+ opts: { repo: string, minVersion: string, configKey: string },
337
+ ): Promise<string> => {
338
+ if (pin !== "latest") {
339
+ if (!VERSION_RE.test(pin)) {
340
+ throw new CliError(`app.json ${opts.configKey} must be "latest" or an exact x.y.z release (got "${pin}").`)
341
+ }
342
+ if (cmpVersion(pin, opts.minVersion) < 0) {
343
+ throw new CliError(`app.json ${opts.configKey} ${pin} is older than the ${opts.minVersion} these generated shells need.`)
344
+ }
345
+ return pin
346
+ }
347
+ try {
348
+ return await fetchLatestSdkVersion(opts.repo)
349
+ } catch (e) {
350
+ const kept = fallback ?? opts.minVersion
351
+ warn(`Could not resolve the latest SDK (${(e as Error).message}) — staying on ${kept}.`)
352
+ return kept
353
+ }
354
+ }
355
+
356
+ /* ---------------------------------- plugin vendoring ---------------------------------- */
357
+
358
+ export interface ResolvedPlugin {
359
+ id: string
360
+ dir: string // directory containing lecodes-plugin.json
361
+ manifest: PluginManifest
362
+ lock: LockEntry
363
+ }
364
+
365
+ /** Clone-or-update the plugin repo cache and check out the wanted commit (detached).
366
+ * `LECODES_PLUGINS_DIR` overrides with a local working tree — the plugin-development loop. */
367
+ export const ensureRepo = (repo: string, wantTag: string | null, lock: LockEntry | undefined): { dir: string, tag: string, commit: string } => {
368
+ const override = process.env.LECODES_PLUGINS_DIR
369
+ if (override) {
370
+ note(`Using local plugin checkout ${override} (LECODES_PLUGINS_DIR)`)
371
+ return { dir: resolve(override), tag: "local", commit: "local" }
372
+ }
373
+
374
+ const cache = join(homedir(), ".lecodes", "plugins", repo.replace("/", "__"))
375
+ const url = `https://github.com/${repo}.git`
376
+ if (!existsSync(join(cache, ".git"))) {
377
+ mkdirSync(dirname(cache), { recursive: true })
378
+ note(`Cloning ${url}…`)
379
+ git(dirname(cache), "clone", "--quiet", url, cache)
380
+ }
381
+
382
+ // A lock pin wins; else an explicit @tag; else the latest v* tag (fetched fresh).
383
+ let tag = lock?.tag ?? wantTag
384
+ let commit = lock?.commit
385
+ if (commit) {
386
+ try { git(cache, "cat-file", "-e", `${commit}^{commit}`) } catch {
387
+ git(cache, "fetch", "--quiet", "--tags", "origin")
388
+ }
389
+ } else {
390
+ git(cache, "fetch", "--quiet", "--tags", "origin")
391
+ if (!tag) {
392
+ tag = git(cache, "tag", "--list", "v*", "--sort=-version:refname").split("\n")[0] ?? ""
393
+ if (!tag) throw new CliError(`${repo} has no v* version tags to install from`)
394
+ }
395
+ commit = git(cache, "rev-list", "-n", "1", tag)
396
+ }
397
+ git(cache, "checkout", "--quiet", "--detach", commit)
398
+ return { dir: cache, tag: tag ?? "unknown", commit }
399
+ }
400
+
401
+ /** Resolve one app.json plugin entry to vendorable sources + manifest + lock pin. The platform
402
+ * being synced must be supported (its manifest branch non-null) — a hard error otherwise. */
403
+ export const resolvePlugin = (id: string, spec: string, lock: LockEntry | undefined, platform: "ios" | "android"): ResolvedPlugin => {
404
+ let repo: string
405
+ let subdir: string
406
+ let wantTag: string | null = null
407
+
408
+ if (spec.startsWith("github:")) {
409
+ const [repoPart, tagPart] = spec.slice("github:".length).split("@")
410
+ repo = repoPart
411
+ wantTag = tagPart ?? null
412
+ subdir = "" // third-party: manifest at the repo root
413
+ } else {
414
+ repo = FIRST_PARTY_PLUGINS[id]
415
+ if (!repo) {
416
+ throw new CliError(`Unknown plugin "${id}" — known: ${Object.keys(FIRST_PARTY_PLUGINS).join(", ")}, or use a github:owner/repo spec.`)
417
+ }
418
+ subdir = `plugins/${id}`
419
+ }
420
+
421
+ const checkout = ensureRepo(repo, wantTag, lock?.repo === repo ? lock : undefined)
422
+ const dir = subdir ? join(checkout.dir, subdir) : checkout.dir
423
+ const manifest = readJson<PluginManifest>(join(dir, "lecodes-plugin.json"))
424
+ if (!manifest) throw new CliError(`Plugin "${id}": no lecodes-plugin.json in ${dir}`)
425
+ if (manifest.id !== id) throw new CliError(`Plugin "${id}": manifest declares id "${manifest.id}"`)
426
+ if (!manifest[platform]) {
427
+ throw new CliError(`Plugin "${id}" does not support ${platform === "ios" ? "iOS" : "Android"} (manifest "${platform}" is null)`)
428
+ }
429
+
430
+ // Namespacing: third-party channel names must be vendor-prefixed ("acme.thing").
431
+ if (spec.startsWith("github:")) {
432
+ const names = [...(manifest.provides?.views ?? []), ...(manifest.provides?.services ?? [])]
433
+ for (const name of names) {
434
+ if (!name.includes(".")) warn(`Plugin "${id}" provides bare channel name "${name}" — third-party names should be vendor-prefixed (e.g. "acme.${name}").`)
435
+ }
436
+ }
437
+
438
+ return {
439
+ id, dir, manifest,
440
+ lock: { repo, tag: checkout.tag, commit: checkout.commit, version: manifest.version ?? "unknown" },
441
+ }
442
+ }
443
+
444
+ /** Newest v* tag of a plugin repo, via the shared plugin cache. Null when it can't be read
445
+ * (no network, no tags, local override) — `app update` reports it and moves on. */
446
+ export const latestPluginTag = (repo: string): string | null => {
447
+ try { return ensureRepo(repo, null, undefined).tag } catch { return null }
448
+ }