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.
- package/README.md +56 -57
- package/dist/index.js +11052 -7592
- package/package.json +14 -8
- package/runtime/materials/decal-relief.mat +12 -1
- package/runtime/materials/decal.mat +17 -3
- package/runtime/materials/lightmap.mat +117 -138
- package/runtime/materials/particles-quad.mat +6 -2
- package/runtime/materials/terrain-lightmap.mat +123 -0
- package/runtime/materials/terrain.mat +36 -55
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/VERSION.json +5 -0
- package/runtime/web/assets/camera-5XOM8eHl.js +1 -0
- package/runtime/web/assets/creator-2d-D9WKYM6h.js +1 -0
- package/runtime/web/assets/creator-2d-ukvEmNEp.wasm +0 -0
- package/runtime/web/assets/creator-full-BBXzQlhq.js +1 -0
- package/runtime/web/assets/creator-full-D2dSDdDj.wasm +0 -0
- package/runtime/web/assets/creator-full.js-BYswGsqR.symbols +8124 -0
- package/runtime/web/assets/creator-ui-DIc92ion.wasm +0 -0
- package/runtime/web/assets/creator-ui-_1Z_AX2M.js +1 -0
- package/runtime/web/assets/geolocation-BjTKN9_P.js +1 -0
- package/runtime/web/assets/inspect-C7x78PNs.js +2 -0
- package/runtime/web/assets/map-CyBxOSV0.js +1 -0
- package/runtime/web/assets/map-XzoOFt0E.js +2 -0
- package/runtime/web/assets/mapImpl-bxvQ2h1Z.js +823 -0
- package/runtime/web/assets/maplibre-gl-worker-B7LXXUkR.js +8 -0
- package/runtime/web/assets/materials-uri4FvPE.bin +0 -0
- package/runtime/web/assets/materials-world-CQWt8TAm.bin +0 -0
- package/runtime/web/assets/qr-scanner-CK_cYoJR.js +1 -0
- package/runtime/web/assets/uberarchive-DLH-kBhM.bin +0 -0
- package/runtime/web/index.html +36 -0
- package/runtime/web/player.js +21 -0
- package/runtime/web-page/index.html +41 -0
- package/runtime/web-page/page.css +1 -0
- package/runtime/web-page/page.js +31 -0
- package/src/api.ts +1 -302
- package/src/cli/args.ts +37 -0
- package/src/cli/command.ts +113 -0
- package/src/cli/errors.ts +13 -0
- package/src/cli/help.ts +158 -0
- package/src/cli/index.ts +7 -0
- package/src/cli/output.ts +102 -0
- package/src/cli/run.ts +106 -0
- package/src/commands/{appAndroid.ts → app/android.ts} +454 -43
- package/src/commands/{appDesktop.ts → app/desktop.ts} +64 -33
- package/src/commands/{appDesktopMac.ts → app/desktopMac.ts} +34 -27
- package/src/commands/{appIcon.ts → app/icon.ts} +11 -8
- package/src/commands/{app.ts → app/index.ts} +400 -290
- package/src/commands/{appShared.ts → app/shared.ts} +43 -44
- package/src/commands/{appTemplatesAndroid.ts → app/templates/android.ts} +72 -39
- package/src/commands/{appTemplatesGradlew.ts → app/templates/gradlew.ts} +1 -1
- package/src/commands/{appTemplates.ts → app/templates/ios.ts} +2 -2
- package/src/commands/assets.ts +23 -21
- package/src/commands/clone.ts +48 -51
- package/src/commands/compile.ts +76 -89
- package/src/commands/create.ts +46 -47
- package/src/commands/design/comments.ts +361 -0
- package/src/commands/design/context.ts +101 -0
- package/src/commands/design/index.ts +391 -0
- package/src/commands/design/snapshot.ts +134 -0
- package/src/commands/{designTemplates.ts → design/templates.ts} +4 -3
- package/src/commands/desktop.ts +94 -82
- package/src/commands/dev.ts +260 -182
- package/src/commands/diff.ts +39 -51
- package/src/commands/index.ts +64 -0
- package/src/commands/{init.ts → init/index.ts} +195 -191
- package/src/commands/{projectTemplates.ts → init/templates.ts} +64 -21
- package/src/commands/install.ts +80 -67
- package/src/commands/lightmap.ts +543 -252
- package/src/commands/link.ts +87 -94
- package/src/commands/login.ts +25 -23
- package/src/commands/navmesh.ts +169 -139
- package/src/commands/plugin.ts +46 -0
- package/src/commands/pn.ts +201 -244
- package/src/commands/pull.ts +49 -56
- package/src/commands/push.ts +48 -54
- package/src/commands/render.ts +176 -111
- package/src/commands/scene.ts +63 -55
- package/src/commands/serve.ts +58 -0
- package/src/commands/{shaders.ts → shaders/index.ts} +61 -42
- package/src/commands/{shadersNew.ts → shaders/new.ts} +3 -3
- package/src/commands/shared.ts +79 -0
- package/src/commands/status.ts +22 -26
- package/src/commands/test.ts +94 -63
- package/src/commands/thumbs.ts +185 -175
- package/src/commands/update/index.ts +226 -0
- package/src/commands/{types.ts → update/types.ts} +31 -22
- package/src/compile/collect.ts +5 -5
- package/src/compile/collectLocal.ts +3 -3
- package/src/compile/designCompile.ts +4 -4
- package/src/compile/headlessBundle.ts +16 -12
- package/src/compile/nativeStack.ts +1 -1
- package/src/compile/projectCompile.ts +12 -4
- package/src/compile/sceneCompile.ts +14 -26
- package/src/compile/screenEntry.ts +123 -127
- package/src/compile/shaders.ts +3 -3
- package/src/{lecodes-3d-editor.d.ts → declarations/lecodes-3d-editor.d.ts} +4 -0
- package/src/{lecodes-assets.d.ts → declarations/lecodes-assets.d.ts} +1 -1
- package/src/{lecodes-renderer.d.ts → declarations/lecodes-headless.d.ts} +3 -3
- package/src/dev/androidDev.ts +1 -1
- package/src/dev/clientTemplates.ts +18 -14
- package/src/dev/devServer.ts +78 -41
- package/src/dev/iosDev.ts +180 -0
- package/src/dev/webRunner.ts +83 -15
- package/src/{cmgenTool.ts → hosts/cmgenTool.ts} +2 -2
- package/src/{desktopRenderer.ts → hosts/desktopRenderer.ts} +104 -32
- package/src/{desktopScript.ts → hosts/desktopScript.ts} +151 -36
- package/src/{distRoot.ts → hosts/distRoot.ts} +9 -5
- package/src/{matcTool.ts → hosts/matcTool.ts} +2 -2
- package/src/{peerInstall.ts → hosts/peerInstall.ts} +7 -7
- package/src/{peers.ts → hosts/peers.ts} +2 -2
- package/src/{releases.ts → hosts/releases.ts} +1 -1
- package/src/index.ts +35 -480
- package/src/platform/api.ts +302 -0
- package/src/{browserAuth.ts → platform/browserAuth.ts} +1 -1
- package/src/{config.ts → platform/config.ts} +1 -1
- package/src/{serverDiff.ts → platform/serverDiff.ts} +3 -3
- package/src/plugin/contract.ts +438 -0
- package/src/plugin/emitKotlin.ts +357 -0
- package/src/plugin/emitSdk.ts +259 -0
- package/src/plugin/emitSwift.ts +336 -0
- package/src/plugin/emitWeb.ts +383 -0
- package/src/plugin/generate.ts +157 -0
- package/src/plugin/lines.ts +28 -0
- package/src/plugin/lower.ts +231 -0
- package/src/plugin/manifest.ts +192 -0
- package/src/{projectEnv.ts → project/env.ts} +33 -2
- package/src/{ignore.ts → project/ignore.ts} +164 -163
- package/src/{localFiles.ts → project/localFiles.ts} +1 -1
- package/src/{manifest.ts → project/manifest.ts} +1 -1
- package/src/{project.ts → project/materialize.ts} +2 -2
- package/src/project/paths.ts +20 -0
- package/src/{textDiff.ts → project/textDiff.ts} +1 -1
- package/src/{types.ts → project/types.ts} +0 -0
- package/runtime/materials/lightmap-baked-lite.mat +0 -175
- package/runtime/materials/lightmap-baked.mat +0 -176
- package/runtime/scene-harness.json +0 -1
- package/runtime/web/assets/__vite-browser-external-BIHI7g3E.js +0 -1
- package/runtime/web/assets/basis-C64VHDVD.js +0 -1
- package/runtime/web/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
- package/runtime/web/assets/createViewerLite-Ct_PZdof.js +0 -852
- package/runtime/web/assets/draco-BiISTFcR.js +0 -118
- package/runtime/web/assets/draco_decoder-DsQ12WqX.wasm +0 -0
- package/runtime/web/assets/index-BMt7AnC5.js +0 -2
- package/runtime/web/assets/mapViewImpl-B2JcES8l.js +0 -810
- package/runtime/web/assets/maplibre-gl-worker-CJfwIrte.js +0 -8
- package/runtime/web/assets/worker-Caf-yYEI.js +0 -2
- package/runtime/web/embed.html +0 -32
- package/runtime/web/embed.js +0 -171
- package/src/commands/design.ts +0 -846
- package/src/commands/update.ts +0 -211
- package/src/util.ts +0 -146
- /package/src/{lecodes-design.d.ts → declarations/lecodes-design.d.ts} +0 -0
- /package/src/{qrcode-terminal.d.ts → declarations/qrcode-terminal.d.ts} +0 -0
- /package/src/{designMeta.ts → project/designMeta.ts} +0 -0
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { resolve } from "node:path"
|
|
2
|
-
import { CliError } from "
|
|
2
|
+
import { CliError } from "../cli"
|
|
3
|
+
// The evaluator lives with the headless runner (one source of truth); a relative source import,
|
|
4
|
+
// since lecodes-headless is an OPTIONAL peer of the CLI and --desktop must not require it.
|
|
5
|
+
import { evaluateDumpExpect, evaluatePaintExpect, parseTreeDump, type DumpSemanticExpect, type PaintExpect } from "../../../../runtime/headless/src/headless/paintDump"
|
|
3
6
|
|
|
4
7
|
/*
|
|
5
8
|
* `lecodes render --desktop --script`: the same JSON scenario a headless run takes, driven through
|
|
6
|
-
* the NATIVE desktop host. The host
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* the NATIVE desktop host. The host takes a line-based command file (CREATOR_SCRIPT, the contract in
|
|
10
|
+
* hosts/desktop/src/host/script.h) and reports `[script] #<line> done <op>` on stdout as each command
|
|
11
|
+
* completes. A pointer target goes over as `X Y` (logical px) or as a quoted "name": the RUNTIME
|
|
12
|
+
* resolves it exactly as the headless session does (name → exact text → text substring, creator-pkg
|
|
13
|
+
* pkgFindNode) and the renderer places it — so a flow written by name runs on the desktop too; `fill`
|
|
14
|
+
* / `scroll` / `clip` are host commands of their own. This module does both halves: translate the
|
|
15
|
+
* scenario into that file and turn the host's stdout back into per-step results — `expect.log` is
|
|
16
|
+
* evaluated here against the console lines seen up to each step's marker; `expect.paint` and the
|
|
17
|
+
* semantic expects (`text` / `node` / `screen` / `value`) against the tree dump the host writes right
|
|
18
|
+
* after the step (the same rules as the headless session, paintDump.ts). Only `expect.audio` (the
|
|
19
|
+
* headless recorder's) and the `json` output (the headless renderer's shape) are reported as
|
|
20
|
+
* skipped, never silently passed. The windowless host (`--sim`) has no UI at all: names, hover,
|
|
21
|
+
* typing, fill, scroll and screenshots are refused there.
|
|
13
22
|
*/
|
|
14
23
|
|
|
15
24
|
type Point = { x: number, y: number }
|
|
@@ -24,9 +33,16 @@ export type DesktopPlanStep = {
|
|
|
24
33
|
/** Script line numbers this step emitted; the LAST one's `done` marker completes the step. */
|
|
25
34
|
lines: number[]
|
|
26
35
|
expectLog?: string
|
|
27
|
-
/**
|
|
36
|
+
/** expect.paint and the semantic expects: the host writes the tree dump to `paintDump` (a `dump`
|
|
37
|
+
* line) right before the step's marker; the assertions are evaluated here against that file. */
|
|
38
|
+
expectPaint?: PaintExpect
|
|
39
|
+
expectSemantic?: DumpSemanticExpect
|
|
40
|
+
paintDump?: string
|
|
41
|
+
/** A `dump` output step: the file the host wrote. */
|
|
42
|
+
dump?: string
|
|
43
|
+
/** Expect keys the native host can't evaluate (`audio`: the headless recorder's) — reported as skipped. */
|
|
28
44
|
skippedExpect: string[]
|
|
29
|
-
/** Outputs the native host can't produce (`json`). */
|
|
45
|
+
/** Outputs the native host can't produce (`json`: the headless renderer's semantic shape — use `dump`). */
|
|
30
46
|
skippedOutputs: string[]
|
|
31
47
|
screenshot?: string
|
|
32
48
|
}
|
|
@@ -39,31 +55,41 @@ export type DesktopPlan = {
|
|
|
39
55
|
frameCap: number
|
|
40
56
|
}
|
|
41
57
|
|
|
42
|
-
const ACTIONS = ["tap", "type", "fill", "scroll", "wait", "frames", "drag", "hold", "key", "keyDown", "keyUp", "look", "mouseTo", "wheel", "stick", "gamepad", "camera", "view"] as const
|
|
43
|
-
const SEMANTIC_EXPECT = ["text", "notText", "node", "notNode", "screen", "value"
|
|
58
|
+
const ACTIONS = ["tap", "type", "fill", "scroll", "wait", "frames", "drag", "hold", "hover", "key", "keyDown", "keyUp", "look", "mouseTo", "wheel", "stick", "gamepad", "camera", "view"] as const
|
|
59
|
+
const SEMANTIC_EXPECT = ["text", "notText", "node", "notNode", "screen", "value"]
|
|
60
|
+
const UNSUPPORTED_EXPECT = ["audio"]
|
|
44
61
|
const VIEW_NAMES = ["iso", "top", "front", "side", "back"]
|
|
45
62
|
const isVec3 = (v: unknown): v is [number, number, number] => Array.isArray(v) && v.length === 3 && v.every((n) => typeof n === "number")
|
|
46
63
|
|
|
47
|
-
|
|
64
|
+
type Target = Point | string
|
|
65
|
+
const pt = (p: Target) => typeof p === "string" ? p : `(${p.x},${p.y})`
|
|
66
|
+
/** A target as the host's script reads it: `X Y`, or a quoted "name" (`"` and `\` escaped). */
|
|
67
|
+
const wire = (p: Target) => typeof p === "string" ? `"${p.replace(/[\\"]/g, (ch) => "\\" + ch)}"` : `${p.x} ${p.y}`
|
|
68
|
+
/** `type` text as the host's `type` line takes it: the rest of the line, `\n` = enter, `\\` = a backslash. */
|
|
69
|
+
const wireText = (s: string) => s.replace(/\\/g, "\\\\").replace(/\r?\n/g, "\\n")
|
|
48
70
|
|
|
49
71
|
/** Translate scenario steps into the host's command file. `headroomFrames` runs first so the app
|
|
50
|
-
* can load (the `--frames` value); screenshots land under `outDir`.
|
|
51
|
-
*
|
|
52
|
-
|
|
72
|
+
* can load (the `--frames` value); screenshots land under `outDir`. `sim` = the windowless host
|
|
73
|
+
* (`--sim`): it has no UI, so names, `hover`, `type`, `fill` and `scroll` are refused there, it
|
|
74
|
+
* paints nothing, so per-step screenshots are refused too, and a tap/drag/hold reaches the SCENE's
|
|
75
|
+
* touch input directly. */
|
|
76
|
+
export const translateScenario = (rawSteps: unknown, opts: { outDir: string, headroomFrames: number, sim?: boolean }): DesktopPlan => {
|
|
53
77
|
if (!Array.isArray(rawSteps)) throw new CliError(`"steps" must be an array of step objects.`)
|
|
54
78
|
const lines: string[] = []
|
|
55
79
|
const steps: DesktopPlanStep[] = []
|
|
56
80
|
let budgetMs = 0
|
|
57
81
|
const emit = (line: string): number => { lines.push(line); return lines.length }
|
|
58
|
-
emit(`# generated by lecodes render --desktop --script`)
|
|
82
|
+
emit(`# generated by lecodes render ${opts.sim ? "--sim" : "--desktop"} --script`)
|
|
59
83
|
emit(`frames ${Math.max(0, opts.headroomFrames)}`)
|
|
60
84
|
|
|
61
|
-
const
|
|
85
|
+
const target = (at: string, key: string, v: unknown): Target => {
|
|
62
86
|
if (isPoint(v)) return v
|
|
63
87
|
if (typeof v === "string") {
|
|
64
|
-
throw new CliError(`${at}: "${key}"
|
|
88
|
+
if (v.length === 0) throw new CliError(`${at}: "${key}" is an empty name.`)
|
|
89
|
+
if (opts.sim) throw new CliError(`${at}: "${key}": "${v}" — the windowless host has no UI to resolve a name in; give --sim scripts { "x", "y" } coordinates (scene input), or run the scenario with --desktop / headless.`)
|
|
90
|
+
return v
|
|
65
91
|
}
|
|
66
|
-
throw new CliError(`${at}: "${key}" must be { "x", "y" }.`)
|
|
92
|
+
throw new CliError(`${at}: "${key}" must be a name or { "x", "y" }.`)
|
|
67
93
|
}
|
|
68
94
|
const num = (at: string, key: string, v: unknown, def?: number): number => {
|
|
69
95
|
if (v === undefined && def !== undefined) return def
|
|
@@ -84,10 +110,10 @@ export const translateScenario = (rawSteps: unknown, opts: { outDir: string, hea
|
|
|
84
110
|
const parts: string[] = []
|
|
85
111
|
const action = actions[0]
|
|
86
112
|
|
|
87
|
-
if (action === "type" || action === "fill" || action === "scroll") {
|
|
88
|
-
throw new CliError(`${at}: "${action}" isn't available with --
|
|
113
|
+
if ((action === "type" || action === "hover" || action === "fill" || action === "scroll") && opts.sim) {
|
|
114
|
+
throw new CliError(`${at}: "${action}" isn't available with --sim (the windowless host has no UI) — run this scenario with --desktop or headless.`)
|
|
89
115
|
}
|
|
90
|
-
if ((action === "tap" || action === "drag" || action === "hold") && inspecting) {
|
|
116
|
+
if ((action === "tap" || action === "drag" || action === "hold" || action === "hover" || action === "type") && inspecting) {
|
|
91
117
|
throw new CliError(`${at}: "${action}" while an inspection camera is active — the app's hit-testing uses its own camera, not the one in the shot. Add { "view": "off" } (or { "camera": "off" }) first.`)
|
|
92
118
|
}
|
|
93
119
|
if (action === "view") {
|
|
@@ -112,19 +138,52 @@ export const translateScenario = (rawSteps: unknown, opts: { outDir: string, hea
|
|
|
112
138
|
}
|
|
113
139
|
if (s.fov !== undefined && action !== "view") throw new CliError(`${at}: "fov" only goes with "view" (inside "camera" use camera.fov).`)
|
|
114
140
|
if (action === "tap") {
|
|
115
|
-
const p =
|
|
116
|
-
plan.lines.push(emit(`tap ${p
|
|
141
|
+
const p = target(at, "tap", s.tap)
|
|
142
|
+
plan.lines.push(emit(`tap ${wire(p)}`)); budgetMs += 4 * FRAME_MS
|
|
117
143
|
parts.push(`tap ${pt(p)}`)
|
|
144
|
+
} else if (action === "type") {
|
|
145
|
+
// The host types into the FOCUSED input: a tap on the target focuses it, then the characters go
|
|
146
|
+
// in one by one (appended — the headless session sets the value; on an empty input they agree).
|
|
147
|
+
const p = target(at, "type", s.type)
|
|
148
|
+
if (typeof s.text !== "string") throw new CliError(`${at}: "type" needs a "text" string.`)
|
|
149
|
+
plan.lines.push(emit(`tap ${wire(p)}`))
|
|
150
|
+
plan.lines.push(emit(`type ${wireText(s.text)}`)); budgetMs += 6 * FRAME_MS
|
|
151
|
+
parts.push(`type ${pt(p)}="${s.text}"`)
|
|
118
152
|
} else if (action === "drag") {
|
|
119
|
-
const a =
|
|
153
|
+
const a = target(at, "drag", s.drag), b = target(at, "to", s.to)
|
|
120
154
|
const ms = num(at, "ms", s.ms, 300)
|
|
121
|
-
plan.lines.push(emit(`drag ${a
|
|
155
|
+
plan.lines.push(emit(`drag ${wire(a)} ${wire(b)} ${Math.round(ms)}`)); budgetMs += ms + 4 * FRAME_MS
|
|
122
156
|
parts.push(`drag ${pt(a)} → ${pt(b)} over ${ms}ms`)
|
|
123
157
|
} else if (action === "hold") {
|
|
124
|
-
const p =
|
|
158
|
+
const p = target(at, "hold", s.hold)
|
|
125
159
|
const ms = num(at, "ms", s.ms)
|
|
126
|
-
plan.lines.push(emit(`hold ${p
|
|
160
|
+
plan.lines.push(emit(`hold ${wire(p)} ${Math.round(ms)}`)); budgetMs += ms + 4 * FRAME_MS
|
|
127
161
|
parts.push(`hold ${pt(p)} ${ms}ms`)
|
|
162
|
+
} else if (action === "hover") {
|
|
163
|
+
// The cursor over a node: Input.mouse.position + the UI hover (`$hovered`, onMouseEnter). The
|
|
164
|
+
// host completes it two frames later, after the enter / leave crossed into JS.
|
|
165
|
+
const p = target(at, "hover", s.hover)
|
|
166
|
+
plan.lines.push(emit(`mouseto ${wire(p)}`)); budgetMs += 3 * FRAME_MS
|
|
167
|
+
parts.push(`hover ${pt(p)}`)
|
|
168
|
+
} else if (action === "fill") {
|
|
169
|
+
const f = s.fill
|
|
170
|
+
if (typeof f !== "object" || f === null || ((f as { value?: unknown }).value !== undefined && typeof (f as { value?: unknown }).value !== "string")) {
|
|
171
|
+
throw new CliError(`${at}: "fill" must be {} or { "value": string }.`)
|
|
172
|
+
}
|
|
173
|
+
const value = (f as { value?: string }).value
|
|
174
|
+
plan.lines.push(emit(value !== undefined ? `fill ${wireText(value)}` : "fill")); budgetMs += 8 * 6 * FRAME_MS // room for a form of inputs
|
|
175
|
+
parts.push(value !== undefined ? `fill inputs ("${value}")` : "fill inputs")
|
|
176
|
+
} else if (action === "scroll") {
|
|
177
|
+
// `{ "scroll": 300 }` = the first scrollable; `{ "scroll": "feed", "by": 300 }` = the named one's.
|
|
178
|
+
if (typeof s.scroll === "number") {
|
|
179
|
+
if (s.by !== undefined) throw new CliError(`${at}: "by" only goes with a named "scroll".`)
|
|
180
|
+
plan.lines.push(emit(`scroll ${s.scroll}`)); parts.push(`scroll ${s.scroll}`)
|
|
181
|
+
} else if (typeof s.scroll === "string") {
|
|
182
|
+
if (typeof s.by !== "number") throw new CliError(`${at}: scrolling a named target needs "by" (px).`)
|
|
183
|
+
if (s.scroll.length === 0) throw new CliError(`${at}: "scroll" is an empty name.`)
|
|
184
|
+
plan.lines.push(emit(`scroll ${s.by} ${wire(s.scroll)}`)); parts.push(`scroll ${s.scroll} by ${s.by}`)
|
|
185
|
+
} else throw new CliError(`${at}: "scroll" must be a number (px) or a name (+ "by").`)
|
|
186
|
+
budgetMs += 2 * FRAME_MS
|
|
128
187
|
} else if (action === "key") {
|
|
129
188
|
if (typeof s.key !== "string") throw new CliError(`${at}: "key" must be a KeyboardEvent.code string.`)
|
|
130
189
|
const ms = num(at, "ms", s.ms)
|
|
@@ -144,7 +203,7 @@ export const translateScenario = (rawSteps: unknown, opts: { outDir: string, hea
|
|
|
144
203
|
} else if (action === "mouseTo") {
|
|
145
204
|
const v = s.mouseTo
|
|
146
205
|
if (!Array.isArray(v) || v.length !== 2 || !v.every((n: unknown) => typeof n === "number")) throw new CliError(`${at}: "mouseTo" must be [x, y] (logical px).`)
|
|
147
|
-
plan.lines.push(emit(`mouseto ${v[0]} ${v[1]}`)); budgetMs += FRAME_MS
|
|
206
|
+
plan.lines.push(emit(`mouseto ${v[0]} ${v[1]}`)); budgetMs += 3 * FRAME_MS
|
|
148
207
|
parts.push(`mouseTo ${v[0]},${v[1]}`)
|
|
149
208
|
} else if (action === "wheel") {
|
|
150
209
|
const v = s.wheel
|
|
@@ -184,29 +243,71 @@ export const translateScenario = (rawSteps: unknown, opts: { outDir: string, hea
|
|
|
184
243
|
if (typeof s.expect !== "object" || s.expect === null) throw new CliError(`${at}: "expect" must be an object.`)
|
|
185
244
|
const e = s.expect as Record<string, unknown>
|
|
186
245
|
const bits: string[] = []
|
|
246
|
+
let needDump = false
|
|
187
247
|
for (const k of Object.keys(e)) {
|
|
188
248
|
if (k === "log") {
|
|
189
249
|
if (typeof e.log !== "string") throw new CliError(`${at}: expect.log must be a string.`)
|
|
190
250
|
plan.expectLog = e.log
|
|
191
251
|
bits.push(`log~"${e.log}"`)
|
|
252
|
+
} else if (k === "paint") {
|
|
253
|
+
if (typeof e.paint !== "object" || e.paint === null) throw new CliError(`${at}: expect.paint must be an object of node names.`)
|
|
254
|
+
plan.expectPaint = e.paint as PaintExpect
|
|
255
|
+
needDump = true
|
|
256
|
+
bits.push(`paint(${Object.keys(e.paint).join(", ")})`)
|
|
192
257
|
} else if (SEMANTIC_EXPECT.includes(k)) {
|
|
258
|
+
if (k === "value") {
|
|
259
|
+
const v = e.value as { target?: unknown, equals?: unknown } | null
|
|
260
|
+
if (typeof v !== "object" || v === null || typeof v.target !== "string" || typeof v.equals !== "string") throw new CliError(`${at}: expect.value needs { "target": string, "equals": string }.`)
|
|
261
|
+
} else if (typeof e[k] !== "string") throw new CliError(`${at}: expect.${k} must be a string.`)
|
|
262
|
+
plan.expectSemantic = { ...(plan.expectSemantic ?? {}), [k]: e[k] }
|
|
263
|
+
needDump = true
|
|
264
|
+
const v = e[k]
|
|
265
|
+
bits.push(k === "screen" ? `screen=${v}` : k === "node" ? `node=${v}` : k === "notNode" ? `!node=${v}` : k === "text" ? `text~"${v}"` : k === "notText" ? `!text~"${v}"` : `${(v as { target: string }).target}="${(v as { equals: string }).equals}"`)
|
|
266
|
+
} else if (UNSUPPORTED_EXPECT.includes(k)) {
|
|
193
267
|
plan.skippedExpect.push(k)
|
|
194
268
|
} else {
|
|
195
269
|
throw new CliError(`${at}: unknown expect key "${k}".`)
|
|
196
270
|
}
|
|
197
271
|
}
|
|
272
|
+
if (needDump) {
|
|
273
|
+
plan.paintDump = resolve(opts.outDir, `.dump-${i}.json`)
|
|
274
|
+
// The tree dump lands one frame after the action settled, like a screenshot.
|
|
275
|
+
plan.lines.push(emit(`frames 1`))
|
|
276
|
+
plan.lines.push(emit(`dump ${plan.paintDump}`)); budgetMs += 2 * FRAME_MS
|
|
277
|
+
}
|
|
198
278
|
if (bits.length > 0) parts.push(`expect ${bits.join(" · ")}`)
|
|
199
279
|
}
|
|
200
|
-
if (s.clip !== undefined) throw new CliError(`${at}: "clip"
|
|
280
|
+
if (s.clip !== undefined && s.screenshot === undefined) throw new CliError(`${at}: "clip" only goes with "screenshot".`)
|
|
201
281
|
if (s.screenshot !== undefined) {
|
|
202
282
|
if (typeof s.screenshot !== "string") throw new CliError(`${at}: "screenshot" must be a path.`)
|
|
283
|
+
if (opts.sim) throw new CliError(`${at}: "screenshot" isn't available with --sim (the windowless host paints nothing) — judge the run by expect.log, or run it with --desktop for pixels.`)
|
|
203
284
|
plan.screenshot = resolve(opts.outDir, s.screenshot)
|
|
204
285
|
// The shot lands AFTER the action settles (two frames), like the headless act → capture order.
|
|
286
|
+
// A clip (a node's box or a logical-px rect) is set for the shot and cleared after it.
|
|
287
|
+
const clip = s.clip
|
|
288
|
+
if (clip !== undefined) {
|
|
289
|
+
if (typeof clip === "string") {
|
|
290
|
+
if (clip.length === 0) throw new CliError(`${at}: "clip" is an empty name.`)
|
|
291
|
+
plan.lines.push(emit(`frames 2`)) // the node's box after the action settled
|
|
292
|
+
plan.lines.push(emit(`clip ${wire(clip)}`))
|
|
293
|
+
} else if (typeof clip === "object" && clip !== null && ["x", "y", "width", "height"].every((k) => typeof (clip as Record<string, unknown>)[k] === "number")) {
|
|
294
|
+
const r = clip as { x: number, y: number, width: number, height: number }
|
|
295
|
+
plan.lines.push(emit(`clip ${r.x} ${r.y} ${r.width} ${r.height}`))
|
|
296
|
+
} else throw new CliError(`${at}: "clip" must be a node name or { "x", "y", "width", "height" }.`)
|
|
297
|
+
}
|
|
205
298
|
plan.lines.push(emit(`frames 2`))
|
|
206
|
-
plan.lines.push(emit(`shot ${plan.screenshot}`)); budgetMs +=
|
|
207
|
-
|
|
299
|
+
plan.lines.push(emit(`shot ${plan.screenshot}`)); budgetMs += 5 * FRAME_MS
|
|
300
|
+
if (clip !== undefined) plan.lines.push(emit(`clip off`))
|
|
301
|
+
parts.push(`→ ${s.screenshot}${clip !== undefined ? ` (clip ${typeof clip === "string" ? clip : "rect"})` : ""}`)
|
|
208
302
|
}
|
|
209
303
|
if (s.json !== undefined) plan.skippedOutputs.push("json")
|
|
304
|
+
if (s.dump !== undefined) {
|
|
305
|
+
if (typeof s.dump !== "string") throw new CliError(`${at}: "dump" must be a path.`)
|
|
306
|
+
plan.dump = resolve(opts.outDir, s.dump)
|
|
307
|
+
plan.lines.push(emit(`frames 1`))
|
|
308
|
+
plan.lines.push(emit(`dump ${plan.dump}`)); budgetMs += 2 * FRAME_MS
|
|
309
|
+
parts.push(`→ ${s.dump} (dump)`)
|
|
310
|
+
}
|
|
210
311
|
if (plan.lines.length === 0) {
|
|
211
312
|
// An expect-only step still needs a marker to anchor its evaluation.
|
|
212
313
|
plan.lines.push(emit(`frames 1`)); budgetMs += FRAME_MS
|
|
@@ -230,7 +331,7 @@ export type DesktopRunResult = {
|
|
|
230
331
|
|
|
231
332
|
/** Turn the host's stdout into per-step results. Console lines are every non-`[script]` line the
|
|
232
333
|
* host printed up to a step's completion marker (the core prints `console.log` as `[DEBUG] …`). */
|
|
233
|
-
export const evaluateDesktopRun = (plan: DesktopPlan, output: string): DesktopRunResult => {
|
|
334
|
+
export const evaluateDesktopRun = (plan: DesktopPlan, output: string, readFile: (path: string) => string | null = () => null): DesktopRunResult => {
|
|
234
335
|
const lines = output.split(/\r?\n/)
|
|
235
336
|
const doneAt = new Map<number, { at: number, error?: string }>()
|
|
236
337
|
const consoleLines: string[] = []
|
|
@@ -251,8 +352,8 @@ export const evaluateDesktopRun = (plan: DesktopPlan, output: string): DesktopRu
|
|
|
251
352
|
let ok = true
|
|
252
353
|
for (const st of plan.steps) {
|
|
253
354
|
const skipped = [
|
|
254
|
-
...st.skippedExpect.map((k) => `expect.${k} (
|
|
255
|
-
...st.skippedOutputs.map((k) => `${k} output (
|
|
355
|
+
...st.skippedExpect.map((k) => `expect.${k} (the headless audio recorder's — not on a native host)`),
|
|
356
|
+
...st.skippedOutputs.map((k) => `${k} output (the headless renderer's shape — use "dump" on a native host)`),
|
|
256
357
|
]
|
|
257
358
|
let error: string | undefined
|
|
258
359
|
let cutoff = consoleLines.length
|
|
@@ -262,6 +363,20 @@ export const evaluateDesktopRun = (plan: DesktopPlan, output: string): DesktopRu
|
|
|
262
363
|
if (d.error) { error = error ?? d.error; break }
|
|
263
364
|
cutoff = d.at
|
|
264
365
|
}
|
|
366
|
+
if (!error && (st.expectPaint !== undefined || st.expectSemantic !== undefined)) {
|
|
367
|
+
const text = st.paintDump ? readFile(st.paintDump) : null
|
|
368
|
+
if (text === null) error = "the host wrote no tree dump for the expect (an older host without the `dump` command?)"
|
|
369
|
+
else {
|
|
370
|
+
try {
|
|
371
|
+
const dump = parseTreeDump(text)
|
|
372
|
+
const failures = [
|
|
373
|
+
...(st.expectSemantic !== undefined ? evaluateDumpExpect(dump, st.expectSemantic) : []),
|
|
374
|
+
...(st.expectPaint !== undefined ? evaluatePaintExpect(dump, st.expectPaint) : []),
|
|
375
|
+
]
|
|
376
|
+
if (failures.length > 0) error = failures.join("; ")
|
|
377
|
+
} catch (e) { error = `expect: ${e instanceof Error ? e.message : String(e)}` }
|
|
378
|
+
}
|
|
379
|
+
}
|
|
265
380
|
if (!error && st.expectLog !== undefined) {
|
|
266
381
|
const seen = consoleLines.slice(0, cutoff)
|
|
267
382
|
if (!seen.some((l) => l.includes(st.expectLog!))) {
|
|
@@ -5,16 +5,20 @@ import { fileURLToPath } from "node:url"
|
|
|
5
5
|
/* Root of the CLI's on-disk companion files (runtime/ with the vendored SDK source + type bundle).
|
|
6
6
|
*
|
|
7
7
|
* Two distribution shapes:
|
|
8
|
-
* - npm package
|
|
9
|
-
*
|
|
8
|
+
* - npm package: the bundle is dist/index.js, companions at ../runtime.
|
|
9
|
+
* - monorepo dev: this module runs from src/hosts/, so the package root is two levels up. The
|
|
10
|
+
* package.json marker matters here — runtime/ is gitignored and absent until vendor-runtime runs,
|
|
11
|
+
* and callers also resolve siblings from the root (../../engines/gl).
|
|
10
12
|
* - standalone binary (`bun build --compile`, see scripts/build-bin.ts): import.meta.url points into
|
|
11
13
|
* the virtual bunfs archive which does NOT exist on disk, so the companions ship in the release
|
|
12
14
|
* archive NEXT TO the executable and are resolved via process.execPath. */
|
|
13
15
|
export const distRoot = (() => {
|
|
16
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
14
17
|
const candidates = [
|
|
15
|
-
join(
|
|
16
|
-
|
|
18
|
+
join(here, ".."), // npm package: dist/..
|
|
19
|
+
join(here, "..", ".."), // monorepo dev: src/hosts/../..
|
|
20
|
+
dirname(process.execPath), // standalone binary: next to the exe
|
|
17
21
|
]
|
|
18
|
-
for (const c of candidates) if (existsSync(join(c, "runtime"))) return c
|
|
22
|
+
for (const c of candidates) if (existsSync(join(c, "runtime")) || existsSync(join(c, "package.json"))) return c
|
|
19
23
|
return candidates[0]
|
|
20
24
|
})()
|
|
@@ -2,7 +2,7 @@ import { existsSync, readdirSync } from "node:fs"
|
|
|
2
2
|
import { homedir } from "node:os"
|
|
3
3
|
import { join, resolve } from "node:path"
|
|
4
4
|
import { downloadReleaseArchive, fetchReleaseProduct, newestForPlatform, semverNewer, type ManifestFile, type ManifestVersion } from "./releases"
|
|
5
|
-
import { CliError, note } from "
|
|
5
|
+
import { CliError, note } from "../cli"
|
|
6
6
|
|
|
7
7
|
/*
|
|
8
8
|
* Filament's matc (material compiler) — the tool behind LOCAL shader compile: `lecodes dev`
|
|
@@ -14,7 +14,7 @@ import { CliError, note } from "./util"
|
|
|
14
14
|
* releases/versions.json — published by scripts/release/release-windows.ps1 / release-ubuntu.sh
|
|
15
15
|
* from the desktop build's in-tree filament host tools, so it always matches the engines the
|
|
16
16
|
* hosts ship). Cache: ~/.lecodes/matc/matc-<version>/. LECODES_MATC overrides resolution entirely
|
|
17
|
-
* (local dev: point it at
|
|
17
|
+
* (local dev: point it at hosts/desktop/build/creator-gl/filament/tools/matc/matc.exe).
|
|
18
18
|
*
|
|
19
19
|
* No version pin: a filamat's compatibility is tied to the vendored Filament, which changes rarely —
|
|
20
20
|
* resolution takes the newest cached copy, else downloads the newest release PUBLISHED FOR THIS
|
|
@@ -2,13 +2,13 @@ import { spawnSync } from "node:child_process"
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
3
3
|
import { join } from "node:path"
|
|
4
4
|
import { pathToFileURL } from "node:url"
|
|
5
|
-
import pkg from "
|
|
5
|
+
import pkg from "../../package.json"
|
|
6
6
|
import { peerDirInPrefix, peersDir, peerVersionGlobal } from "./peers"
|
|
7
|
-
import { CliError, c, confirm, log, note } from "
|
|
7
|
+
import { CliError, c, confirm, log, note } from "../cli"
|
|
8
8
|
|
|
9
9
|
/*
|
|
10
10
|
* Peers install themselves on first use. `lecodes render` on a machine with only the CLI asks once,
|
|
11
|
-
* then puts lecodes-
|
|
11
|
+
* then puts lecodes-headless (+ @napi-rs/canvas for PNG) into ~/.lecodes/peers and loads it from
|
|
12
12
|
* there — the same pattern as matc and the desktop host (downloaded into ~/.lecodes on first use),
|
|
13
13
|
* applied to npm packages. Why a private prefix and not `npm install -g`: no sudo / system prefix
|
|
14
14
|
* to guess, and the runtime-set peer is installed at EXACTLY this CLI's version — the compiler and the
|
|
@@ -31,9 +31,9 @@ const readJson = (path: string): any => { try { return JSON.parse(readFileSync(p
|
|
|
31
31
|
/** The npm spec a peer installs at: the renderer at exactly this CLI's version (one ABI set), canvas
|
|
32
32
|
* at the range the installed renderer declares, the editors / assets at this CLI's peer ranges. */
|
|
33
33
|
export const peerSpec = (name: string): string => {
|
|
34
|
-
if (name === "lecodes-
|
|
34
|
+
if (name === "lecodes-headless") return `lecodes-headless@${pkg.version}`
|
|
35
35
|
if (name === "@napi-rs/canvas") {
|
|
36
|
-
const renderer = readJson(join(peerDirInPrefix("lecodes-
|
|
36
|
+
const renderer = readJson(join(peerDirInPrefix("lecodes-headless"), "package.json"))
|
|
37
37
|
const range = renderer?.peerDependencies?.["@napi-rs/canvas"] ?? renderer?.optionalDependencies?.["@napi-rs/canvas"] ?? CANVAS_RANGE_FALLBACK
|
|
38
38
|
return `@napi-rs/canvas@${range}`
|
|
39
39
|
}
|
|
@@ -42,7 +42,7 @@ export const peerSpec = (name: string): string => {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/** Peers that only make sense together: the renderer's PNG output needs canvas next to it. */
|
|
45
|
-
const companions = (name: string): string[] => name === "lecodes-
|
|
45
|
+
const companions = (name: string): string[] => name === "lecodes-headless" ? ["lecodes-headless", "@napi-rs/canvas"] : [name]
|
|
46
46
|
|
|
47
47
|
const onPath = (cmd: string): boolean => {
|
|
48
48
|
try { return spawnSync(cmd, ["--version"], { stdio: "ignore", shell: process.platform === "win32" }).status === 0 } catch { return false }
|
|
@@ -66,7 +66,7 @@ export const installPeers = async (names: string[], why: string, opts: { yes?: b
|
|
|
66
66
|
}, null, 2) + "\n")
|
|
67
67
|
}
|
|
68
68
|
// The renderer must be in place before canvas's range can be read off it — install in order.
|
|
69
|
-
const ordered = [...names].sort((a, b) => (a === "lecodes-
|
|
69
|
+
const ordered = [...names].sort((a, b) => (a === "lecodes-headless" ? -1 : b === "lecodes-headless" ? 1 : 0))
|
|
70
70
|
const specsNow = ordered.map(peerSpec)
|
|
71
71
|
|
|
72
72
|
const yes = opts.yes === true || !!process.env.LECODES_YES
|
|
@@ -10,7 +10,7 @@ import { dirname, join, resolve } from "node:path"
|
|
|
10
10
|
* peers (a project-local `-D` install is invisible to it).
|
|
11
11
|
* prefix ~/.lecodes/peers (LECODES_PEERS_DIR) — the CLI's own install dir, filled ON FIRST USE by
|
|
12
12
|
* peerInstall.ts when a command needs a peer that isn't there: no admin rights, and the
|
|
13
|
-
* runtime-set peer (lecodes-
|
|
13
|
+
* runtime-set peer (lecodes-headless) is pinned to THIS CLI's version, so the compiler and
|
|
14
14
|
* the headless host can't drift apart. `@napi-rs/canvas` (PNG output) rides along with it.
|
|
15
15
|
*
|
|
16
16
|
* Global wins when both exist (it is what the user chose by hand). `--version` reports each peer's
|
|
@@ -18,7 +18,7 @@ import { dirname, join, resolve } from "node:path"
|
|
|
18
18
|
* errors or a design screen rendering empty — is visible at a glance. The probe is an export subpath
|
|
19
19
|
* that resolves to a real file in BOTH the published package and the workspace source. */
|
|
20
20
|
export const OPTIONAL_PEERS: [name: string, probe: string][] = [
|
|
21
|
-
["lecodes-
|
|
21
|
+
["lecodes-headless", "lecodes-headless/headless"],
|
|
22
22
|
["lecodes-design", "lecodes-design/server"],
|
|
23
23
|
["lecodes-3d-editor", "lecodes-3d-editor/server"],
|
|
24
24
|
["lecodes-assets", "lecodes-assets/cli"],
|
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto"
|
|
|
2
2
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"
|
|
3
3
|
import { join } from "node:path"
|
|
4
4
|
import { spawnSync } from "node:child_process"
|
|
5
|
-
import { CliError } from "
|
|
5
|
+
import { CliError } from "../cli"
|
|
6
6
|
|
|
7
7
|
/*
|
|
8
8
|
* The S3 release channel the CLI downloads native tools from: the manifest scripts/release/*
|