lecodes-cli 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +1069 -396
  3. package/package.json +14 -13
  4. package/runtime/scene-harness.json +1 -1
  5. package/runtime/sdk-types.json +1 -1
  6. package/runtime/web/assets/Roboto-Variable-DHm9jpd-.woff2 +0 -0
  7. package/runtime/web/assets/__vite-browser-external-BIHI7g3E.js +1 -0
  8. package/runtime/web/assets/basis-C64VHDVD.js +1 -0
  9. package/runtime/web/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
  10. package/runtime/web/assets/createViewerLite-BvHobVhp.js +852 -0
  11. package/runtime/web/assets/draco-BiISTFcR.js +118 -0
  12. package/runtime/web/assets/draco_decoder-DsQ12WqX.wasm +0 -0
  13. package/runtime/web/assets/index-CfNCCBjN.js +2 -0
  14. package/runtime/web/assets/mapViewImpl-B2JcES8l.js +810 -0
  15. package/runtime/web/assets/maplibre-gl-worker-CJfwIrte.js +8 -0
  16. package/runtime/web/assets/neutral_ibl128-D-lzdkLN.ktx +0 -0
  17. package/runtime/web/assets/worker-Caf-yYEI.js +2 -0
  18. package/runtime/web/embed.html +32 -0
  19. package/runtime/web/embed.js +171 -0
  20. package/src/api.ts +302 -0
  21. package/src/browserAuth.ts +87 -0
  22. package/src/cmgenTool.ts +104 -0
  23. package/src/commands/app.ts +896 -0
  24. package/src/commands/appAndroid.ts +603 -0
  25. package/src/commands/appDesktop.ts +339 -0
  26. package/src/commands/appDesktopMac.ts +470 -0
  27. package/src/commands/appIcon.ts +187 -0
  28. package/src/commands/appShared.ts +542 -0
  29. package/src/commands/appTemplates.ts +907 -0
  30. package/src/commands/appTemplatesAndroid.ts +599 -0
  31. package/src/commands/appTemplatesGradlew.ts +9 -0
  32. package/src/commands/assets.ts +28 -0
  33. package/src/commands/clone.ts +60 -0
  34. package/src/commands/compile.ts +253 -0
  35. package/src/commands/create.ts +59 -0
  36. package/src/commands/design.ts +863 -0
  37. package/src/commands/designTemplates.ts +11 -0
  38. package/src/commands/desktop.ts +127 -0
  39. package/src/commands/dev.ts +236 -0
  40. package/src/commands/diff.ts +59 -0
  41. package/src/commands/init.ts +191 -0
  42. package/src/commands/install.ts +198 -0
  43. package/src/commands/lightmap.ts +290 -0
  44. package/src/commands/link.ts +147 -0
  45. package/src/commands/login.ts +24 -0
  46. package/src/commands/navmesh.ts +225 -0
  47. package/src/commands/pn.ts +305 -0
  48. package/src/commands/projectTemplates.ts +130 -0
  49. package/src/commands/pull.ts +109 -0
  50. package/src/commands/push.ts +173 -0
  51. package/src/commands/render.ts +416 -0
  52. package/src/commands/scene.ts +148 -0
  53. package/src/commands/shaders.ts +157 -0
  54. package/src/commands/status.ts +29 -0
  55. package/src/commands/test.ts +371 -0
  56. package/src/commands/thumbs.ts +178 -0
  57. package/src/commands/types.ts +87 -0
  58. package/src/commands/update.ts +190 -0
  59. package/src/compile/assetIcons.ts +214 -0
  60. package/src/compile/collect.ts +46 -0
  61. package/src/compile/collectLocal.ts +37 -0
  62. package/src/compile/designCompile.ts +104 -0
  63. package/src/compile/fonts.ts +156 -0
  64. package/src/compile/headlessBundle.ts +129 -0
  65. package/src/compile/nativeStack.ts +21 -0
  66. package/src/compile/projectCompile.ts +109 -0
  67. package/src/compile/sceneCompile.ts +113 -0
  68. package/src/compile/screenEntry.ts +127 -0
  69. package/src/compile/shaders.ts +245 -0
  70. package/src/config.ts +42 -0
  71. package/src/designMeta.ts +35 -0
  72. package/src/desktopRenderer.ts +545 -0
  73. package/src/desktopScript.ts +276 -0
  74. package/src/dev/clientTemplates.ts +160 -0
  75. package/src/dev/devServer.ts +333 -0
  76. package/src/dev/webRunner.ts +22 -0
  77. package/src/dev/wsServer.ts +144 -0
  78. package/src/distRoot.ts +20 -0
  79. package/src/ignore.ts +163 -0
  80. package/src/index.ts +494 -0
  81. package/src/lecodes-3d-editor.d.ts +41 -0
  82. package/src/lecodes-assets.d.ts +7 -0
  83. package/src/lecodes-design.d.ts +191 -0
  84. package/src/lecodes-renderer.d.ts +131 -0
  85. package/src/localFiles.ts +144 -0
  86. package/src/manifest.ts +46 -0
  87. package/src/matcTool.ts +137 -0
  88. package/src/peers.ts +40 -0
  89. package/src/project.ts +61 -0
  90. package/src/projectEnv.ts +94 -0
  91. package/src/qrcode-terminal.d.ts +9 -0
  92. package/src/releases.ts +125 -0
  93. package/src/serverDiff.ts +78 -0
  94. package/src/textDiff.ts +103 -0
  95. package/src/types.ts +0 -0
  96. package/src/util.ts +146 -0
  97. package/runtime/sdk/animate/animate.ts +0 -238
  98. package/runtime/sdk/animate/bezier.ts +0 -138
  99. package/runtime/sdk/animate/easings.ts +0 -126
  100. package/runtime/sdk/canvas/Canvas.ts +0 -305
  101. package/runtime/sdk/compile/aspectMacro.ts +0 -86
  102. package/runtime/sdk/compile/assetIconMacro.ts +0 -384
  103. package/runtime/sdk/compile/assetMacro.ts +0 -146
  104. package/runtime/sdk/compile/assetName.ts +0 -50
  105. package/runtime/sdk/compile/bundler.ts +0 -287
  106. package/runtime/sdk/compile/compileProject.ts +0 -129
  107. package/runtime/sdk/compile/detectEntry.ts +0 -128
  108. package/runtime/sdk/compile/fontMacro.ts +0 -459
  109. package/runtime/sdk/compile/fontRegistry.ts +0 -78
  110. package/runtime/sdk/compile/header.ts +0 -67
  111. package/runtime/sdk/compile/index.ts +0 -100
  112. package/runtime/sdk/compile/libraryImports.ts +0 -52
  113. package/runtime/sdk/compile/liteMaterial.ts +0 -247
  114. package/runtime/sdk/compile/sceneEditor.ts +0 -88
  115. package/runtime/sdk/compile/serverSplit.ts +0 -233
  116. package/runtime/sdk/compile/serverTypes.ts +0 -227
  117. package/runtime/sdk/compile/sfnt.ts +0 -98
  118. package/runtime/sdk/compile/shaderSchema.ts +0 -202
  119. package/runtime/sdk/compile/shaderTargets.ts +0 -81
  120. package/runtime/sdk/compile/sourcemap.ts +0 -25
  121. package/runtime/sdk/core/Aspect.ts +0 -568
  122. package/runtime/sdk/core/InspectorUI.ts +0 -212
  123. package/runtime/sdk/core/StateMachine.ts +0 -308
  124. package/runtime/sdk/core/color.ts +0 -66
  125. package/runtime/sdk/core/compWrite.ts +0 -42
  126. package/runtime/sdk/core/events.ts +0 -38
  127. package/runtime/sdk/core/fields.ts +0 -120
  128. package/runtime/sdk/core/registry.ts +0 -23
  129. package/runtime/sdk/core/signals.ts +0 -277
  130. package/runtime/sdk/core/time.ts +0 -84
  131. package/runtime/sdk/g2/Camera2D.ts +0 -40
  132. package/runtime/sdk/g2/CharacterController2D.ts +0 -276
  133. package/runtime/sdk/g2/Node2D.ts +0 -267
  134. package/runtime/sdk/g2/OneWay2D.ts +0 -66
  135. package/runtime/sdk/g2/Physics2D.ts +0 -346
  136. package/runtime/sdk/g2/Scene2D.ts +0 -209
  137. package/runtime/sdk/g2/Shape2D.ts +0 -259
  138. package/runtime/sdk/g2/Sprite.ts +0 -89
  139. package/runtime/sdk/g2/SpriteAnimation.ts +0 -171
  140. package/runtime/sdk/g2/SpriteSheet.ts +0 -166
  141. package/runtime/sdk/g2/Texture2D.ts +0 -47
  142. package/runtime/sdk/g2/Tilemap.ts +0 -41
  143. package/runtime/sdk/g2/Tileset.ts +0 -71
  144. package/runtime/sdk/g2/Trigger2D.ts +0 -77
  145. package/runtime/sdk/g2/autotile.ts +0 -433
  146. package/runtime/sdk/g2/cells.ts +0 -91
  147. package/runtime/sdk/g2/defineScene2d.ts +0 -381
  148. package/runtime/sdk/g2/groups2d.ts +0 -106
  149. package/runtime/sdk/g2/loop.ts +0 -50
  150. package/runtime/sdk/g2/scenarios2d.ts +0 -69
  151. package/runtime/sdk/g2/touch.ts +0 -83
  152. package/runtime/sdk/gl/Camera.ts +0 -160
  153. package/runtime/sdk/gl/CameraPlace.ts +0 -52
  154. package/runtime/sdk/gl/CharacterController.ts +0 -238
  155. package/runtime/sdk/gl/Gearbox.ts +0 -212
  156. package/runtime/sdk/gl/Geometry.ts +0 -279
  157. package/runtime/sdk/gl/IK.ts +0 -193
  158. package/runtime/sdk/gl/InstancedMesh.ts +0 -132
  159. package/runtime/sdk/gl/Light.ts +0 -107
  160. package/runtime/sdk/gl/Lightmap.ts +0 -256
  161. package/runtime/sdk/gl/Material.ts +0 -260
  162. package/runtime/sdk/gl/Mesh.ts +0 -83
  163. package/runtime/sdk/gl/Model.ts +0 -124
  164. package/runtime/sdk/gl/NavAgent.ts +0 -337
  165. package/runtime/sdk/gl/NavMesh.ts +0 -397
  166. package/runtime/sdk/gl/Node.ts +0 -350
  167. package/runtime/sdk/gl/Noise.ts +0 -30
  168. package/runtime/sdk/gl/Particles.ts +0 -676
  169. package/runtime/sdk/gl/Physics.ts +0 -222
  170. package/runtime/sdk/gl/Plane.ts +0 -53
  171. package/runtime/sdk/gl/Ragdoll.ts +0 -270
  172. package/runtime/sdk/gl/Ray.ts +0 -16
  173. package/runtime/sdk/gl/Scene.ts +0 -486
  174. package/runtime/sdk/gl/Shape.ts +0 -409
  175. package/runtime/sdk/gl/Terrain.ts +0 -1092
  176. package/runtime/sdk/gl/Texture.ts +0 -63
  177. package/runtime/sdk/gl/Trigger.ts +0 -45
  178. package/runtime/sdk/gl/Vehicle.ts +0 -473
  179. package/runtime/sdk/gl/Wheel.ts +0 -240
  180. package/runtime/sdk/gl/animation/AnimationClip.ts +0 -204
  181. package/runtime/sdk/gl/animation/Animator.ts +0 -120
  182. package/runtime/sdk/gl/animation/Layer.ts +0 -31
  183. package/runtime/sdk/gl/animation/Loop.ts +0 -30
  184. package/runtime/sdk/gl/animation/Playback.ts +0 -44
  185. package/runtime/sdk/gl/animation/core.ts +0 -336
  186. package/runtime/sdk/gl/controls.ts +0 -95
  187. package/runtime/sdk/gl/physicsEvents.ts +0 -20
  188. package/runtime/sdk/gl/scenarios.ts +0 -291
  189. package/runtime/sdk/gl/state.ts +0 -6
  190. package/runtime/sdk/gl/terrainMesh.ts +0 -219
  191. package/runtime/sdk/gl/touch.ts +0 -68
  192. package/runtime/sdk/inject.ts +0 -197
  193. package/runtime/sdk/math/Mathf.ts +0 -118
  194. package/runtime/sdk/math/mat4.ts +0 -278
  195. package/runtime/sdk/math/quat.ts +0 -232
  196. package/runtime/sdk/math/vec.ts +0 -255
  197. package/runtime/sdk/net/codec.ts +0 -119
  198. package/runtime/sdk/net/core.ts +0 -384
  199. package/runtime/sdk/net/index.ts +0 -181
  200. package/runtime/sdk/net/replication.ts +0 -622
  201. package/runtime/sdk/plugins/camera.ts +0 -81
  202. package/runtime/sdk/plugins/geolocation.ts +0 -123
  203. package/runtime/sdk/plugins/oauth.ts +0 -61
  204. package/runtime/sdk/plugins/permission.ts +0 -7
  205. package/runtime/sdk/plugins/push.ts +0 -132
  206. package/runtime/sdk/plugins/qr.ts +0 -73
  207. package/runtime/sdk/plugins/service.ts +0 -47
  208. package/runtime/sdk/runtime/app.ts +0 -101
  209. package/runtime/sdk/runtime/appEvents.ts +0 -54
  210. package/runtime/sdk/runtime/channel.ts +0 -61
  211. package/runtime/sdk/runtime/clipboard.ts +0 -20
  212. package/runtime/sdk/runtime/datetime.ts +0 -329
  213. package/runtime/sdk/runtime/device.ts +0 -293
  214. package/runtime/sdk/runtime/fetch.ts +0 -77
  215. package/runtime/sdk/runtime/files.ts +0 -23
  216. package/runtime/sdk/runtime/input.ts +0 -175
  217. package/runtime/sdk/runtime/media.ts +0 -111
  218. package/runtime/sdk/runtime/misc.ts +0 -16
  219. package/runtime/sdk/runtime/net.ts +0 -36
  220. package/runtime/sdk/runtime/rpc.ts +0 -218
  221. package/runtime/sdk/runtime/service.ts +0 -83
  222. package/runtime/sdk/runtime/share.ts +0 -9
  223. package/runtime/sdk/runtime/storage.ts +0 -13
  224. package/runtime/sdk/runtime/touch.ts +0 -76
  225. package/runtime/sdk/scene/defineScene.ts +0 -1353
  226. package/runtime/sdk/scene/editorPlugins.ts +0 -110
  227. package/runtime/sdk/scene/gizmos.ts +0 -148
  228. package/runtime/sdk/scene/grammar.ts +0 -120
  229. package/runtime/sdk/scene/material.ts +0 -188
  230. package/runtime/sdk/server/auth/appConfig.ts +0 -12
  231. package/runtime/sdk/server/auth/global.ts +0 -80
  232. package/runtime/sdk/server/auth/host.ts +0 -318
  233. package/runtime/sdk/server/auth/models.ts +0 -83
  234. package/runtime/sdk/server/auth/types.ts +0 -50
  235. package/runtime/sdk/server/channel.ts +0 -56
  236. package/runtime/sdk/server/context.ts +0 -36
  237. package/runtime/sdk/server/db/defineDb.ts +0 -237
  238. package/runtime/sdk/server/db/fields.ts +0 -132
  239. package/runtime/sdk/server/db/httpTransport.ts +0 -93
  240. package/runtime/sdk/server/db/index.ts +0 -7
  241. package/runtime/sdk/server/db/marci/query.ts +0 -412
  242. package/runtime/sdk/server/db/types.ts +0 -202
  243. package/runtime/sdk/server/errors.ts +0 -12
  244. package/runtime/sdk/server/host.ts +0 -74
  245. package/runtime/sdk/server/inject.ts +0 -13
  246. package/runtime/sdk/server/runtime.ts +0 -133
  247. package/runtime/sdk/server/validate.ts +0 -87
  248. package/runtime/sdk/ui/NativeView.ts +0 -142
  249. package/runtime/sdk/ui/UI.ts +0 -39
  250. package/runtime/sdk/ui/UIBottomSheet.ts +0 -139
  251. package/runtime/sdk/ui/UIButton.ts +0 -101
  252. package/runtime/sdk/ui/UIContainer.ts +0 -60
  253. package/runtime/sdk/ui/UIImage.ts +0 -83
  254. package/runtime/sdk/ui/UIInput.ts +0 -185
  255. package/runtime/sdk/ui/UIModal.ts +0 -139
  256. package/runtime/sdk/ui/UINode.ts +0 -830
  257. package/runtime/sdk/ui/UIPager.ts +0 -362
  258. package/runtime/sdk/ui/UIPopover.ts +0 -100
  259. package/runtime/sdk/ui/UIScreen.ts +0 -123
  260. package/runtime/sdk/ui/UIScrollable.ts +0 -87
  261. package/runtime/sdk/ui/UISpacer.ts +0 -14
  262. package/runtime/sdk/ui/UITabs.ts +0 -236
  263. package/runtime/sdk/ui/UIText.ts +0 -51
  264. package/runtime/sdk/ui/UIVideo.ts +0 -88
  265. package/runtime/sdk/ui/UIVirtualizedList.ts +0 -241
  266. package/runtime/sdk/ui/UIWidget.ts +0 -127
  267. package/runtime/sdk/ui/fonts.ts +0 -13
  268. package/runtime/sdk/ui/presentable.ts +0 -117
  269. package/runtime/sdk/ui/router.ts +0 -132
  270. package/runtime/sdk/ui/theme.ts +0 -84
@@ -0,0 +1,109 @@
1
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
2
+ import { dirname, join } from "node:path"
3
+ import { downloadBlob, getCommits, getProject, type Project, type ProjectAsset } from "../api"
4
+ import { loadConfig, requireApiUrl, requireToken } from "../config"
5
+ import { findProjectRoot, readManifest, writeManifest, type FileKind } from "../manifest"
6
+ import { buildPaths, materializeAssets } from "../project"
7
+ import { materializeTypes, writeTsconfig } from "../types"
8
+ import { diffLocal, hasChanges, scanProject } from "../localFiles"
9
+ import { CliError, c, info, success, progress, endProgress, flagBool, sha256, toKey, type Args } from "../util"
10
+
11
+ /* `lecodes pull` — overwrite local files with the project's current remote state. Refuses to
12
+ * clobber uncommitted local edits unless --force. Given file/folder paths, pulls only those (a
13
+ * targeted "take the server's version of this file" for manual conflict resolution). */
14
+ export const pull = async (args: Args) => {
15
+ const root = findProjectRoot(process.cwd())
16
+ const manifest = readManifest(root)
17
+ const config = loadConfig()
18
+ const apiUrl = requireApiUrl(config)
19
+ const token = requireToken(config)
20
+
21
+ // Targeted pull of specific paths — no dirty guard (the point is to merge into local edits) and
22
+ // baseHeadId is left as-is (you still owe a full pull/--force before a clean push).
23
+ if (args._.length > 0) {
24
+ const project = await getProject(apiUrl, token, manifest.uuid)
25
+ await pullPaths(root, manifest, project, args._.map(toKey), apiUrl, token)
26
+ return
27
+ }
28
+
29
+ if (!flagBool(args, "force") && hasChanges(diffLocal(manifest, scanProject(root, manifest)))) {
30
+ throw new CliError("You have local changes. Push them first, or run `lecodes pull --force` to discard them.")
31
+ }
32
+
33
+ info(c.dim("Fetching project…"))
34
+ const project = await getProject(apiUrl, token, manifest.uuid)
35
+
36
+ // Remove tracked files that no longer exist remotely (untracked local files are left alone).
37
+ const remotePaths = new Set([...buildPaths(project.assets).entries()]
38
+ .filter(([id]) => project.assets.find(a => a.id === id)?.type !== "folder")
39
+ .map(([, path]) => path))
40
+ for (const path of Object.keys(manifest.files)) {
41
+ if (!remotePaths.has(path)) {
42
+ const abs = join(root, ...path.replace(/^\//, "").split("/"))
43
+ if (existsSync(abs)) rmSync(abs)
44
+ }
45
+ }
46
+
47
+ const files = await materializeAssets(root, project, apiUrl, token, progress)
48
+ endProgress()
49
+ if (!flagBool(args, "no-types")) {
50
+ await materializeTypes(root, apiUrl)
51
+ writeTsconfig(root)
52
+ }
53
+
54
+ const commits = await getCommits(apiUrl, token, manifest.uuid)
55
+ writeManifest(root, { ...manifest, name: project.name, baseHeadId: commits.headId, files })
56
+
57
+ success(`Pulled latest into ${c.bold(manifest.name)}.`)
58
+ }
59
+
60
+ const kindOf = (type: ProjectAsset["type"]): FileKind => (type === "file" ? "resource" : type as FileKind)
61
+
62
+ /** Pull only the given paths (a file, or every file under a folder) from the server, in place. */
63
+ const pullPaths = async (
64
+ root: string, manifest: ReturnType<typeof readManifest>, project: Project,
65
+ paths: string[], apiUrl: string, token: string,
66
+ ) => {
67
+ const pathById = buildPaths(project.assets)
68
+ const byPath = new Map<string, ProjectAsset>()
69
+ for (const asset of project.assets) byPath.set(pathById.get(asset.id)!, asset)
70
+
71
+ // Expand any folder argument to the files it contains.
72
+ const targets: ProjectAsset[] = []
73
+ for (const path of paths) {
74
+ const asset = byPath.get(path)
75
+ if (!asset) throw new CliError(`Not found on the server: ${path}`)
76
+ if (asset.type === "folder") {
77
+ const prefix = path + "/"
78
+ const under = project.assets.filter(a => a.type !== "folder" && pathById.get(a.id)!.startsWith(prefix))
79
+ if (under.length === 0) throw new CliError(`Folder is empty on the server: ${path}`)
80
+ targets.push(...under)
81
+ } else {
82
+ targets.push(asset)
83
+ }
84
+ }
85
+
86
+ const files = { ...manifest.files }
87
+ for (const asset of targets) {
88
+ const key = pathById.get(asset.id)!
89
+ const abs = join(root, ...key.replace(/^\//, "").split("/"))
90
+ mkdirSync(dirname(abs), { recursive: true })
91
+ let sha: string
92
+ if (asset.type === "file") {
93
+ if (!asset.file?.src) continue
94
+ const bytes = await downloadBlob(apiUrl, asset.file.src)
95
+ writeFileSync(abs, bytes)
96
+ sha = sha256(bytes)
97
+ } else {
98
+ const text = asset.text ?? ""
99
+ writeFileSync(abs, text)
100
+ sha = sha256(text)
101
+ }
102
+ files[key] = { id: asset.id, type: kindOf(asset.type), sha }
103
+ info(c.green(` pulled ${key}`))
104
+ }
105
+
106
+ writeManifest(root, { ...manifest, files })
107
+ success(`Pulled ${targets.length} file${targets.length === 1 ? "" : "s"} from the server.`)
108
+ info(c.dim("Local edits to other files are untouched. Run `lecodes diff` to review, then `lecodes push --force`."))
109
+ }
@@ -0,0 +1,173 @@
1
+ import { basename } from "node:path"
2
+ import { readFileSync } from "node:fs"
3
+ import { getCommits, getProject, publishProject, sync, uploadBlob, type Project, type SyncFileInput, type SyncFolderInput } from "../api"
4
+ import { loadConfig, requireApiUrl, requireToken } from "../config"
5
+ import { findProjectRoot, readManifest, writeManifest, type FileKind, type Manifest, type ManifestFile } from "../manifest"
6
+ import { classifyKind, diffLocal, hasChanges, isTextKind, scanProject } from "../localFiles"
7
+ import { computeServerDiff } from "../serverDiff"
8
+ import { buildPaths } from "../project"
9
+ import { CliError, c, confirm, flagBool, flagStr, info, log, prompt, success, warn, type Args } from "../util"
10
+
11
+ /*
12
+ * `lecodes push -m "message"` — diff the working tree against the manifest, upload changed binaries,
13
+ * then POST /sync (atomic set-tree + one checkpoint). Identity is carried via assetId so renames /
14
+ * moves keep their history; unchanged binaries are referenced by id and never re-uploaded.
15
+ *
16
+ * With --publish the push also deploys the project (compile → live bundle) in the same /sync call,
17
+ * so the changes go live atomically. On a clean tree, --publish just deploys the current state.
18
+ *
19
+ * It also carries the design board's queued comment writes — replies and resolutions Claude or the
20
+ * developer wrote locally. They go AFTER the design, deliberately: a reply saying "fixed" that
21
+ * arrives before the fix is a lie the reviewer has no way to detect. That ordering is why nothing
22
+ * else in the toolchain sends a comment upward.
23
+ */
24
+ export const push = async (args: Args) => {
25
+ const root = findProjectRoot(process.cwd())
26
+ const manifest = readManifest(root)
27
+ const config = loadConfig()
28
+ const apiUrl = requireApiUrl(config)
29
+ const token = requireToken(config)
30
+
31
+ const publish = flagBool(args, "publish")
32
+ const scan = scanProject(root, manifest)
33
+ const diff = diffLocal(manifest, scan)
34
+ if (!hasChanges(diff)) {
35
+ if (publish) {
36
+ await publishProject(apiUrl, token, manifest.uuid)
37
+ success("No file changes — published the current project state.")
38
+ } else {
39
+ info(c.dim("Working tree clean — nothing to push."))
40
+ }
41
+ // Still flush: a queued answer must not be stuck behind "no files changed", which is exactly the
42
+ // state you're in after resolving a comment about something you already pushed.
43
+ await flushComments(args)
44
+ return
45
+ }
46
+
47
+ const force = flagBool(args, "force")
48
+ // Concurrency guard: a push rewrites the server's WHOLE tree (set-tree), so anything that changed
49
+ // on the server since we cloned/pulled would be clobbered. Two kinds of drift to catch:
50
+ if (!force && manifest.baseHeadId !== null) {
51
+ const commits = await getCommits(apiUrl, token, manifest.uuid)
52
+ // (1) A new checkpoint was frozen on the server — that advances the head id.
53
+ if (commits.headId !== manifest.baseHeadId) {
54
+ throw new CliError("The project changed on the server since your last sync. Run `lecodes diff` to compare, `lecodes pull` to take the server's version, or `lecodes push --force` to overwrite.")
55
+ }
56
+ // (2) Uncommitted web-editor edits live in the head IN PLACE — same head id, just `dirty`. They
57
+ // still get rewritten by the set-tree push, so if the live head's file set has drifted from our
58
+ // manifest (e.g. a resource was added in the editor) a blind push would silently DELETE it. Bail.
59
+ if (commits.dirty) {
60
+ const drift = serverPathDrift(await getProject(apiUrl, token, manifest.uuid), manifest)
61
+ if (drift) {
62
+ throw new CliError(`The project has uncommitted changes in the web editor your checkout doesn't have (${drift}). Run \`lecodes pull\` to take them, or \`lecodes push --force\` to overwrite the server.`)
63
+ }
64
+ }
65
+ }
66
+
67
+ // With --force we may clobber server-side edits. Show exactly what changes on the server and
68
+ // confirm (interactive only; skip with --yes or when piped).
69
+ if (force) {
70
+ const project = await getProject(apiUrl, token, manifest.uuid)
71
+ const serverDiff = computeServerDiff(project, scan)
72
+ if (serverDiff.length) {
73
+ info(c.yellow(`--force will overwrite the server. ${serverDiff.length} file${serverDiff.length === 1 ? "" : "s"} change:`))
74
+ const label: Record<string, string> = { added: c.green(" create "), removed: c.red(" delete "), modified: c.yellow(" update ") }
75
+ for (const e of serverDiff) log(label[e.status] + e.path + (e.binary ? c.dim(" (binary)") : ""))
76
+ if (process.stdin.isTTY && !flagBool(args, "y", "yes")) {
77
+ if (!(await confirm("Overwrite the server with your local copy?", false))) { info(c.dim("Aborted.")); return }
78
+ }
79
+ }
80
+ }
81
+
82
+ const message = (flagStr(args, "m", "message") ?? await prompt("Checkpoint message")) || "Update from lecodes CLI"
83
+
84
+ // The identity source for a path: the same path in the manifest, else its move origin.
85
+ const sourceFor = (path: string): ManifestFile | undefined =>
86
+ manifest.files[path] ?? (diff.moves.has(path) ? manifest.files[diff.moves.get(path)!] : undefined)
87
+
88
+ const files: SyncFileInput[] = []
89
+ let uploaded = 0
90
+ for (const file of scan.files) {
91
+ const source = sourceFor(file.path)
92
+ const kind: FileKind = source?.type ?? classifyKind(basename(file.path))
93
+ const entry: SyncFileInput = { path: file.path, type: kind }
94
+ if (source) entry.assetId = source.id
95
+
96
+ if (isTextKind(kind)) {
97
+ entry.text = readFileSync(file.absPath, "utf8")
98
+ } else if (!source || source.sha !== file.sha) {
99
+ // New or changed binary — upload it and reference the fresh blob.
100
+ const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename(file.path), readFileSync(file.absPath))
101
+ entry.blobFileId = blob.id
102
+ uploaded++
103
+ }
104
+ // Else: unchanged binary — assetId alone tells the server to keep the existing blob.
105
+ files.push(entry)
106
+ }
107
+
108
+ const folders: SyncFolderInput[] = scan.dirs.map(path => ({ path }))
109
+
110
+ if (uploaded > 0) info(c.dim(`Uploaded ${uploaded} binary file${uploaded === 1 ? "" : "s"}.`))
111
+ const res = await sync(apiUrl, token, manifest.uuid, { message, files, folders, publish })
112
+ if (!res.created) {
113
+ info(c.dim("Server reported no changes."))
114
+ if (publish) reportPublish(res)
115
+ return
116
+ }
117
+
118
+ // Rebuild the manifest from the authoritative server tree + local hashes.
119
+ const shaByPath = new Map(scan.files.map(f => [f.path, f.sha]))
120
+ const newFiles: Record<string, ManifestFile> = {}
121
+ for (const node of res.nodes) {
122
+ newFiles[node.path] = { id: node.id, type: node.type as FileKind, sha: shaByPath.get(node.path) ?? "" }
123
+ }
124
+ writeManifest(root, { ...manifest, baseHeadId: res.headId, files: newFiles })
125
+
126
+ const counts = [
127
+ diff.added.length - diff.moves.size && `${diff.added.length - diff.moves.size} added`,
128
+ diff.modified.length && `${diff.modified.length} modified`,
129
+ diff.moves.size && `${diff.moves.size} moved`,
130
+ diff.deleted.length - diff.moves.size && `${diff.deleted.length - diff.moves.size} deleted`,
131
+ ].filter(Boolean).join(", ")
132
+ success(`Pushed checkpoint${res.seq ? ` #${res.seq}` : ""}${counts ? ` (${counts})` : ""}: ${message}`)
133
+ if (publish) reportPublish(res)
134
+ await flushComments(args)
135
+ }
136
+
137
+ /** Send the design board's queued comment writes, now that the design they describe is up. Silent
138
+ * when there's no design folder, no queue, or no login — this is a rider on the push, not a step
139
+ * the user asked for. */
140
+ const flushComments = async (args: Args) => {
141
+ const { flushDesignComments } = await import("./design")
142
+ const result = await flushDesignComments(args)
143
+ if (!result) return
144
+ if (result.threads) {
145
+ success(`Sent ${result.sent} comment update${result.sent === 1 ? "" : "s"} across ${result.threads} thread${result.threads === 1 ? "" : "s"}.`)
146
+ }
147
+ for (const failure of result.failures) warn(`Comment not sent — ${failure} (still queued; the next push retries it).`)
148
+ }
149
+
150
+ /** How the server's current file set differs from what we last synced (the manifest), or null when
151
+ * it matches. Catches files added/removed out-of-band (e.g. in the web editor) that a set-tree push
152
+ * would otherwise clobber — see the concurrency guard above. Compares paths only (no false positives
153
+ * for CLI-only flows, where the manifest is rebuilt from the server tree on every clone/pull/push). */
154
+ const serverPathDrift = (project: Project, manifest: Manifest): string | null => {
155
+ const pathById = buildPaths(project.assets)
156
+ const serverPaths = new Set(project.assets.filter(a => a.type !== "folder").map(a => pathById.get(a.id)!))
157
+ const manifestPaths = new Set(Object.keys(manifest.files))
158
+ const added = [...serverPaths].filter(p => !manifestPaths.has(p))
159
+ const removed = [...manifestPaths].filter(p => !serverPaths.has(p))
160
+ const describe = (label: string, paths: string[]) =>
161
+ `${paths.length} ${label} on the server: ${paths.slice(0, 3).join(", ")}${paths.length > 3 ? ", …" : ""}`
162
+ const parts = [
163
+ added.length ? describe("added", added) : "",
164
+ removed.length ? describe("removed", removed) : "",
165
+ ].filter(Boolean)
166
+ return parts.length ? parts.join("; ") : null
167
+ }
168
+
169
+ /** Report the outcome of a --publish deploy bundled with the push (best-effort on the server). */
170
+ const reportPublish = (res: { published?: boolean, publishError?: string }) => {
171
+ if (res.published) success("Published — the project is now live.")
172
+ else warn(`Push landed, but publish failed: ${res.publishError ?? "unknown error"}`)
173
+ }