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
@@ -1,16 +1,17 @@
1
1
  import { existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs"
2
2
  import { dirname, resolve } from "node:path"
3
3
  import { compileHeadlessBundle } from "../compile/headlessBundle"
4
- import { desktopDefaultViewport, desktopPluginLibraries, resolveDesktopRenderer } from "./appShared"
5
- import { fileScenarioIO, requireRenderer, resolveFixtures, resolveSafeAreaChoice } from "./render"
4
+ import { desktopDefaultViewport, desktopPluginLibraries, resolveDesktopRenderer } from "./app/shared"
5
+ import { desktopRenderFlags, fileScenarioIO, requireRenderer, resolveFixtures, resolveSafeAreaChoice, viewportFlags } from "./render"
6
6
  import { buildBundle } from "./compile"
7
- import { findProjectRoot } from "../manifest"
8
- import { runDesktopRender } from "../desktopRenderer"
9
- import { evaluateDesktopRun, translateScenario } from "../desktopScript"
10
- import { CliError, c, flagBool, flagStr, log, logErr, note, warnErr, type Args } from "../util"
7
+ import { findProjectRoot } from "../project/manifest"
8
+ import { runDesktopRender } from "../hosts/desktopRenderer"
9
+ import { evaluateDesktopRun, translateScenario } from "../hosts/desktopScript"
10
+ import { CliError, bool, c, defineCommand, log, logErr, note, num, str, warnErr, type FlagValues } from "../cli"
11
+ import { bundleFlags, bundleOptions } from "./shared"
11
12
 
12
13
  /*
13
- * `lecodes test` — run the project's headless flow tests (needs the optional `lecodes-renderer`
14
+ * `lecodes test` — run the project's headless flow tests (needs the optional `lecodes-headless`
14
15
  * package). Two sources, both executed as scenario step lists against a fresh session each:
15
16
  *
16
17
  * 1. `tests/*.flow.json` — hand-written scenarios (same schema as `lecodes render --script`:
@@ -26,6 +27,11 @@ import { CliError, c, flagBool, flagStr, log, logErr, note, warnErr, type Args }
26
27
  *
27
28
  * A failing step stops its flow and dumps the failing frame (JSON + PNG when @napi-rs/canvas is
28
29
  * installed) under tests/.artifacts/<flow>/ so a red test explains itself without a rerun.
30
+ *
31
+ * `--desktop` runs the file scenarios through the NATIVE host (desktopScript.ts — targets by name or
32
+ * coordinates, expect.log + expect.paint checked, the semantic expects reported skipped). The
33
+ * design-derived flows need `expect.screen` and `fill`, which the host can't answer, so they stay
34
+ * headless-only. The viewport / timing and --desktop flags are `lecodes render`'s own groups.
29
35
  */
30
36
 
31
37
  type DesignEdge = { from: string, to: string, label?: string, activator?: string }
@@ -37,7 +43,7 @@ type FlowCase = {
37
43
  steps: unknown
38
44
  /** Canned fetch responses for this case's session. File scenarios: their envelope `fixtures`
39
45
  * (inline or a path). Design flows: the project pack (tests/fixtures.json / --fixtures). */
40
- fixtures?: import("lecodes-renderer/headless").Fixtures
46
+ fixtures?: import("lecodes-headless/headless").Fixtures
41
47
  /** Per-file envelope overrides (file scenarios only). */
42
48
  width?: number
43
49
  height?: number
@@ -131,19 +137,47 @@ const readFlowFile = (abs: string, displayName: string): FlowCase => {
131
137
  }
132
138
  }
133
139
 
134
- export const test = async (args: Args) => {
135
- // --desktop: run every file scenario through the NATIVE host (desktopScript.ts — coordinates
136
- // only, expect.log checked, semantic expects reported skipped). Design-derived flows tap by
137
- // activator NAME, which the host can't resolve, so they stay headless-only.
138
- const desktop = flagBool(args, "desktop")
139
- if (desktop && flagBool(args, "flows")) throw new CliError("--flows needs the headless renderer (design flows tap by name) — drop --desktop.")
140
+ const testFlags = {
141
+ flows: bool("run ONLY the design-derived flows"),
142
+ "no-flows": bool("run ONLY the tests/*.flow.json files"),
143
+ fixtures: str("fixture pack for the design flows (overrides the tests/fixtures.json default)", { value: "<file>" }),
144
+ json: bool("machine-readable report on stdout"),
145
+ dir: str("tests folder", { value: "<folder>", default: "tests" }),
146
+ "design-dir": str("design folder", { value: "<dir>", default: "design" }),
147
+ artifacts: str("failure-artifact folder override (default <tests>/.artifacts)", { value: "<dir>" }),
148
+ timeout: num("hard cap on the whole run (default 20000 + 10000 per case; --desktop 60000 + 60000 per case)", { value: "<ms>" }),
149
+ ...viewportFlags,
150
+ ...bundleFlags,
151
+ ...desktopRenderFlags,
152
+ desktop: bool("run the file scenarios through the NATIVE host: the same steps, targets and expects as headless (expect.audio and the json output excepted); design flows stay headless. --frames / --fixed-dt as in render --desktop"),
153
+ }
154
+ // (`sim` comes with desktopRenderFlags: the file scenarios through the WINDOWLESS host — no GPU, no
155
+ // pixels, expect.log only; the same coordinate-only contract as --desktop minus screenshots.)
156
+
157
+ type TestFlags = FlagValues<typeof testFlags>
158
+
159
+ export default defineCommand({
160
+ name: "test",
161
+ summary: "Run the flow tests (tests/*.flow.json + design-derived flows)",
162
+ usage: "[name…]",
163
+ description: "Headless flow tests: tests/*.flow.json scenarios, plus flows derived from the design graph (design/meta.json activator edges — checks the built app against the designed navigation; needs named elements + screen roots). Failing steps dump the frame (JSON + PNG) under tests/.artifacts/<flow>/.\n\nDesign flows read tests/fixtures.json automatically (canned fetch responses) so backend-gated transitions run headless; file scenarios declare their own via the \"fixtures\" envelope. [name…] runs only those files (a path, or a name under the tests folder).",
164
+ flags: testFlags,
165
+ examples: ["lecodes test", "lecodes test hero --logs", "lecodes test --flows --json", "lecodes test --desktop"],
166
+ run: ({ args, flags }) => runTests(args, flags),
167
+ })
168
+
169
+ const runTests = async (args: string[], flags: TestFlags): Promise<void> => {
170
+ // --sim is the native path on the windowless host: everything --desktop does, minus pixels.
171
+ const sim = flags.sim
172
+ const desktop = flags.desktop || sim
173
+ if (desktop && flags.flows) throw new CliError(`--flows needs the headless renderer (design flows read expect.screen and fill inputs) — drop --${sim ? "sim" : "desktop"}.`)
140
174
  // --desktop: ONE renderer decision (host binary + the staged .filamat variant). The root is
141
175
  // recomputed here because `root` below comes from the headless bundle, which --desktop skips.
142
176
  let desktopRoot: string
143
177
  try { desktopRoot = findProjectRoot(process.cwd()) } catch { desktopRoot = process.cwd() }
144
- const desktopRenderer = desktop ? resolveDesktopRenderer(desktopRoot, args) : undefined
145
- const native = desktop ? await buildBundle(args, undefined, { localAssets: true, desktopRenderer }) : null
146
- const headless = desktop ? null : await compileHeadlessBundle(args)
178
+ const desktopRenderer = desktop ? resolveDesktopRenderer(desktopRoot, flags.renderer) : undefined
179
+ const native = desktop ? await buildBundle(bundleOptions(flags), { localAssets: true, desktopRenderer }) : null
180
+ const headless = desktop ? null : await compileHeadlessBundle({ entry: flags.entry, publicUrl: flags["public-url"], remoteAssets: flags["remote-assets"], waitNetwork: flags["wait-network"] })
147
181
  const warnings = headless?.warnings ?? []
148
182
  const useLocalAssets = headless?.useLocalAssets ?? false
149
183
  const js = headless?.js ?? ""
@@ -151,22 +185,21 @@ export const test = async (args: Args) => {
151
185
  if (headless) root = headless.root
152
186
  else { try { root = findProjectRoot(process.cwd()) } catch { root = process.cwd() } }
153
187
  const renderer = desktop ? null : await requireRenderer()
154
- const nativeFrames = Number(flagStr(args, "frames")) || 60
155
- const fixedDtFlag = flagStr(args, "fixed-dt")
156
- const nativeFixedDt = fixedDtFlag !== undefined ? Number(fixedDtFlag) : 16
157
- if (!Number.isFinite(nativeFixedDt) || nativeFixedDt < 0 || nativeFixedDt > 1000) throw new CliError(`--fixed-dt wants a number of ms (0 = wall clock, max 1000), got "${fixedDtFlag}".`)
188
+ const nativeFrames = flags.frames || 60
189
+ const nativeFixedDt = flags["fixed-dt"] ?? 16
190
+ if (nativeFixedDt < 0 || nativeFixedDt > 1000) throw new CliError(`--fixed-dt wants a number of ms (0 = wall clock, max 1000), got "${nativeFixedDt}".`)
158
191
  // Desktop-only projects (app.json desktop block, no ios/android) run flows desktop-sized.
159
192
  const projectViewport = desktopDefaultViewport(root)
160
193
 
161
- const testsDir = resolve(root, flagStr(args, "dir") ?? "tests")
162
- const designMetaPath = resolve(root, flagStr(args, "design-dir") ?? "design", "meta.json")
163
- const asJson = flagBool(args, "json")
194
+ const testsDir = resolve(root, flags.dir)
195
+ const designMetaPath = resolve(root, flags["design-dir"], "meta.json")
196
+ const asJson = flags.json
164
197
  const out = asJson ? logErr : log // --json owns stdout; the human report moves to stderr
165
198
 
166
199
  // 1. File scenarios: the given paths, or every tests/*.flow.json.
167
200
  const cases: FlowCase[] = []
168
- if (args._.length > 0) {
169
- for (const given of args._) {
201
+ if (args.length > 0) {
202
+ for (const given of args) {
170
203
  const candidates = [resolve(given), resolve(testsDir, given), resolve(testsDir, `${given}.flow.json`)]
171
204
  const abs = candidates.find((p) => existsSync(p))
172
205
  if (!abs) throw new CliError(`No such test: "${given}" (looked in ${testsDir}).`)
@@ -182,20 +215,19 @@ export const test = async (args: Args) => {
182
215
  // usually backend-gated). `tests/fixtures.json` is picked up automatically; --fixtures overrides
183
216
  // the path. File scenarios do NOT inherit it — they declare their own via the envelope, so a
184
217
  // deliberately-offline scenario (error states) stays offline.
185
- const fixturesFlag = flagStr(args, "fixtures")
186
218
  const defaultPack = resolve(testsDir, "fixtures.json")
187
- const flowFixtures = fixturesFlag !== undefined
188
- ? resolveFixtures(fixturesFlag, ".")
219
+ const flowFixtures = flags.fixtures !== undefined
220
+ ? resolveFixtures(flags.fixtures, ".")
189
221
  : existsSync(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined
190
222
 
191
223
  // 2. Design-graph flows (opt out with --no-flows; --flows runs ONLY these).
192
- const flowsOnly = flagBool(args, "flows")
193
- const wantFlows = !flagBool(args, "no-flows") && args._.length === 0
224
+ const flowsOnly = flags.flows
225
+ const wantFlows = !flags["no-flows"] && args.length === 0
194
226
  if (flowsOnly) cases.length = 0
195
227
  let skipped: SkippedFlow[] = []
196
228
  let entryScreen: string | null = null
197
229
  if (desktop && wantFlows && existsSync(designMetaPath)) {
198
- note("Design-derived flows skipped with --desktop (they tap by name; run `lecodes test` headless for them).")
230
+ note("Design-derived flows skipped with --desktop (they read expect.screen and fill inputs; run `lecodes test` headless for them).")
199
231
  }
200
232
  if (!desktop && (wantFlows || flowsOnly) && existsSync(designMetaPath)) {
201
233
  let meta: DesignMeta
@@ -211,7 +243,7 @@ export const test = async (args: Args) => {
211
243
  entryScreen = probe.screenName()
212
244
  probe.dispose()
213
245
  if (entryScreen === null) {
214
- warnErr("Design flows skipped: the app's start screen root has no `name`. Name each screen root with its design screen id (e.g. UIScreen([...], { name: \"login\" })).")
246
+ warnErr("Design flows skipped: the app's start screen root has no `name`. Name each screen root with its design screen id (e.g. UIScreen(...).named(\"login\")).")
215
247
  } else {
216
248
  const derived = deriveDesignFlows(meta, entryScreen)
217
249
  cases.push(...derived.flows.map((f) => ({ ...f, fixtures: flowFixtures })))
@@ -229,17 +261,17 @@ export const test = async (args: Args) => {
229
261
 
230
262
  // Hard timeout across the whole run (each case is bounded by settles, but a pathological app can
231
263
  // still spin) — generous per-case allowance, overridable.
232
- const timeoutMs = Number(flagStr(args, "timeout")) || (desktop ? 60000 + cases.length * 60000 : 20000 + cases.length * 10000)
264
+ const timeoutMs = flags.timeout || (desktop ? 60000 + cases.length * 60000 : 20000 + cases.length * 10000)
233
265
  const killer = setTimeout(() => {
234
266
  warnErr(`Test run timed out after ${timeoutMs}ms (raise --timeout or lower --settle).`)
235
267
  process.exit(1)
236
268
  }, timeoutMs)
237
269
  killer.unref?.()
238
270
 
239
- const onConsole = flagBool(args, "logs")
271
+ const onConsole = flags.logs
240
272
  ? (level: string, text: string) => logErr(`${c.dim(`[${level}]`)} ${text}`)
241
273
  : undefined
242
- const artifactsRoot = resolve(root, flagStr(args, "artifacts") ?? resolve(testsDir, ".artifacts"))
274
+ const artifactsRoot = resolve(root, flags.artifacts ?? resolve(testsDir, ".artifacts"))
243
275
 
244
276
  type CaseResult = {
245
277
  name: string
@@ -251,23 +283,38 @@ export const test = async (args: Args) => {
251
283
  }
252
284
  const results: CaseResult[] = []
253
285
 
286
+ const reportCase = (result: CaseResult): void => {
287
+ const skippedCount = result.steps.reduce((n, s) => n + (s.skipped?.length ?? 0), 0)
288
+ if (result.ok) {
289
+ out(`${c.green("✓")} ${result.name}${skippedCount > 0 ? c.dim(` (${skippedCount} semantic check${skippedCount === 1 ? "" : "s"} skipped on --desktop)`) : ""}`)
290
+ } else {
291
+ out(`${c.red("✗")} ${result.name}`)
292
+ if (result.error) out(` ${c.red(result.error)}`)
293
+ for (const s of result.steps) {
294
+ out(s.ok ? c.dim(` ✓ ${s.desc}`) : ` ${c.red(`✗ ${s.desc} — ${s.error}`)}`)
295
+ for (const sk of s.skipped ?? []) out(c.dim(` – skipped: ${sk}`))
296
+ }
297
+ if (result.artifacts.length > 0) out(c.dim(` failing frame: ${result.artifacts.join(", ")}`))
298
+ }
299
+ }
300
+
254
301
  // Native case runner: each case is its own host process (fresh state, like a fresh session);
255
302
  // per-step screenshots + the final frame land under the case's artifacts folder.
256
303
  const runNativeCase = async (kase: FlowCase): Promise<CaseResult> => {
257
304
  const caseDir = resolve(artifactsRoot, slug(kase.name))
258
- const plan = translateScenario(kase.steps, { outDir: caseDir, headroomFrames: nativeFrames })
305
+ const plan = translateScenario(kase.steps, { outDir: caseDir, headroomFrames: nativeFrames, sim })
259
306
  for (const st of plan.steps) if (st.screenshot) mkdirSync(dirname(st.screenshot), { recursive: true })
260
307
  mkdirSync(caseDir, { recursive: true })
261
308
  const finalPng = resolve(caseDir, "final.png")
262
309
  const { output } = await runDesktopRender(native!.js, native!.resources, {
263
- renderer: desktopRenderer,
310
+ renderer: desktopRenderer, host: sim ? "server" : "desktop",
264
311
  pluginDirs: desktopPluginLibraries(root).dirs,
265
- width: Number(flagStr(args, "width")) || kase.width || projectViewport?.width || 390,
266
- height: Number(flagStr(args, "height")) || kase.height || projectViewport?.height || 844,
267
- frames: plan.frameCap, outPng: finalPng, logs: flagBool(args, "logs"),
312
+ width: flags.width || kase.width || projectViewport?.width || 390,
313
+ height: flags.height || kase.height || projectViewport?.height || 844,
314
+ frames: plan.frameCap, outPng: finalPng, logs: flags.logs,
268
315
  timeoutMs: 60000 + plan.steps.length * 5000, script: plan.text, fixedDtMs: nativeFixedDt,
269
316
  })
270
- const r = evaluateDesktopRun(plan, output)
317
+ const r = evaluateDesktopRun(plan, output, (path) => existsSync(path) ? readFileSync(path, "utf8") : null)
271
318
  if (!r.loaded) throw new CliError("This desktop host doesn't support scripted input (CREATOR_SCRIPT). Update it: lecodes desktop update (or point LECODES_DESKTOP_EXE at a newer build).")
272
319
  return {
273
320
  name: kase.name, kind: kase.kind, ok: r.ok,
@@ -278,7 +325,6 @@ export const test = async (args: Args) => {
278
325
  }
279
326
 
280
327
  for (const kase of cases) {
281
- const flagSettle = flagStr(args, "settle")
282
328
  let result: CaseResult
283
329
  if (desktop) {
284
330
  try {
@@ -295,13 +341,13 @@ export const test = async (args: Args) => {
295
341
  const steps = renderer!.parseScenarioSteps(kase.steps)
296
342
  // Every case runs in a FRESH session — no state bleeds between flows.
297
343
  const session = await renderer!.createSession(js, {
298
- width: Number(flagStr(args, "width")) || kase.width || projectViewport?.width || 390,
299
- height: Number(flagStr(args, "height")) || kase.height || projectViewport?.height || 844,
300
- settleMs: flagSettle !== undefined ? Number(flagSettle) : kase.settle,
301
- waitNetwork: flagBool(args, "wait-network"),
344
+ width: flags.width || kase.width || projectViewport?.width || 390,
345
+ height: flags.height || kase.height || projectViewport?.height || 844,
346
+ settleMs: flags.settle ?? kase.settle,
347
+ waitNetwork: flags["wait-network"],
302
348
  localAssets: useLocalAssets,
303
349
  fixtures: kase.fixtures,
304
- ...resolveSafeAreaChoice(flagStr(args, "safe-area") ?? kase.safeArea, flagStr(args, "device") ?? kase.device),
350
+ ...resolveSafeAreaChoice(flags["safe-area"] ?? kase.safeArea, flags.device ?? kase.device),
305
351
  onConsole,
306
352
  })
307
353
  try {
@@ -323,21 +369,6 @@ export const test = async (args: Args) => {
323
369
  reportCase(result)
324
370
  }
325
371
 
326
- function reportCase(result: CaseResult): void {
327
- const skippedCount = result.steps.reduce((n, s) => n + (s.skipped?.length ?? 0), 0)
328
- if (result.ok) {
329
- out(`${c.green("✓")} ${result.name}${skippedCount > 0 ? c.dim(` (${skippedCount} semantic check${skippedCount === 1 ? "" : "s"} skipped on --desktop)`) : ""}`)
330
- } else {
331
- out(`${c.red("✗")} ${result.name}`)
332
- if (result.error) out(` ${c.red(result.error)}`)
333
- for (const s of result.steps) {
334
- out(s.ok ? c.dim(` ✓ ${s.desc}`) : ` ${c.red(`✗ ${s.desc} — ${s.error}`)}`)
335
- for (const sk of s.skipped ?? []) out(c.dim(` – skipped: ${sk}`))
336
- }
337
- if (result.artifacts.length > 0) out(c.dim(` failing frame: ${result.artifacts.join(", ")}`))
338
- }
339
- }
340
-
341
372
  for (const w of warnings) warnErr(w)
342
373
  const passed = results.filter((r) => r.ok).length
343
374
  const failed = results.length - passed