lecodes-cli 0.20.2 → 2.0.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 (154) hide show
  1. package/README.md +56 -57
  2. package/dist/index.js +11052 -7592
  3. package/package.json +14 -8
  4. package/runtime/materials/decal-relief.mat +12 -1
  5. package/runtime/materials/decal.mat +17 -3
  6. package/runtime/materials/lightmap.mat +117 -138
  7. package/runtime/materials/particles-quad.mat +6 -2
  8. package/runtime/materials/terrain-lightmap.mat +123 -0
  9. package/runtime/materials/terrain.mat +36 -55
  10. package/runtime/sdk-types.json +1 -1
  11. package/runtime/web/VERSION.json +5 -0
  12. package/runtime/web/assets/camera-5XOM8eHl.js +1 -0
  13. package/runtime/web/assets/creator-2d-D9WKYM6h.js +1 -0
  14. package/runtime/web/assets/creator-2d-ukvEmNEp.wasm +0 -0
  15. package/runtime/web/assets/creator-full-BBXzQlhq.js +1 -0
  16. package/runtime/web/assets/creator-full-D2dSDdDj.wasm +0 -0
  17. package/runtime/web/assets/creator-full.js-BYswGsqR.symbols +8124 -0
  18. package/runtime/web/assets/creator-ui-DIc92ion.wasm +0 -0
  19. package/runtime/web/assets/creator-ui-_1Z_AX2M.js +1 -0
  20. package/runtime/web/assets/geolocation-BjTKN9_P.js +1 -0
  21. package/runtime/web/assets/inspect-C7x78PNs.js +2 -0
  22. package/runtime/web/assets/map-CyBxOSV0.js +1 -0
  23. package/runtime/web/assets/map-XzoOFt0E.js +2 -0
  24. package/runtime/web/assets/mapImpl-bxvQ2h1Z.js +823 -0
  25. package/runtime/web/assets/maplibre-gl-worker-B7LXXUkR.js +8 -0
  26. package/runtime/web/assets/materials-uri4FvPE.bin +0 -0
  27. package/runtime/web/assets/materials-world-CQWt8TAm.bin +0 -0
  28. package/runtime/web/assets/qr-scanner-CK_cYoJR.js +1 -0
  29. package/runtime/web/assets/uberarchive-DLH-kBhM.bin +0 -0
  30. package/runtime/web/index.html +36 -0
  31. package/runtime/web/player.js +21 -0
  32. package/runtime/web-page/index.html +41 -0
  33. package/runtime/web-page/page.css +1 -0
  34. package/runtime/web-page/page.js +31 -0
  35. package/src/api.ts +1 -302
  36. package/src/cli/args.ts +37 -0
  37. package/src/cli/command.ts +113 -0
  38. package/src/cli/errors.ts +13 -0
  39. package/src/cli/help.ts +158 -0
  40. package/src/cli/index.ts +7 -0
  41. package/src/cli/output.ts +102 -0
  42. package/src/cli/run.ts +106 -0
  43. package/src/commands/{appAndroid.ts → app/android.ts} +454 -43
  44. package/src/commands/{appDesktop.ts → app/desktop.ts} +64 -33
  45. package/src/commands/{appDesktopMac.ts → app/desktopMac.ts} +34 -27
  46. package/src/commands/{appIcon.ts → app/icon.ts} +11 -8
  47. package/src/commands/{app.ts → app/index.ts} +400 -290
  48. package/src/commands/{appShared.ts → app/shared.ts} +43 -44
  49. package/src/commands/{appTemplatesAndroid.ts → app/templates/android.ts} +72 -39
  50. package/src/commands/{appTemplatesGradlew.ts → app/templates/gradlew.ts} +1 -1
  51. package/src/commands/{appTemplates.ts → app/templates/ios.ts} +2 -2
  52. package/src/commands/assets.ts +23 -21
  53. package/src/commands/clone.ts +48 -51
  54. package/src/commands/compile.ts +76 -89
  55. package/src/commands/create.ts +46 -47
  56. package/src/commands/design/comments.ts +361 -0
  57. package/src/commands/design/context.ts +101 -0
  58. package/src/commands/design/index.ts +391 -0
  59. package/src/commands/design/snapshot.ts +134 -0
  60. package/src/commands/{designTemplates.ts → design/templates.ts} +4 -3
  61. package/src/commands/desktop.ts +94 -82
  62. package/src/commands/dev.ts +260 -182
  63. package/src/commands/diff.ts +39 -51
  64. package/src/commands/index.ts +64 -0
  65. package/src/commands/{init.ts → init/index.ts} +195 -191
  66. package/src/commands/{projectTemplates.ts → init/templates.ts} +64 -21
  67. package/src/commands/install.ts +80 -67
  68. package/src/commands/lightmap.ts +543 -252
  69. package/src/commands/link.ts +87 -94
  70. package/src/commands/login.ts +25 -23
  71. package/src/commands/navmesh.ts +169 -139
  72. package/src/commands/plugin.ts +46 -0
  73. package/src/commands/pn.ts +201 -244
  74. package/src/commands/pull.ts +49 -56
  75. package/src/commands/push.ts +48 -54
  76. package/src/commands/render.ts +176 -111
  77. package/src/commands/scene.ts +63 -55
  78. package/src/commands/serve.ts +58 -0
  79. package/src/commands/{shaders.ts → shaders/index.ts} +61 -42
  80. package/src/commands/{shadersNew.ts → shaders/new.ts} +3 -3
  81. package/src/commands/shared.ts +79 -0
  82. package/src/commands/status.ts +22 -26
  83. package/src/commands/test.ts +94 -63
  84. package/src/commands/thumbs.ts +185 -175
  85. package/src/commands/update/index.ts +226 -0
  86. package/src/commands/{types.ts → update/types.ts} +31 -22
  87. package/src/compile/collect.ts +5 -5
  88. package/src/compile/collectLocal.ts +3 -3
  89. package/src/compile/designCompile.ts +4 -4
  90. package/src/compile/headlessBundle.ts +16 -12
  91. package/src/compile/nativeStack.ts +1 -1
  92. package/src/compile/projectCompile.ts +12 -4
  93. package/src/compile/sceneCompile.ts +14 -26
  94. package/src/compile/screenEntry.ts +123 -127
  95. package/src/compile/shaders.ts +3 -3
  96. package/src/{lecodes-3d-editor.d.ts → declarations/lecodes-3d-editor.d.ts} +4 -0
  97. package/src/{lecodes-assets.d.ts → declarations/lecodes-assets.d.ts} +1 -1
  98. package/src/{lecodes-renderer.d.ts → declarations/lecodes-headless.d.ts} +3 -3
  99. package/src/dev/androidDev.ts +1 -1
  100. package/src/dev/clientTemplates.ts +18 -14
  101. package/src/dev/devServer.ts +78 -41
  102. package/src/dev/iosDev.ts +180 -0
  103. package/src/dev/webRunner.ts +83 -15
  104. package/src/{cmgenTool.ts → hosts/cmgenTool.ts} +2 -2
  105. package/src/{desktopRenderer.ts → hosts/desktopRenderer.ts} +104 -32
  106. package/src/{desktopScript.ts → hosts/desktopScript.ts} +151 -36
  107. package/src/{distRoot.ts → hosts/distRoot.ts} +9 -5
  108. package/src/{matcTool.ts → hosts/matcTool.ts} +2 -2
  109. package/src/{peerInstall.ts → hosts/peerInstall.ts} +7 -7
  110. package/src/{peers.ts → hosts/peers.ts} +2 -2
  111. package/src/{releases.ts → hosts/releases.ts} +1 -1
  112. package/src/index.ts +35 -480
  113. package/src/platform/api.ts +302 -0
  114. package/src/{browserAuth.ts → platform/browserAuth.ts} +1 -1
  115. package/src/{config.ts → platform/config.ts} +1 -1
  116. package/src/{serverDiff.ts → platform/serverDiff.ts} +3 -3
  117. package/src/plugin/contract.ts +438 -0
  118. package/src/plugin/emitKotlin.ts +357 -0
  119. package/src/plugin/emitSdk.ts +259 -0
  120. package/src/plugin/emitSwift.ts +336 -0
  121. package/src/plugin/emitWeb.ts +383 -0
  122. package/src/plugin/generate.ts +157 -0
  123. package/src/plugin/lines.ts +28 -0
  124. package/src/plugin/lower.ts +231 -0
  125. package/src/plugin/manifest.ts +192 -0
  126. package/src/{projectEnv.ts → project/env.ts} +33 -2
  127. package/src/{ignore.ts → project/ignore.ts} +164 -163
  128. package/src/{localFiles.ts → project/localFiles.ts} +1 -1
  129. package/src/{manifest.ts → project/manifest.ts} +1 -1
  130. package/src/{project.ts → project/materialize.ts} +2 -2
  131. package/src/project/paths.ts +20 -0
  132. package/src/{textDiff.ts → project/textDiff.ts} +1 -1
  133. package/src/{types.ts → project/types.ts} +0 -0
  134. package/runtime/materials/lightmap-baked-lite.mat +0 -175
  135. package/runtime/materials/lightmap-baked.mat +0 -176
  136. package/runtime/scene-harness.json +0 -1
  137. package/runtime/web/assets/__vite-browser-external-BIHI7g3E.js +0 -1
  138. package/runtime/web/assets/basis-C64VHDVD.js +0 -1
  139. package/runtime/web/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
  140. package/runtime/web/assets/createViewerLite-Ct_PZdof.js +0 -852
  141. package/runtime/web/assets/draco-BiISTFcR.js +0 -118
  142. package/runtime/web/assets/draco_decoder-DsQ12WqX.wasm +0 -0
  143. package/runtime/web/assets/index-BMt7AnC5.js +0 -2
  144. package/runtime/web/assets/mapViewImpl-B2JcES8l.js +0 -810
  145. package/runtime/web/assets/maplibre-gl-worker-CJfwIrte.js +0 -8
  146. package/runtime/web/assets/worker-Caf-yYEI.js +0 -2
  147. package/runtime/web/embed.html +0 -32
  148. package/runtime/web/embed.js +0 -171
  149. package/src/commands/design.ts +0 -846
  150. package/src/commands/update.ts +0 -211
  151. package/src/util.ts +0 -146
  152. /package/src/{lecodes-design.d.ts → declarations/lecodes-design.d.ts} +0 -0
  153. /package/src/{qrcode-terminal.d.ts → declarations/qrcode-terminal.d.ts} +0 -0
  154. /package/src/{designMeta.ts → project/designMeta.ts} +0 -0
@@ -0,0 +1,64 @@
1
+ import type { Group } from "../cli"
2
+ import app from "./app"
3
+ import assets from "./assets"
4
+ import clone from "./clone"
5
+ import compile from "./compile"
6
+ import create from "./create"
7
+ import design from "./design"
8
+ import desktop from "./desktop"
9
+ import dev from "./dev"
10
+ import diff from "./diff"
11
+ import init from "./init"
12
+ import install from "./install"
13
+ import lightmap from "./lightmap"
14
+ import link from "./link"
15
+ import login from "./login"
16
+ import navmesh from "./navmesh"
17
+ import plugin from "./plugin"
18
+ import pn from "./pn"
19
+ import pull from "./pull"
20
+ import push from "./push"
21
+ import render from "./render"
22
+ import scene from "./scene"
23
+ import serve from "./serve"
24
+ import shaders from "./shaders"
25
+ import status from "./status"
26
+ import test from "./test"
27
+ import thumbs from "./thumbs"
28
+ import update from "./update"
29
+
30
+ /*
31
+ * THE command list. Adding a command = one file with a `defineCommand` default export + one line
32
+ * here, in the group it belongs to (CONTRIBUTING.md). Order within a group is the help's order.
33
+ */
34
+ export const commandGroups = (opts: { install: boolean }): Group[] => [
35
+ { title: "Project", commands: [init, create, clone, link, status, diff, pull, push, login] },
36
+ { title: "Run & test", commands: [
37
+ dev,
38
+ compile,
39
+ render,
40
+ test,
41
+ desktop,
42
+ serve,
43
+ ] },
44
+ { title: "Editors", commands: [
45
+ design,
46
+ scene,
47
+ thumbs,
48
+ ] },
49
+ { title: "Content", commands: [
50
+ assets,
51
+ shaders,
52
+ lightmap,
53
+ navmesh,
54
+ ] },
55
+ { title: "Native apps", commands: [
56
+ app,
57
+ plugin,
58
+ pn,
59
+ ] },
60
+ { title: "Maintenance", commands: [
61
+ update,
62
+ { ...install, hidden: !opts.install },
63
+ ] },
64
+ ]
@@ -1,191 +1,195 @@
1
- import { existsSync, mkdirSync, writeFileSync } from "node:fs"
2
- import { basename, dirname, join, resolve } from "node:path"
3
- import { LECODES_DIR } from "../manifest"
4
- import { materializeTypesLocal, writeTsconfig } from "../types"
5
- import { DEFAULT_IGNORE_FILE, ensureGitignore } from "../ignore"
6
- import { appClaudeMd, CLAUDE_SETTINGS, STARTER_FLOW } from "./projectTemplates"
7
- import { c, flagBool, log, note, success, warnErr, type Args } from "../util"
8
-
9
- /*
10
- * `lecodes init [dir]` — scaffold a brand-new LOCAL project: no account, no login, no server.
11
- *
12
- * Writes a starter entry, a CLAUDE.md (the headless render/test workflow for AI assistants) with
13
- * a passing starter flow test, the SDK's import-free type surface + a tsconfig (so VS Code gives
14
- * full IntelliSense offline), and a `.lecodesignore`. The result runs immediately with `lecodes dev`
15
- * (hot reload on a device) and compiles with `lecodes compile -o app.js` (a self-contained bundle
16
- * for LeCodes Desktop). It's the offline counterpart to `lecodes create`, which makes a project on
17
- * the server; a local project needs no manifest — `dev`/`compile` compile straight from the tree.
18
- *
19
- * Idempotent: existing files are kept (never clobbered), so it's safe to re-run or run in a folder
20
- * that already has content. Only the generated tsconfig + `.lecodes/types/` are rewritten each run.
21
- */
22
-
23
- const STARTER = `// Welcome to LeCodes — the whole SDK is available as globals, no imports needed.
24
- // Run it live on a device with \`lecodes dev\`, or bundle it with \`lecodes compile -o app.js\`.
25
-
26
- let count = 0
27
-
28
- const counter = UIText({ color: "#9fb0d0", fontSize: 22, marginTop: 18 }, "Taps: 0")
29
-
30
- const button = UIButton(
31
- {
32
- backgroundColor: "#4f7cff",
33
- borderRadius: 14,
34
- paddingVertical: 16,
35
- paddingHorizontal: 40,
36
- marginTop: 24,
37
- alignItems: "center",
38
- justifyContent: "center",
39
- },
40
- [UIText({ color: "#ffffff", fontSize: 22 }, "Tap me")],
41
- )
42
- .style({ name: "tap" }) // names make elements targetable in tests/*.flow.json (see CLAUDE.md)
43
- .onClick(() => {
44
- count++
45
- counter.text = "Taps: " + count
46
- })
47
-
48
- const screen = UIScreen(
49
- { backgroundColor: "#1e2230", alignItems: "center", justifyContent: "center", padding: 40 },
50
- [
51
- UIColumn(
52
- { backgroundColor: "#2b3550", borderRadius: 24, padding: 40, alignItems: "center" },
53
- [UIText({ color: "#ffffff", fontSize: 32 }, "Hello, LeCodes"), button, counter],
54
- ),
55
- ],
56
- ).style({ name: "home" }) // the screen root's name is what tests' \`expect: { screen: … }\` reads
57
-
58
- screen.open()
59
- `
60
-
61
- /** `--desktop`: declare the desktop target up front. Window sizes are logical units (the host
62
- * scales for HiDPI); `dev --desktop` / `desktop run` / `render --desktop` all read this, and a
63
- * desktop-only app.json makes headless render/test default to the window size too. */
64
- const appJsonDesktop = (name: string) => JSON.stringify({
65
- name,
66
- desktop: {
67
- window: { width: 1280, height: 800, minWidth: 640, minHeight: 480 },
68
- },
69
- }, null, 2) + "\n"
70
-
71
- const readmeTemplate = (name: string) => `# ${name}
72
-
73
- A LeCodes app. The whole SDK is available as globals (\`UIScreen\`, \`UIText\`, \`Scene2D\`, …) —
74
- no imports needed. \`main.ts\` is the entry point.
75
-
76
- ## Run it
77
-
78
- \`\`\`
79
- lecodes dev # live on your phone: scan the QR with the LeCodes app, hot reload
80
- lecodes compile -o app.js # bundle to a single self-contained JS file
81
- \`\`\`
82
-
83
- ## Test it (no device needed)
84
-
85
- \`\`\`
86
- lecodes render --png shot.png # headless screenshot of the running app
87
- lecodes test # run tests/*.flow.json — the starter ships with a passing one
88
- \`\`\`
89
-
90
- Scenario format + conventions: see [CLAUDE.md](./CLAUDE.md) (written for AI assistants, useful
91
- for humans too).
92
-
93
- ## Put it on the platform
94
-
95
- This project is local-only. To give it a home on le.codes (web editor, sharing, OTA updates):
96
-
97
- \`\`\`
98
- lecodes login # once per machine
99
- lecodes link # create a project from this folder and connect it
100
- lecodes push -m "initial" # upload it as the first checkpoint (--publish to go live)
101
- \`\`\`
102
-
103
- Already made the project in the web editor? \`lecodes link --project <uuid>\` adopts it — it
104
- shows what your next push would change there before connecting anything.
105
-
106
- ## Go native
107
-
108
- \`\`\`
109
- lecodes app init ios # generate a native Xcode shell in ios/
110
- lecodes app sync # embed the compiled bundle + plugins into it
111
- lecodes app open # open it in Xcode, build & run
112
- \`\`\`
113
-
114
- ## Layout
115
-
116
- - \`main.ts\` — the app's entry point. Add more .ts files freely and import them from
117
- \`main.ts\`; a file named \`main.ts\` is always the entry.
118
- - \`tests/\` — headless flow tests (\`lecodes test\`); failing steps dump the frame to
119
- \`tests/.artifacts/\`
120
- - \`CLAUDE.md\` — guidance for AI assistants (the headless feedback loop + testability conventions)
121
- - \`assets/\` — images/fonts the app references (created on demand)
122
- - \`tsconfig.json\` + \`.lecodes/types/\` — generated IDE types (rewritten by the CLI; don't edit).
123
- They come from the SDK bundled in the CLI; \`lecodes update\` refreshes them from the platform.
124
- - \`.lecodes/\` is gitignored — per-checkout state (those types + \`manifest.json\`, this machine's
125
- sync cursor once linked). After a fresh git clone: \`lecodes update\`, then
126
- \`lecodes link --project <uuid>\` to re-attach.
127
- - \`.lecodesignore\` — files a future \`lecodes push\` would skip
128
-
129
- Full command list: \`lecodes --help\`.
130
- `
131
-
132
- export const init = async (args: Args) => {
133
- const dirArg = (args._[0] ?? ".").replace(/[/\\]+$/, "") || "."
134
- const root = resolve(dirArg)
135
- const name = basename(root)
136
-
137
- mkdirSync(root, { recursive: true })
138
-
139
- let created = 0
140
- const write = (rel: string, content: string) => {
141
- const abs = join(root, rel)
142
- if (existsSync(abs)) { note(`kept ${rel}`); return }
143
- mkdirSync(dirname(abs), { recursive: true })
144
- writeFileSync(abs, content)
145
- log(`${c.green("created")} ${rel}`)
146
- created++
147
- }
148
-
149
- const forDesktop = flagBool(args, "desktop")
150
-
151
- // Starter entry — a root file named main.ts is always the detected entrypoint.
152
- write("main.ts", STARTER)
153
- write("README.md", readmeTemplate(name))
154
- // --desktop: an app.json declaring the desktop target (window config in logical units).
155
- if (forDesktop) write("app.json", appJsonDesktop(name))
156
- // AI-assistant guidance + a passing flow test: the headless loop is discoverable from day one.
157
- write("CLAUDE.md", appClaudeMd(name))
158
- write(".claude/settings.json", CLAUDE_SETTINGS)
159
- write("tests/counter.flow.json", STARTER_FLOW)
160
- // Keep local render/preview outputs (screenshot.png, shots/) out of any future push.
161
- write(".lecodesignore", DEFAULT_IGNORE_FILE)
162
- // .lecodes/ is per-checkout state (generated types + the push cursor) — keep it out of git.
163
- const gi = ensureGitignore(root)
164
- if (gi === "created") { log(`${c.green("created")} .gitignore`); created++ }
165
- else if (gi === "updated") log(`${c.green("updated")} .gitignore ${c.dim("(+ .lecodes/)")}`)
166
- else note("kept .gitignore")
167
-
168
- // IDE types: the SDK's import-free global surface + a tsconfig, so VS Code resolves UIScreen,
169
- // Scene2D, setLoop, … with no server. Regenerated every run to pick up CLI upgrades.
170
- const wroteTypes = materializeTypesLocal(root)
171
- writeTsconfig(root)
172
- log(`${c.green("written")} tsconfig.json${wroteTypes ? ` + ${LECODES_DIR}/types/` : ""} (IDE types)`)
173
- if (!wroteTypes) {
174
- warnErr("Bundled SDK types not found — tsconfig written, but globals may show unresolved until the CLI is rebuilt with vendored types.")
175
- }
176
-
177
- log("")
178
- success(created
179
- ? `Local project ${c.bold(name)} ready${dirArg === "." ? "" : ` in ${c.bold(dirArg)}/`}.`
180
- : `Project ${c.bold(name)} already initialized.`)
181
- const cd = dirArg === "." ? "" : `cd ${dirArg} && `
182
- if (forDesktop) {
183
- log(` ${c.bold(cd + "lecodes dev --desktop")} run it in a desktop window with hot reload`)
184
- log(` ${c.bold(cd + "lecodes test")} run the flow tests headless (desktop-sized)`)
185
- log(` ${c.bold(cd + "lecodes desktop run")} open the compiled app in the desktop host`)
186
- } else {
187
- log(` ${c.bold(cd + "lecodes dev")} run it on a device with hot reload`)
188
- log(` ${c.bold(cd + "lecodes test")} run the flow tests headless (no device)`)
189
- log(` ${c.bold(cd + "lecodes compile -o app.js")} bundle it for LeCodes Desktop`)
190
- }
191
- }
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs"
2
+ import { basename, dirname, join, resolve } from "node:path"
3
+ import { LECODES_DIR } from "../../project/manifest"
4
+ import { materializeTypesLocal, writeTsconfig } from "../../project/types"
5
+ import { DEFAULT_IGNORE_FILE, ensureGitignore } from "../../project/ignore"
6
+ import { appClaudeMd, CLAUDE_SETTINGS, STARTER_FLOW } from "./templates"
7
+ import { bool, c, defineCommand, log, note, success, warnErr } from "../../cli"
8
+
9
+ /*
10
+ * `lecodes init [dir]` — scaffold a brand-new LOCAL project: no account, no login, no server.
11
+ *
12
+ * Writes a starter entry, a CLAUDE.md (the headless render/test workflow for AI assistants) with
13
+ * a passing starter flow test, the SDK's import-free type surface + a tsconfig (so VS Code gives
14
+ * full IntelliSense offline), and a `.lecodesignore`. The result runs immediately with `lecodes dev`
15
+ * (hot reload on a device) and compiles with `lecodes compile -o app.js` (a self-contained bundle
16
+ * for LeCodes Desktop). It's the offline counterpart to `lecodes create`, which makes a project on
17
+ * the server; a local project needs no manifest — `dev`/`compile` compile straight from the tree.
18
+ *
19
+ * Idempotent: existing files are kept (never clobbered), so it's safe to re-run or run in a folder
20
+ * that already has content. Only the generated tsconfig + `.lecodes/types/` are rewritten each run.
21
+ */
22
+
23
+ const STARTER = `// Welcome to LeCodes — the whole SDK is available as globals, no imports needed.
24
+ // Run it live on a device with \`lecodes dev\`, or bundle it with \`lecodes compile -o app.js\`.
25
+
26
+ let count = 0
27
+
28
+ const counter = UIText("Taps: 0").style({ color: "#9fb0d0", fontSize: 22, marginTop: 18 })
29
+
30
+ const button = UIButton(UIText("Tap me").style({ color: "#ffffff", fontSize: 22 }))
31
+ .style({
32
+ backgroundColor: "#4f7cff",
33
+ borderRadius: 14,
34
+ paddingVertical: 16,
35
+ paddingHorizontal: 40,
36
+ marginTop: 24,
37
+ alignItems: "center",
38
+ justifyContent: "center",
39
+ $hovered: { backgroundColor: "#6b91ff" }, // the mouse over it (desktop); $pressed is the press
40
+ })
41
+ .named("tap") // names make elements targetable in tests/*.flow.json (see CLAUDE.md)
42
+ .onClick(() => {
43
+ count++
44
+ counter.text = "Taps: " + count
45
+ })
46
+
47
+ const screen = UIScreen(
48
+ UIColumn(UIText("Hello, LeCodes").style({ color: "#ffffff", fontSize: 32 }), button, counter)
49
+ .style({ backgroundColor: "#2b3550", borderRadius: 24, padding: 40, alignItems: "center" }),
50
+ )
51
+ .style({ backgroundColor: "#1e2230", alignItems: "center", justifyContent: "center", padding: 40 })
52
+ .named("home") // the screen root's name is what tests' \`expect: { screen: … }\` reads
53
+
54
+ screen.open()
55
+ `
56
+
57
+ /** `--desktop`: declare the desktop target up front. Window sizes are logical units (the host
58
+ * scales for HiDPI); `dev --desktop` / `desktop run` / `render --desktop` all read this, and a
59
+ * desktop-only app.json makes headless render/test default to the window size too. */
60
+ const appJsonDesktop = (name: string) => JSON.stringify({
61
+ name,
62
+ desktop: {
63
+ window: { width: 1280, height: 800, minWidth: 640, minHeight: 480 },
64
+ },
65
+ }, null, 2) + "\n"
66
+
67
+ const readmeTemplate = (name: string) => `# ${name}
68
+
69
+ A LeCodes app. The whole SDK is available as globals (\`UIScreen\`, \`UIText\`, \`Scene2D\`, …) —
70
+ no imports needed. \`main.ts\` is the entry point.
71
+
72
+ ## Run it
73
+
74
+ \`\`\`
75
+ lecodes dev # live on your phone: scan the QR with the LeCodes app, hot reload
76
+ lecodes compile -o app.js # bundle to a single self-contained JS file
77
+ \`\`\`
78
+
79
+ ## Test it (no device needed)
80
+
81
+ \`\`\`
82
+ lecodes render --png shot.png # headless screenshot of the running app
83
+ lecodes test # run tests/*.flow.json — the starter ships with a passing one
84
+ \`\`\`
85
+
86
+ Scenario format + conventions: see [CLAUDE.md](./CLAUDE.md) (written for AI assistants, useful
87
+ for humans too).
88
+
89
+ ## Put it on the platform
90
+
91
+ This project is local-only. To give it a home on le.codes (web editor, sharing, OTA updates):
92
+
93
+ \`\`\`
94
+ lecodes login # once per machine
95
+ lecodes link # create a project from this folder and connect it
96
+ lecodes push -m "initial" # upload it as the first checkpoint (--publish to go live)
97
+ \`\`\`
98
+
99
+ Already made the project in the web editor? \`lecodes link --project <uuid>\` adopts it — it
100
+ shows what your next push would change there before connecting anything.
101
+
102
+ ## Go native
103
+
104
+ \`\`\`
105
+ lecodes app init ios # generate a native Xcode shell in ios/
106
+ lecodes app sync # embed the compiled bundle + plugins into it
107
+ lecodes app open # open it in Xcode, build & run
108
+ \`\`\`
109
+
110
+ ## Layout
111
+
112
+ - \`main.ts\` — the app's entry point. Add more .ts files freely and import them from
113
+ \`main.ts\`; a file named \`main.ts\` is always the entry.
114
+ - \`tests/\` — headless flow tests (\`lecodes test\`); failing steps dump the frame to
115
+ \`tests/.artifacts/\`
116
+ - \`CLAUDE.md\` — guidance for AI assistants (the headless feedback loop + testability conventions)
117
+ - \`assets/\` — images/fonts the app references (created on demand)
118
+ - \`tsconfig.json\` + \`.lecodes/types/\` — generated IDE types (rewritten by the CLI; don't edit).
119
+ They come from the SDK bundled in the CLI; \`lecodes update\` refreshes them from the platform.
120
+ - \`.lecodes/\` is gitignored — per-checkout state (those types + \`manifest.json\`, this machine's
121
+ sync cursor once linked). After a fresh git clone: \`lecodes update\`, then
122
+ \`lecodes link --project <uuid>\` to re-attach.
123
+ - \`.lecodesignore\` — files a future \`lecodes push\` would skip
124
+
125
+ Full command list: \`lecodes --help\`.
126
+ `
127
+
128
+ export default defineCommand({
129
+ name: "init",
130
+ summary: "Scaffold a new LOCAL project (no login, no server)",
131
+ usage: "[dir]",
132
+ description: "Writes a starter main.ts, a CLAUDE.md + a passing flow test (the headless render / test loop), the IDE types + tsconfig, and a .lecodesignore. Run it with `lecodes dev` or `lecodes compile`. Idempotent: existing files are kept.",
133
+ flags: { desktop: bool("also write an app.json declaring the desktop target (window size; `dev --desktop`, `desktop run` and headless render / test pick it up)") },
134
+ examples: ["lecodes init myapp", "lecodes init --desktop"],
135
+ run: ({ args, flags }) => init(args[0], flags.desktop),
136
+ })
137
+
138
+ const init = (dir: string | undefined, forDesktop: boolean): void => {
139
+ const dirArg = (dir ?? ".").replace(/[/\\]+$/, "") || "."
140
+ const root = resolve(dirArg)
141
+ const name = basename(root)
142
+
143
+ mkdirSync(root, { recursive: true })
144
+
145
+ let created = 0
146
+ const write = (rel: string, content: string) => {
147
+ const abs = join(root, rel)
148
+ if (existsSync(abs)) { note(`kept ${rel}`); return }
149
+ mkdirSync(dirname(abs), { recursive: true })
150
+ writeFileSync(abs, content)
151
+ log(`${c.green("created")} ${rel}`)
152
+ created++
153
+ }
154
+
155
+ // Starter entry — a root file named main.ts is always the detected entrypoint.
156
+ write("main.ts", STARTER)
157
+ write("README.md", readmeTemplate(name))
158
+ // --desktop: an app.json declaring the desktop target (window config in logical units).
159
+ if (forDesktop) write("app.json", appJsonDesktop(name))
160
+ // AI-assistant guidance + a passing flow test: the headless loop is discoverable from day one.
161
+ write("CLAUDE.md", appClaudeMd(name))
162
+ write(".claude/settings.json", CLAUDE_SETTINGS)
163
+ write("tests/counter.flow.json", STARTER_FLOW)
164
+ // Keep local render/preview outputs (screenshot.png, shots/) out of any future push.
165
+ write(".lecodesignore", DEFAULT_IGNORE_FILE)
166
+ // .lecodes/ is per-checkout state (generated types + the push cursor) — keep it out of git.
167
+ const gi = ensureGitignore(root)
168
+ if (gi === "created") { log(`${c.green("created")} .gitignore`); created++ }
169
+ else if (gi === "updated") log(`${c.green("updated")} .gitignore ${c.dim("(+ .lecodes/)")}`)
170
+ else note("kept .gitignore")
171
+
172
+ // IDE types: the SDK's import-free global surface + a tsconfig, so VS Code resolves UIScreen,
173
+ // Scene2D, setLoop, … with no server. Regenerated every run to pick up CLI upgrades.
174
+ const wroteTypes = materializeTypesLocal(root)
175
+ writeTsconfig(root)
176
+ log(`${c.green("written")} tsconfig.json${wroteTypes ? ` + ${LECODES_DIR}/types/` : ""} (IDE types)`)
177
+ if (!wroteTypes) {
178
+ warnErr("Bundled SDK types not found — tsconfig written, but globals may show unresolved until the CLI is rebuilt with vendored types.")
179
+ }
180
+
181
+ log("")
182
+ success(created
183
+ ? `Local project ${c.bold(name)} ready${dirArg === "." ? "" : ` in ${c.bold(dirArg)}/`}.`
184
+ : `Project ${c.bold(name)} already initialized.`)
185
+ const cd = dirArg === "." ? "" : `cd ${dirArg} && `
186
+ if (forDesktop) {
187
+ log(` ${c.bold(cd + "lecodes dev --desktop")} run it in a desktop window with hot reload`)
188
+ log(` ${c.bold(cd + "lecodes test")} run the flow tests headless (desktop-sized)`)
189
+ log(` ${c.bold(cd + "lecodes desktop run")} open the compiled app in the desktop host`)
190
+ } else {
191
+ log(` ${c.bold(cd + "lecodes dev")} run it on a device with hot reload`)
192
+ log(` ${c.bold(cd + "lecodes test")} run the flow tests headless (no device)`)
193
+ log(` ${c.bold(cd + "lecodes compile -o app.js")} bundle it for LeCodes Desktop`)
194
+ }
195
+ }
@@ -2,7 +2,7 @@
2
2
  * has its own set — designTemplates.ts). CLAUDE.md is the AI-assistant counterpart of README.md:
3
3
  * it teaches the headless feedback loop (render / --script / test) and the conventions that make
4
4
  * an app testable, so an assistant opening the project drives the real app instead of guessing.
5
- * Facts here mirror lecodes-renderer's scenario engine — update both when the format changes. */
5
+ * Facts here mirror lecodes-headless's scenario engine — update both when the format changes. */
6
6
 
7
7
  export const appClaudeMd = (name: string) => `# ${name} — LeCodes app
8
8
 
@@ -30,17 +30,22 @@ lecodes render --desktop --view iso --png iso.png # 3D: inspection camera fitt
30
30
  # scene (iso|top|front|side|back [--fov N]) — or --camera x,y,z
31
31
  # --look-at x,y,z; the app's camera is untouched. In scripts:
32
32
  # { "view": "iso", "screenshot": "iso.png" } then { "view": "off" }
33
- lecodes render --desktop --script <file> # the same scenario on the NATIVE host (3D/Jolt, real
34
- # GPU): coordinates only ({ x, y }), expect.log checked,
35
- # text/node/screen expects skipped; deterministic 16 ms frames
33
+ lecodes render --desktop --script <file> # the same scenario on the NATIVE host (real GPU, the
34
+ # real hit-test): the same steps, targets and expects as
35
+ # headless (only expect.audio / the json output are headless-only)
36
+ lecodes render --sim --script <file> # the WINDOWLESS native host (lecodes-server): the real
37
+ # engines, no GPU, no pixels, no UI hit-test — expect.log /
38
+ # expect.paint only; runs on CI and beside an open desktop window
36
39
  lecodes test # all tests/*.flow.json + design-derived flows; run before "done"
37
- lecodes test --desktop # the file flows on the NATIVE host (3D/Jolt/real GPU; coordinate
38
- # flows only — design flows stay headless)
40
+ lecodes test --desktop # the file flows on the NATIVE host (design flows stay headless)
41
+ lecodes test --sim # the windowless twin: coordinates + expect.log / expect.paint only
39
42
  lecodes dev --android --time 12 --png phone.png # the REAL phone over adb (USB): launches the app on
40
43
  # the dev url via adb reverse, streams device logs as [device:log],
41
44
  # screenshots after 12 s, force-stops, exit 1 on a crash. --serial
42
45
  # <id> with several devices, --apk <file> to install a build first.
43
46
  # Without --time it runs until Ctrl+C (which stops the app).
47
+ lecodes dev --ios --time 12 --png sim.png # the same on a booted iOS simulator or a plugged-in iPhone
48
+ # (Mac + Xcode): --udid <id|name>, --app <LeCodesApp.app> to install.
44
49
  \`\`\`
45
50
 
46
51
  Iterate with \`--script\` scenarios and keep the useful ones as \`tests/<name>.flow.json\` — they
@@ -49,7 +54,8 @@ are the regression suite. A failing step dumps the failing frame (JSON + PNG) un
49
54
 
50
55
  ## Scenario format (tests/*.flow.json and --script files)
51
56
 
52
- One action per step (\`tap\`, \`type\`+\`text\`, \`fill\`, \`scroll\` [+\`by\`], \`wait\`, and for
57
+ One action per step (\`tap\`, \`type\`+\`text\`, \`fill\`, \`scroll\` [+\`by\`], \`wait\`, \`hover\` (the
58
+ mouse over a node: \`$hovered\` + \`onMouseEnter\`; hovering elsewhere ends it), and for
53
59
  games: \`frames\` (advance N fixed 1/60 steps, no sleeping), \`drag\`+\`to\` [+\`ms\`], \`hold\`+\`ms\`,
54
60
  \`key\`+\`ms\` / \`keyDown\` / \`keyUp\` (KeyboardEvent.code: "ArrowRight", "KeyW", "Space" — mouse
55
61
  buttons are keys too: "MouseLeft"/"MouseRight"), \`look: [dx, dy]\` [+\`ms\`] (mouse motion for
@@ -57,12 +63,29 @@ buttons are keys too: "MouseLeft"/"MouseRight"), \`look: [dx, dy]\` [+\`ms\`] (m
57
63
  \`stick: { left: [x, y], right: [x, y], triggers: [l, r] }\` (values persist; \`index\` = pad),
58
64
  \`gamepad: "connect" | "disconnect"\`, pad buttons as keys: \`keyDown: "GamepadSouth"\`), then an
59
65
  optional \`expect\` (\`screen\`, \`node\`/\`notNode\`, \`text\`/\`notText\`,
60
- \`value: { target, equals }\`, \`log\` — a \`console.log\` line contains it), then outputs
61
- (\`screenshot\` [+\`clip\`: a node name or \`{ x, y, width, height }\`], \`json\`). Targets resolve by
62
- \`name\` → exact visible text → substring → input placeholder; a tap/drag on empty space reaches
66
+ \`value: { target, equals }\`, \`log\` — a \`console.log\` line contains it,
67
+ \`paint: { "<name>": { color: "#rrggbbaa", fontSize: 18, present: [...], absent: [...] } }\` — the
68
+ resolved style of a named node, \`audio: { played, playing }\`), then outputs
69
+ (\`screenshot\` [+\`clip\`: a node name or \`{ x, y, width, height }\`], \`json\`, \`dump\` — the
70
+ runtime's tree with every node's box + resolved paint, identical on every host). Targets resolve
71
+ by \`name\` → exact visible text → substring → input placeholder; a tap/drag on empty space reaches
63
72
  the 2D scene (sprite \`click\`/\`touchstart\`, \`ev.track()\`). Envelope keys: \`name\`,
64
73
  \`width\`/\`height\`, \`settle\`, \`fixtures\`, \`steps\`.
65
74
 
75
+ Three hosts run the same file, at a fixed 16 ms clock on all of them:
76
+
77
+ | host | flag | what it is | targets | expects |
78
+ |---|---|---|---|---|
79
+ | headless | (none) | Bun: the runtime + UI renderer + a recording 3D core (no GPU) | names or \`{ x, y }\` | all |
80
+ | sim | \`--sim\` | the windowless native server: real engines, no pixels, no UI | \`{ x, y }\`, straight to the scene | \`log\`, \`paint\` |
81
+ | desktop | \`--desktop\` | the hidden desktop window: pixels, GL materials, the real hit-test | names or \`{ x, y }\` | all but \`audio\` |
82
+
83
+ One flow file, the same verdict on headless and desktop: names resolve the same way (\`name\` →
84
+ exact text → substring → placeholder), every step runs (\`hover\`, \`type\`, \`fill\`, \`scroll\`,
85
+ \`clip\` included) and the semantic expects read the same tree. Only \`expect.audio\` and the
86
+ \`json\` output are the headless renderer's. On the desktop \`type\` taps the input and types into
87
+ it (appends); the sim has no UI, so it takes coordinates and \`expect.log\` / \`expect.paint\` only.
88
+
66
89
  \`\`\`json
67
90
  { "steps": [
68
91
  { "expect": { "screen": "home", "text": "Taps: 0" } },
@@ -72,19 +95,21 @@ the 2D scene (sprite \`click\`/\`touchstart\`, \`ev.track()\`). Envelope keys: \
72
95
  ] }
73
96
  \`\`\`
74
97
 
75
- Game time is deterministic headless: \`setLoop\`, 2D physics/animation and aspect \`update(dt)\`
76
- advance in fixed 1/60 steps driven by \`wait\`/\`frames\`/\`drag\`/\`key\` — never by a wall clock —
77
- so a flow that passes once passes every time. \`Input.key(code)\` reads the keys a flow holds and
78
- \`Input.on('keydown' | 'keyup', e => e.code)\` fires for each \`key\`/\`keyDown\`/\`keyUp\` step
79
- (events land before the next loop tick); \`stick\` values are what \`Input.gamepad(0).axis()\` reads.
80
- Headless draws UI + the 2D scene (sprites, tilemaps, Box2D); 3D is captured as JSON with REAL Jolt
81
- physics (bodies fall, characters collide, triggers fire, ragdolls drop — a character with no ground
82
- under it falls, like on device) — use \`--desktop\` for 3D pixels. \`console.log\` + \`expect.log\` is the assertion
83
- channel for game state that has no UI.
98
+ Game time is deterministic on every host: the Aspect / System phases (\`updateBefore\` / \`update\`),
99
+ 2D physics and animation advance in fixed 1/60 steps driven by \`wait\`/\`frames\`/\`drag\`/\`key\` —
100
+ never by a wall clock — so a flow that passes once passes every time. \`Input.key(code)\` reads the
101
+ keys a flow holds and \`Input.on('keydown' | 'keyup', e => e.code)\` fires for each
102
+ \`key\`/\`keyDown\`/\`keyUp\` step (events land before the next loop tick); \`stick\` values are what
103
+ \`Input.gamepad(0).axis()\` reads. Headless draws UI + the 2D scene (sprites, tilemaps, Box2D); 3D is
104
+ captured as JSON with REAL Jolt physics (bodies fall, characters collide, triggers fire, ragdolls
105
+ drop — a character with no ground under it falls, like on device) — use \`--desktop\` for 3D pixels,
106
+ \`--sim\` for engine behaviour on a box without a GPU. \`console.log\` + \`expect.log\` is the
107
+ assertion channel for game state that has no UI.
84
108
 
85
109
  ## Make the app testable
86
110
 
87
- - Give interactive elements a \`name\`: \`UIButton(...).style({ name: "login" })\`.
111
+ - Give interactive elements a \`name\`: \`UIButton(...).named("login")\` (or \`el.name = "login"\`;
112
+ it is not a style key).
88
113
  - Name every screen's ROOT node with the screen's id — \`expect.screen\` reads it.
89
114
  - If the project has a \`design/\` folder, keep the design's screen ids and activator \`name\`s
90
115
  in the app code: \`lecodes test\` then checks the built app against the designed navigation
@@ -94,6 +119,16 @@ channel for game state that has no UI.
94
119
  match wins). \`lecodes test\` auto-loads it for design-derived flows; a scenario file opts in
95
120
  with \`"fixtures": "fixtures.json"\` in its envelope.
96
121
  - \`fill\` fills every empty input — use it before tapping a disabled-until-filled submit.
122
+ - Visual state is a \`$class\` in the style (\`$pressed\`, \`$hovered\`, \`$focused\`, your own
123
+ \`$selected\` set with \`el.class.selected = true\` or bound with \`.class({ selected: () => … })\`);
124
+ the interaction states read back as \`button.isPressed\` / \`button.isHovered\`.
125
+ A flow asserts the EFFECT through \`expect.paint\` on a named node (the resolved \`color\` /
126
+ \`backgroundColor\` from the dump), never the class name — that works headless and native alike.
127
+ - Hover is the mouse's: \`{ "hover": "login" }\`, a \`frames\` step, then \`expect.paint\` on the node's
128
+ resolved colour (\`$hovered\`) and \`expect.log\` from \`onMouseEnter\`; hover somewhere else to see
129
+ it end. Works headless and on \`--desktop\`; \`mouseTo: [x, y]\` is the same cursor by coordinates.
130
+ \`$focused\` is the desktop renderer's; a keyboard-driven highlight (your own \`$focus\` class moved
131
+ by arrow keys) is assertable everywhere: \`key\` steps, then \`expect.paint\` on the highlighted node.
97
132
 
98
133
  ## 3D scenes
99
134
 
@@ -108,7 +143,15 @@ channel for game state that has no UI.
108
143
  \`lecodes update\` if it's missing, never commit \`.lecodes/\`). For semantics and examples,
109
144
  fetch the docs — they are served as raw markdown for AI tools:
110
145
 
111
- - https://le.codes/llms.txt — the docs map; fetch it first to find the right page
146
+ - https://le.codes/llms.txt — the docs map; fetch it first to find the right page. It opens with the
147
+ CLI: every \`lecodes\` command with what it is for, the agent page (running without a terminal,
148
+ \`--entry\`, login) and the editors that expose MCP tools — \`lecodes design\` (the screen board)
149
+ and \`lecodes scene\` (the 3D scene editor for \`.scene.ts\` files). \`lecodes <command> --help\`
150
+ is the authority on flags.
151
+ **Building a whole thing — a game, enemies, a crowd of characters, a native plugin? llms.txt opens
152
+ with a COOKBOOK: read the matching recipe BEFORE you design the structure.** It has the file
153
+ layout, a skeleton per part, how to verify, and the traps already paid for (start with
154
+ "Project architecture for a game" for any game bigger than one file).
112
155
  - https://le.codes/en/docs/api/<area>/<page>.md — one reference page as raw markdown
113
156
  (e.g. \`ui/styling.md\`, \`2d/scene2d.md\`, \`3d/scene.md\`, \`runtime/storage.md\`)
114
157
  - https://le.codes/en/docs/guide/<page>.md — guide pages (project anatomy, CLI, tracks)