lecodes-cli 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/index.js +1069 -396
- package/package.json +14 -13
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/assets/Roboto-Variable-DHm9jpd-.woff2 +0 -0
- package/runtime/web/assets/__vite-browser-external-BIHI7g3E.js +1 -0
- package/runtime/web/assets/basis-C64VHDVD.js +1 -0
- package/runtime/web/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
- package/runtime/web/assets/createViewerLite-BvHobVhp.js +852 -0
- package/runtime/web/assets/draco-BiISTFcR.js +118 -0
- package/runtime/web/assets/draco_decoder-DsQ12WqX.wasm +0 -0
- package/runtime/web/assets/index-CfNCCBjN.js +2 -0
- package/runtime/web/assets/mapViewImpl-B2JcES8l.js +810 -0
- package/runtime/web/assets/maplibre-gl-worker-CJfwIrte.js +8 -0
- package/runtime/web/assets/neutral_ibl128-D-lzdkLN.ktx +0 -0
- package/runtime/web/assets/worker-Caf-yYEI.js +2 -0
- package/runtime/web/embed.html +32 -0
- package/runtime/web/embed.js +171 -0
- package/src/api.ts +302 -0
- package/src/browserAuth.ts +87 -0
- package/src/cmgenTool.ts +104 -0
- package/src/commands/app.ts +896 -0
- package/src/commands/appAndroid.ts +603 -0
- package/src/commands/appDesktop.ts +339 -0
- package/src/commands/appDesktopMac.ts +470 -0
- package/src/commands/appIcon.ts +187 -0
- package/src/commands/appShared.ts +542 -0
- package/src/commands/appTemplates.ts +907 -0
- package/src/commands/appTemplatesAndroid.ts +599 -0
- package/src/commands/appTemplatesGradlew.ts +9 -0
- package/src/commands/assets.ts +28 -0
- package/src/commands/clone.ts +60 -0
- package/src/commands/compile.ts +253 -0
- package/src/commands/create.ts +59 -0
- package/src/commands/design.ts +863 -0
- package/src/commands/designTemplates.ts +11 -0
- package/src/commands/desktop.ts +127 -0
- package/src/commands/dev.ts +236 -0
- package/src/commands/diff.ts +59 -0
- package/src/commands/init.ts +191 -0
- package/src/commands/install.ts +198 -0
- package/src/commands/lightmap.ts +290 -0
- package/src/commands/link.ts +147 -0
- package/src/commands/login.ts +24 -0
- package/src/commands/navmesh.ts +225 -0
- package/src/commands/pn.ts +305 -0
- package/src/commands/projectTemplates.ts +130 -0
- package/src/commands/pull.ts +109 -0
- package/src/commands/push.ts +173 -0
- package/src/commands/render.ts +416 -0
- package/src/commands/scene.ts +148 -0
- package/src/commands/shaders.ts +157 -0
- package/src/commands/status.ts +29 -0
- package/src/commands/test.ts +371 -0
- package/src/commands/thumbs.ts +178 -0
- package/src/commands/types.ts +87 -0
- package/src/commands/update.ts +190 -0
- package/src/compile/assetIcons.ts +214 -0
- package/src/compile/collect.ts +46 -0
- package/src/compile/collectLocal.ts +37 -0
- package/src/compile/designCompile.ts +104 -0
- package/src/compile/fonts.ts +156 -0
- package/src/compile/headlessBundle.ts +129 -0
- package/src/compile/nativeStack.ts +21 -0
- package/src/compile/projectCompile.ts +109 -0
- package/src/compile/sceneCompile.ts +113 -0
- package/src/compile/screenEntry.ts +127 -0
- package/src/compile/shaders.ts +245 -0
- package/src/config.ts +42 -0
- package/src/designMeta.ts +35 -0
- package/src/desktopRenderer.ts +545 -0
- package/src/desktopScript.ts +276 -0
- package/src/dev/clientTemplates.ts +160 -0
- package/src/dev/devServer.ts +333 -0
- package/src/dev/webRunner.ts +22 -0
- package/src/dev/wsServer.ts +144 -0
- package/src/distRoot.ts +20 -0
- package/src/ignore.ts +163 -0
- package/src/index.ts +494 -0
- package/src/lecodes-3d-editor.d.ts +41 -0
- package/src/lecodes-assets.d.ts +7 -0
- package/src/lecodes-design.d.ts +191 -0
- package/src/lecodes-renderer.d.ts +131 -0
- package/src/localFiles.ts +144 -0
- package/src/manifest.ts +46 -0
- package/src/matcTool.ts +137 -0
- package/src/peers.ts +40 -0
- package/src/project.ts +61 -0
- package/src/projectEnv.ts +94 -0
- package/src/qrcode-terminal.d.ts +9 -0
- package/src/releases.ts +125 -0
- package/src/serverDiff.ts +78 -0
- package/src/textDiff.ts +103 -0
- package/src/types.ts +0 -0
- package/src/util.ts +146 -0
- package/runtime/sdk/animate/animate.ts +0 -238
- package/runtime/sdk/animate/bezier.ts +0 -138
- package/runtime/sdk/animate/easings.ts +0 -126
- package/runtime/sdk/canvas/Canvas.ts +0 -305
- package/runtime/sdk/compile/aspectMacro.ts +0 -86
- package/runtime/sdk/compile/assetIconMacro.ts +0 -384
- package/runtime/sdk/compile/assetMacro.ts +0 -146
- package/runtime/sdk/compile/assetName.ts +0 -50
- package/runtime/sdk/compile/bundler.ts +0 -287
- package/runtime/sdk/compile/compileProject.ts +0 -129
- package/runtime/sdk/compile/detectEntry.ts +0 -128
- package/runtime/sdk/compile/fontMacro.ts +0 -459
- package/runtime/sdk/compile/fontRegistry.ts +0 -78
- package/runtime/sdk/compile/header.ts +0 -67
- package/runtime/sdk/compile/index.ts +0 -100
- package/runtime/sdk/compile/libraryImports.ts +0 -52
- package/runtime/sdk/compile/liteMaterial.ts +0 -247
- package/runtime/sdk/compile/sceneEditor.ts +0 -88
- package/runtime/sdk/compile/serverSplit.ts +0 -233
- package/runtime/sdk/compile/serverTypes.ts +0 -227
- package/runtime/sdk/compile/sfnt.ts +0 -98
- package/runtime/sdk/compile/shaderSchema.ts +0 -202
- package/runtime/sdk/compile/shaderTargets.ts +0 -81
- package/runtime/sdk/compile/sourcemap.ts +0 -25
- package/runtime/sdk/core/Aspect.ts +0 -568
- package/runtime/sdk/core/InspectorUI.ts +0 -212
- package/runtime/sdk/core/StateMachine.ts +0 -308
- package/runtime/sdk/core/color.ts +0 -66
- package/runtime/sdk/core/compWrite.ts +0 -42
- package/runtime/sdk/core/events.ts +0 -38
- package/runtime/sdk/core/fields.ts +0 -120
- package/runtime/sdk/core/registry.ts +0 -23
- package/runtime/sdk/core/signals.ts +0 -277
- package/runtime/sdk/core/time.ts +0 -84
- package/runtime/sdk/g2/Camera2D.ts +0 -40
- package/runtime/sdk/g2/CharacterController2D.ts +0 -276
- package/runtime/sdk/g2/Node2D.ts +0 -267
- package/runtime/sdk/g2/OneWay2D.ts +0 -66
- package/runtime/sdk/g2/Physics2D.ts +0 -346
- package/runtime/sdk/g2/Scene2D.ts +0 -209
- package/runtime/sdk/g2/Shape2D.ts +0 -259
- package/runtime/sdk/g2/Sprite.ts +0 -89
- package/runtime/sdk/g2/SpriteAnimation.ts +0 -171
- package/runtime/sdk/g2/SpriteSheet.ts +0 -166
- package/runtime/sdk/g2/Texture2D.ts +0 -47
- package/runtime/sdk/g2/Tilemap.ts +0 -41
- package/runtime/sdk/g2/Tileset.ts +0 -71
- package/runtime/sdk/g2/Trigger2D.ts +0 -77
- package/runtime/sdk/g2/autotile.ts +0 -433
- package/runtime/sdk/g2/cells.ts +0 -91
- package/runtime/sdk/g2/defineScene2d.ts +0 -381
- package/runtime/sdk/g2/groups2d.ts +0 -106
- package/runtime/sdk/g2/loop.ts +0 -50
- package/runtime/sdk/g2/scenarios2d.ts +0 -69
- package/runtime/sdk/g2/touch.ts +0 -83
- package/runtime/sdk/gl/Camera.ts +0 -160
- package/runtime/sdk/gl/CameraPlace.ts +0 -52
- package/runtime/sdk/gl/CharacterController.ts +0 -238
- package/runtime/sdk/gl/Gearbox.ts +0 -212
- package/runtime/sdk/gl/Geometry.ts +0 -279
- package/runtime/sdk/gl/IK.ts +0 -193
- package/runtime/sdk/gl/InstancedMesh.ts +0 -132
- package/runtime/sdk/gl/Light.ts +0 -107
- package/runtime/sdk/gl/Lightmap.ts +0 -256
- package/runtime/sdk/gl/Material.ts +0 -260
- package/runtime/sdk/gl/Mesh.ts +0 -83
- package/runtime/sdk/gl/Model.ts +0 -124
- package/runtime/sdk/gl/NavAgent.ts +0 -337
- package/runtime/sdk/gl/NavMesh.ts +0 -397
- package/runtime/sdk/gl/Node.ts +0 -350
- package/runtime/sdk/gl/Noise.ts +0 -30
- package/runtime/sdk/gl/Particles.ts +0 -676
- package/runtime/sdk/gl/Physics.ts +0 -222
- package/runtime/sdk/gl/Plane.ts +0 -53
- package/runtime/sdk/gl/Ragdoll.ts +0 -270
- package/runtime/sdk/gl/Ray.ts +0 -16
- package/runtime/sdk/gl/Scene.ts +0 -486
- package/runtime/sdk/gl/Shape.ts +0 -409
- package/runtime/sdk/gl/Terrain.ts +0 -1092
- package/runtime/sdk/gl/Texture.ts +0 -63
- package/runtime/sdk/gl/Trigger.ts +0 -45
- package/runtime/sdk/gl/Vehicle.ts +0 -473
- package/runtime/sdk/gl/Wheel.ts +0 -240
- package/runtime/sdk/gl/animation/AnimationClip.ts +0 -204
- package/runtime/sdk/gl/animation/Animator.ts +0 -120
- package/runtime/sdk/gl/animation/Layer.ts +0 -31
- package/runtime/sdk/gl/animation/Loop.ts +0 -30
- package/runtime/sdk/gl/animation/Playback.ts +0 -44
- package/runtime/sdk/gl/animation/core.ts +0 -336
- package/runtime/sdk/gl/controls.ts +0 -95
- package/runtime/sdk/gl/physicsEvents.ts +0 -20
- package/runtime/sdk/gl/scenarios.ts +0 -291
- package/runtime/sdk/gl/state.ts +0 -6
- package/runtime/sdk/gl/terrainMesh.ts +0 -219
- package/runtime/sdk/gl/touch.ts +0 -68
- package/runtime/sdk/inject.ts +0 -197
- package/runtime/sdk/math/Mathf.ts +0 -118
- package/runtime/sdk/math/mat4.ts +0 -278
- package/runtime/sdk/math/quat.ts +0 -232
- package/runtime/sdk/math/vec.ts +0 -255
- package/runtime/sdk/net/codec.ts +0 -119
- package/runtime/sdk/net/core.ts +0 -384
- package/runtime/sdk/net/index.ts +0 -181
- package/runtime/sdk/net/replication.ts +0 -622
- package/runtime/sdk/plugins/camera.ts +0 -81
- package/runtime/sdk/plugins/geolocation.ts +0 -123
- package/runtime/sdk/plugins/oauth.ts +0 -61
- package/runtime/sdk/plugins/permission.ts +0 -7
- package/runtime/sdk/plugins/push.ts +0 -132
- package/runtime/sdk/plugins/qr.ts +0 -73
- package/runtime/sdk/plugins/service.ts +0 -47
- package/runtime/sdk/runtime/app.ts +0 -101
- package/runtime/sdk/runtime/appEvents.ts +0 -54
- package/runtime/sdk/runtime/channel.ts +0 -61
- package/runtime/sdk/runtime/clipboard.ts +0 -20
- package/runtime/sdk/runtime/datetime.ts +0 -329
- package/runtime/sdk/runtime/device.ts +0 -293
- package/runtime/sdk/runtime/fetch.ts +0 -77
- package/runtime/sdk/runtime/files.ts +0 -23
- package/runtime/sdk/runtime/input.ts +0 -175
- package/runtime/sdk/runtime/media.ts +0 -111
- package/runtime/sdk/runtime/misc.ts +0 -16
- package/runtime/sdk/runtime/net.ts +0 -36
- package/runtime/sdk/runtime/rpc.ts +0 -218
- package/runtime/sdk/runtime/service.ts +0 -83
- package/runtime/sdk/runtime/share.ts +0 -9
- package/runtime/sdk/runtime/storage.ts +0 -13
- package/runtime/sdk/runtime/touch.ts +0 -76
- package/runtime/sdk/scene/defineScene.ts +0 -1353
- package/runtime/sdk/scene/editorPlugins.ts +0 -110
- package/runtime/sdk/scene/gizmos.ts +0 -148
- package/runtime/sdk/scene/grammar.ts +0 -120
- package/runtime/sdk/scene/material.ts +0 -188
- package/runtime/sdk/server/auth/appConfig.ts +0 -12
- package/runtime/sdk/server/auth/global.ts +0 -80
- package/runtime/sdk/server/auth/host.ts +0 -318
- package/runtime/sdk/server/auth/models.ts +0 -83
- package/runtime/sdk/server/auth/types.ts +0 -50
- package/runtime/sdk/server/channel.ts +0 -56
- package/runtime/sdk/server/context.ts +0 -36
- package/runtime/sdk/server/db/defineDb.ts +0 -237
- package/runtime/sdk/server/db/fields.ts +0 -132
- package/runtime/sdk/server/db/httpTransport.ts +0 -93
- package/runtime/sdk/server/db/index.ts +0 -7
- package/runtime/sdk/server/db/marci/query.ts +0 -412
- package/runtime/sdk/server/db/types.ts +0 -202
- package/runtime/sdk/server/errors.ts +0 -12
- package/runtime/sdk/server/host.ts +0 -74
- package/runtime/sdk/server/inject.ts +0 -13
- package/runtime/sdk/server/runtime.ts +0 -133
- package/runtime/sdk/server/validate.ts +0 -87
- package/runtime/sdk/ui/NativeView.ts +0 -142
- package/runtime/sdk/ui/UI.ts +0 -39
- package/runtime/sdk/ui/UIBottomSheet.ts +0 -139
- package/runtime/sdk/ui/UIButton.ts +0 -101
- package/runtime/sdk/ui/UIContainer.ts +0 -60
- package/runtime/sdk/ui/UIImage.ts +0 -83
- package/runtime/sdk/ui/UIInput.ts +0 -185
- package/runtime/sdk/ui/UIModal.ts +0 -139
- package/runtime/sdk/ui/UINode.ts +0 -830
- package/runtime/sdk/ui/UIPager.ts +0 -362
- package/runtime/sdk/ui/UIPopover.ts +0 -100
- package/runtime/sdk/ui/UIScreen.ts +0 -123
- package/runtime/sdk/ui/UIScrollable.ts +0 -87
- package/runtime/sdk/ui/UISpacer.ts +0 -14
- package/runtime/sdk/ui/UITabs.ts +0 -236
- package/runtime/sdk/ui/UIText.ts +0 -51
- package/runtime/sdk/ui/UIVideo.ts +0 -88
- package/runtime/sdk/ui/UIVirtualizedList.ts +0 -241
- package/runtime/sdk/ui/UIWidget.ts +0 -127
- package/runtime/sdk/ui/fonts.ts +0 -13
- package/runtime/sdk/ui/presentable.ts +0 -117
- package/runtime/sdk/ui/router.ts +0 -132
- package/runtime/sdk/ui/theme.ts +0 -84
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { resolve } from "node:path"
|
|
2
|
+
import { CliError } from "./util"
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* `lecodes render --desktop --script`: the same JSON scenario a headless run takes, driven through
|
|
6
|
+
* the NATIVE desktop host. The host has no semantic UI tree, so it can't resolve names or read text —
|
|
7
|
+
* it takes a line-based command file (CREATOR_SCRIPT, see packages/desktop/src/main.cpp `scriptStep`)
|
|
8
|
+
* with COORDINATES, and reports `[script] #<line> done <op>` on stdout as each command completes.
|
|
9
|
+
* This module does both halves: translate the scenario into that file (refusing, with guidance, the
|
|
10
|
+
* steps the host can't honour) and turn the host's stdout back into per-step results — `expect.log`
|
|
11
|
+
* is evaluated here against the console lines seen up to each step's marker; the semantic expects
|
|
12
|
+
* (`screen`/`node`/`text`/`value`) are reported as skipped, never silently passed.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
type Point = { x: number, y: number }
|
|
16
|
+
const isPoint = (v: unknown): v is Point =>
|
|
17
|
+
typeof v === "object" && v !== null && typeof (v as Point).x === "number" && typeof (v as Point).y === "number"
|
|
18
|
+
|
|
19
|
+
const FRAME_MS = 16
|
|
20
|
+
|
|
21
|
+
export type DesktopPlanStep = {
|
|
22
|
+
index: number
|
|
23
|
+
desc: string
|
|
24
|
+
/** Script line numbers this step emitted; the LAST one's `done` marker completes the step. */
|
|
25
|
+
lines: number[]
|
|
26
|
+
expectLog?: string
|
|
27
|
+
/** Expect keys the native host can't evaluate (no semantic tree) — reported as skipped. */
|
|
28
|
+
skippedExpect: string[]
|
|
29
|
+
/** Outputs the native host can't produce (`json`). */
|
|
30
|
+
skippedOutputs: string[]
|
|
31
|
+
screenshot?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type DesktopPlan = {
|
|
35
|
+
/** The CREATOR_SCRIPT file contents. */
|
|
36
|
+
text: string
|
|
37
|
+
steps: DesktopPlanStep[]
|
|
38
|
+
/** A safety cap for CREATOR_MAX_FRAMES: the script's own time in frames plus slack. */
|
|
39
|
+
frameCap: number
|
|
40
|
+
}
|
|
41
|
+
|
|
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"]
|
|
44
|
+
const VIEW_NAMES = ["iso", "top", "front", "side", "back"]
|
|
45
|
+
const isVec3 = (v: unknown): v is [number, number, number] => Array.isArray(v) && v.length === 3 && v.every((n) => typeof n === "number")
|
|
46
|
+
|
|
47
|
+
const pt = (p: Point) => `(${p.x},${p.y})`
|
|
48
|
+
|
|
49
|
+
/** 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`. Throws a CliError for steps the
|
|
51
|
+
* native host can't honour (named targets, typing, scrolling, clips). */
|
|
52
|
+
export const translateScenario = (rawSteps: unknown, opts: { outDir: string, headroomFrames: number }): DesktopPlan => {
|
|
53
|
+
if (!Array.isArray(rawSteps)) throw new CliError(`"steps" must be an array of step objects.`)
|
|
54
|
+
const lines: string[] = []
|
|
55
|
+
const steps: DesktopPlanStep[] = []
|
|
56
|
+
let budgetMs = 0
|
|
57
|
+
const emit = (line: string): number => { lines.push(line); return lines.length }
|
|
58
|
+
emit(`# generated by lecodes render --desktop --script`)
|
|
59
|
+
emit(`frames ${Math.max(0, opts.headroomFrames)}`)
|
|
60
|
+
|
|
61
|
+
const coord = (at: string, key: string, v: unknown): Point => {
|
|
62
|
+
if (isPoint(v)) return v
|
|
63
|
+
if (typeof v === "string") {
|
|
64
|
+
throw new CliError(`${at}: "${key}": "${v}" — the native host can't resolve names; give --desktop scripts { "x", "y" } coordinates (logical px), or run the scenario headless (without --desktop).`)
|
|
65
|
+
}
|
|
66
|
+
throw new CliError(`${at}: "${key}" must be { "x", "y" }.`)
|
|
67
|
+
}
|
|
68
|
+
const num = (at: string, key: string, v: unknown, def?: number): number => {
|
|
69
|
+
if (v === undefined && def !== undefined) return def
|
|
70
|
+
if (typeof v !== "number") throw new CliError(`${at}: "${key}" must be a number.`)
|
|
71
|
+
return v
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// While an inspection camera is active, the app's raycasts read ITS camera, not what the shot shows —
|
|
75
|
+
// a tap/drag there would silently hit the wrong thing, so pointer steps are refused until `"off"`.
|
|
76
|
+
let inspecting = false
|
|
77
|
+
rawSteps.forEach((step, i) => {
|
|
78
|
+
const at = `steps[${i}]`
|
|
79
|
+
if (typeof step !== "object" || step === null || Array.isArray(step)) throw new CliError(`${at}: must be an object.`)
|
|
80
|
+
const s = step as Record<string, unknown>
|
|
81
|
+
const actions = ACTIONS.filter((k) => s[k] !== undefined)
|
|
82
|
+
if (actions.length > 1) throw new CliError(`${at}: one action per step (found ${actions.join(" + ")}).`)
|
|
83
|
+
const plan: DesktopPlanStep = { index: i, desc: "", lines: [], skippedExpect: [], skippedOutputs: [] }
|
|
84
|
+
const parts: string[] = []
|
|
85
|
+
const action = actions[0]
|
|
86
|
+
|
|
87
|
+
if (action === "type" || action === "fill" || action === "scroll") {
|
|
88
|
+
throw new CliError(`${at}: "${action}" isn't available with --desktop (the native host has no semantic tree / soft keyboard) — run this scenario headless, or drive the gesture with "drag" coordinates.`)
|
|
89
|
+
}
|
|
90
|
+
if ((action === "tap" || action === "drag" || action === "hold") && inspecting) {
|
|
91
|
+
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
|
+
}
|
|
93
|
+
if (action === "view") {
|
|
94
|
+
const v = s.view
|
|
95
|
+
const fov = s.fov !== undefined ? num(at, "fov", s.fov) : undefined
|
|
96
|
+
if (v === "off") { plan.lines.push(emit("view off")); inspecting = false; parts.push("view off") }
|
|
97
|
+
else if (typeof v === "string" && VIEW_NAMES.includes(v)) {
|
|
98
|
+
plan.lines.push(emit(`view ${v}${fov !== undefined ? ` ${fov}` : ""}`)); inspecting = true
|
|
99
|
+
parts.push(`view ${v}${fov !== undefined ? ` (fov ${fov})` : ""}`)
|
|
100
|
+
} else throw new CliError(`${at}: "view" wants one of ${VIEW_NAMES.join(", ")} or "off".`)
|
|
101
|
+
budgetMs += 2 * FRAME_MS
|
|
102
|
+
} else if (action === "camera") {
|
|
103
|
+
const cam = s.camera
|
|
104
|
+
if (cam === "off") { plan.lines.push(emit("camera off")); inspecting = false; parts.push("camera off") }
|
|
105
|
+
else if (typeof cam === "object" && cam !== null && isVec3((cam as any).at) && isVec3((cam as any).lookAt)) {
|
|
106
|
+
const { at: eye, lookAt, fov } = cam as { at: number[], lookAt: number[], fov?: unknown }
|
|
107
|
+
const fovN = fov !== undefined ? num(at, "camera.fov", fov) : undefined
|
|
108
|
+
plan.lines.push(emit(`camera ${eye.join(" ")} ${lookAt.join(" ")}${fovN !== undefined ? ` ${fovN}` : ""}`)); inspecting = true
|
|
109
|
+
parts.push(`camera at (${eye.join(",")}) → (${lookAt.join(",")})`)
|
|
110
|
+
} else throw new CliError(`${at}: "camera" wants { "at": [x,y,z], "lookAt": [x,y,z], "fov"? } or "off".`)
|
|
111
|
+
budgetMs += 2 * FRAME_MS
|
|
112
|
+
}
|
|
113
|
+
if (s.fov !== undefined && action !== "view") throw new CliError(`${at}: "fov" only goes with "view" (inside "camera" use camera.fov).`)
|
|
114
|
+
if (action === "tap") {
|
|
115
|
+
const p = coord(at, "tap", s.tap)
|
|
116
|
+
plan.lines.push(emit(`tap ${p.x} ${p.y}`)); budgetMs += 4 * FRAME_MS
|
|
117
|
+
parts.push(`tap ${pt(p)}`)
|
|
118
|
+
} else if (action === "drag") {
|
|
119
|
+
const a = coord(at, "drag", s.drag), b = coord(at, "to", s.to)
|
|
120
|
+
const ms = num(at, "ms", s.ms, 300)
|
|
121
|
+
plan.lines.push(emit(`drag ${a.x} ${a.y} ${b.x} ${b.y} ${Math.round(ms)}`)); budgetMs += ms + 4 * FRAME_MS
|
|
122
|
+
parts.push(`drag ${pt(a)} → ${pt(b)} over ${ms}ms`)
|
|
123
|
+
} else if (action === "hold") {
|
|
124
|
+
const p = coord(at, "hold", s.hold)
|
|
125
|
+
const ms = num(at, "ms", s.ms)
|
|
126
|
+
plan.lines.push(emit(`hold ${p.x} ${p.y} ${Math.round(ms)}`)); budgetMs += ms + 4 * FRAME_MS
|
|
127
|
+
parts.push(`hold ${pt(p)} ${ms}ms`)
|
|
128
|
+
} else if (action === "key") {
|
|
129
|
+
if (typeof s.key !== "string") throw new CliError(`${at}: "key" must be a KeyboardEvent.code string.`)
|
|
130
|
+
const ms = num(at, "ms", s.ms)
|
|
131
|
+
plan.lines.push(emit(`key ${s.key} ${Math.round(ms)}`)); budgetMs += ms + 2 * FRAME_MS
|
|
132
|
+
parts.push(`key ${s.key} ${ms}ms`)
|
|
133
|
+
} else if (action === "keyDown" || action === "keyUp") {
|
|
134
|
+
const code = s[action]
|
|
135
|
+
if (typeof code !== "string") throw new CliError(`${at}: "${action}" must be a code string.`)
|
|
136
|
+
plan.lines.push(emit(`${action.toLowerCase()} ${code}`)); budgetMs += 2 * FRAME_MS
|
|
137
|
+
parts.push(`${action} ${code}`)
|
|
138
|
+
} else if (action === "look") {
|
|
139
|
+
const v = s.look
|
|
140
|
+
if (!Array.isArray(v) || v.length !== 2 || !v.every((n: unknown) => typeof n === "number")) throw new CliError(`${at}: "look" must be [dx, dy] (logical px of mouse motion).`)
|
|
141
|
+
const ms = num(at, "ms", s.ms, FRAME_MS)
|
|
142
|
+
plan.lines.push(emit(`look ${v[0]} ${v[1]} ${Math.round(ms)}`)); budgetMs += ms + 2 * FRAME_MS
|
|
143
|
+
parts.push(`look ${v[0]},${v[1]} over ${ms}ms`)
|
|
144
|
+
} else if (action === "mouseTo") {
|
|
145
|
+
const v = s.mouseTo
|
|
146
|
+
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
|
|
148
|
+
parts.push(`mouseTo ${v[0]},${v[1]}`)
|
|
149
|
+
} else if (action === "wheel") {
|
|
150
|
+
const v = s.wheel
|
|
151
|
+
if (!Array.isArray(v) || v.length !== 2 || !v.every((n: unknown) => typeof n === "number")) throw new CliError(`${at}: "wheel" must be [dx, dy] (notches).`)
|
|
152
|
+
plan.lines.push(emit(`wheel ${v[0]} ${v[1]}`)); budgetMs += 2 * FRAME_MS
|
|
153
|
+
parts.push(`wheel ${v[0]},${v[1]}`)
|
|
154
|
+
} else if (action === "stick") {
|
|
155
|
+
const st = s.stick
|
|
156
|
+
if (typeof st !== "object" || st === null) throw new CliError(`${at}: "stick" must be { left?: [x, y], right?: [x, y], triggers?: [l, r] }.`)
|
|
157
|
+
const pad = typeof s.index === "number" ? s.index : 0
|
|
158
|
+
const parts2: string[] = []
|
|
159
|
+
for (const k of ["left", "right", "triggers"] as const) {
|
|
160
|
+
const v = (st as any)[k]
|
|
161
|
+
if (v === undefined) continue
|
|
162
|
+
if (!Array.isArray(v) || v.length !== 2 || !v.every((n: unknown) => typeof n === "number")) throw new CliError(`${at}: "stick.${k}" must be a pair of numbers.`)
|
|
163
|
+
plan.lines.push(emit(`stick ${pad} ${k} ${v[0]} ${v[1]}`)); parts2.push(`${k}=${v[0]},${v[1]}`)
|
|
164
|
+
}
|
|
165
|
+
for (const k of Object.keys(st)) if (!["left", "right", "triggers"].includes(k)) throw new CliError(`${at}: "stick" knows left, right, triggers — not "${k}".`)
|
|
166
|
+
budgetMs += 2 * FRAME_MS
|
|
167
|
+
parts.push(`stick #${pad} ${parts2.join(" ")}`)
|
|
168
|
+
} else if (action === "gamepad") {
|
|
169
|
+
if (s.gamepad !== "connect" && s.gamepad !== "disconnect") throw new CliError(`${at}: "gamepad" must be "connect" or "disconnect".`)
|
|
170
|
+
const pad = typeof s.index === "number" ? s.index : 0
|
|
171
|
+
plan.lines.push(emit(`gamepad ${pad} ${s.gamepad}`)); budgetMs += 2 * FRAME_MS
|
|
172
|
+
parts.push(`gamepad ${s.gamepad} #${pad}`)
|
|
173
|
+
} else if (action === "wait") {
|
|
174
|
+
const ms = num(at, "wait", s.wait)
|
|
175
|
+
plan.lines.push(emit(`wait ${Math.round(ms)}`)); budgetMs += ms + FRAME_MS
|
|
176
|
+
parts.push(`wait ${ms}ms`)
|
|
177
|
+
} else if (action === "frames") {
|
|
178
|
+
const n = num(at, "frames", s.frames)
|
|
179
|
+
plan.lines.push(emit(`frames ${Math.round(n)}`)); budgetMs += n * FRAME_MS + FRAME_MS
|
|
180
|
+
parts.push(`advance ${n} frames`)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (s.expect !== undefined) {
|
|
184
|
+
if (typeof s.expect !== "object" || s.expect === null) throw new CliError(`${at}: "expect" must be an object.`)
|
|
185
|
+
const e = s.expect as Record<string, unknown>
|
|
186
|
+
const bits: string[] = []
|
|
187
|
+
for (const k of Object.keys(e)) {
|
|
188
|
+
if (k === "log") {
|
|
189
|
+
if (typeof e.log !== "string") throw new CliError(`${at}: expect.log must be a string.`)
|
|
190
|
+
plan.expectLog = e.log
|
|
191
|
+
bits.push(`log~"${e.log}"`)
|
|
192
|
+
} else if (SEMANTIC_EXPECT.includes(k)) {
|
|
193
|
+
plan.skippedExpect.push(k)
|
|
194
|
+
} else {
|
|
195
|
+
throw new CliError(`${at}: unknown expect key "${k}".`)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (bits.length > 0) parts.push(`expect ${bits.join(" · ")}`)
|
|
199
|
+
}
|
|
200
|
+
if (s.clip !== undefined) throw new CliError(`${at}: "clip" isn't available with --desktop (the native host dumps the whole window).`)
|
|
201
|
+
if (s.screenshot !== undefined) {
|
|
202
|
+
if (typeof s.screenshot !== "string") throw new CliError(`${at}: "screenshot" must be a path.`)
|
|
203
|
+
plan.screenshot = resolve(opts.outDir, s.screenshot)
|
|
204
|
+
// The shot lands AFTER the action settles (two frames), like the headless act → capture order.
|
|
205
|
+
plan.lines.push(emit(`frames 2`))
|
|
206
|
+
plan.lines.push(emit(`shot ${plan.screenshot}`)); budgetMs += 3 * FRAME_MS
|
|
207
|
+
parts.push(`→ ${s.screenshot}`)
|
|
208
|
+
}
|
|
209
|
+
if (s.json !== undefined) plan.skippedOutputs.push("json")
|
|
210
|
+
if (plan.lines.length === 0) {
|
|
211
|
+
// An expect-only step still needs a marker to anchor its evaluation.
|
|
212
|
+
plan.lines.push(emit(`frames 1`)); budgetMs += FRAME_MS
|
|
213
|
+
}
|
|
214
|
+
plan.desc = typeof s.name === "string" ? s.name : (parts.join(", ") || "(empty step)")
|
|
215
|
+
steps.push(plan)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
const frameCap = Math.max(0, opts.headroomFrames) + Math.ceil(budgetMs / FRAME_MS) + 600
|
|
219
|
+
return { text: lines.join("\n") + "\n", steps, frameCap }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export type DesktopStepResult = { desc: string, ok: boolean, error?: string, skipped: string[] }
|
|
223
|
+
export type DesktopRunResult = {
|
|
224
|
+
/** The host understood CREATOR_SCRIPT at all (an older host ignores it and just renders). */
|
|
225
|
+
loaded: boolean
|
|
226
|
+
ended: boolean
|
|
227
|
+
ok: boolean
|
|
228
|
+
steps: DesktopStepResult[]
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Turn the host's stdout into per-step results. Console lines are every non-`[script]` line the
|
|
232
|
+
* 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 => {
|
|
234
|
+
const lines = output.split(/\r?\n/)
|
|
235
|
+
const doneAt = new Map<number, { at: number, error?: string }>()
|
|
236
|
+
const consoleLines: string[] = []
|
|
237
|
+
let loaded = false, ended = false
|
|
238
|
+
for (const line of lines) {
|
|
239
|
+
const m = /^\[script\] #(\d+) (done|error) (\S+)(?:: (.*))?$/.exec(line)
|
|
240
|
+
if (m) {
|
|
241
|
+
doneAt.set(Number(m[1]), { at: consoleLines.length, error: m[2] === "error" ? `${m[3]}: ${m[4] ?? "failed"}` : undefined })
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
if (line.startsWith("[script] loaded")) { loaded = true; continue }
|
|
245
|
+
if (line === "[script] end") { ended = true; continue }
|
|
246
|
+
if (line.startsWith("[script]")) continue
|
|
247
|
+
consoleLines.push(line)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const steps: DesktopStepResult[] = []
|
|
251
|
+
let ok = true
|
|
252
|
+
for (const st of plan.steps) {
|
|
253
|
+
const skipped = [
|
|
254
|
+
...st.skippedExpect.map((k) => `expect.${k} (no semantic tree on --desktop)`),
|
|
255
|
+
...st.skippedOutputs.map((k) => `${k} output (no semantic tree on --desktop)`),
|
|
256
|
+
]
|
|
257
|
+
let error: string | undefined
|
|
258
|
+
let cutoff = consoleLines.length
|
|
259
|
+
for (const ln of st.lines) {
|
|
260
|
+
const d = doneAt.get(ln)
|
|
261
|
+
if (!d) { error = error ?? "the host never completed this step (crashed, closed, or hit the frame cap before it)"; break }
|
|
262
|
+
if (d.error) { error = error ?? d.error; break }
|
|
263
|
+
cutoff = d.at
|
|
264
|
+
}
|
|
265
|
+
if (!error && st.expectLog !== undefined) {
|
|
266
|
+
const seen = consoleLines.slice(0, cutoff)
|
|
267
|
+
if (!seen.some((l) => l.includes(st.expectLog!))) {
|
|
268
|
+
const tail = seen.filter((l) => l.startsWith("[DEBUG]") || l.startsWith("[INFO]") || l.startsWith("[WARN]") || l.startsWith("[ERROR]")).slice(-8)
|
|
269
|
+
error = `expected a console line containing "${st.expectLog}" — ${tail.length === 0 ? "the app logged nothing" : `last lines: ${tail.map((l) => `"${l}"`).join(", ")}`}`
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (error) ok = false
|
|
273
|
+
steps.push({ desc: st.desc, ok: !error, error, skipped })
|
|
274
|
+
}
|
|
275
|
+
return { loaded, ended, ok: ok && loaded, steps }
|
|
276
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The on-device dev client for `lecodes dev`, shipped FROM the CLI (updatable without app
|
|
3
|
+
* releases). Two pieces:
|
|
4
|
+
*
|
|
5
|
+
* - DEV_SCREEN_TS — a tiny placeholder project (globals-only, like any user project) compiled
|
|
6
|
+
* through the normal pipeline into `dev.js`, the URL the QR code points at. Its auto-generated
|
|
7
|
+
* header says `// gl: disable`, so the app launcher creates no engine for it.
|
|
8
|
+
*
|
|
9
|
+
* - DEV_PRELUDE_JS — plain JS appended VERBATIM to every bundle the dev server serves (dev.js and
|
|
10
|
+
* bundle.js alike). It only touches the raw `_creatorUtils` / `_creator` bridge — no SDK — so it
|
|
11
|
+
* can ride along any compiled project without duplicating the injected runtime. Each
|
|
12
|
+
* `_creatorUtils.run()` world therefore carries its own copy: the running world holds the live
|
|
13
|
+
* connection; when it loads the next bundle, the NEW world's copy connects and the native
|
|
14
|
+
* per-world socket teardown closes the old one. On eval error, run()'s rollback keeps the
|
|
15
|
+
* current world (and its connection) alive, so the reject callback here reports + keeps serving.
|
|
16
|
+
*
|
|
17
|
+
* Placeholders (substituted by the dev server, see renderPrelude):
|
|
18
|
+
* __DEV_ORIGIN__ http://<ip>:<port>
|
|
19
|
+
* __DEV_BASE__ /t/<token>
|
|
20
|
+
* __DEV_TRANSPORT__ "ws" | "poll" (poll = hosts without a websocket bridge, e.g. the desktop host)
|
|
21
|
+
* __DEV_V__ the bundle version this world runs (-1 for dev.js → always load latest)
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export const DEV_SCREEN_TS = `const title = "LeCodes Dev"
|
|
25
|
+
const project = __DEV_NAME__
|
|
26
|
+
|
|
27
|
+
UIScreen([
|
|
28
|
+
UIText(title).style({ fontSize: 30, fontWeight: 700, color: "#ffffff" }),
|
|
29
|
+
UIText(project).style({ fontSize: 16, color: "#9fb0d0", mt: 8 }),
|
|
30
|
+
UIText("Connected — loading the project…").style({ fontSize: 14, color: "#6b7a99", mt: 20 }),
|
|
31
|
+
]).style({ bgColor: "#11131a", justifyContent: "center", alignItems: "center", p: 40 }).open()
|
|
32
|
+
`
|
|
33
|
+
|
|
34
|
+
export const DEV_PRELUDE_JS = `
|
|
35
|
+
;(function () {
|
|
36
|
+
var ORIGIN = "__DEV_ORIGIN__"
|
|
37
|
+
var BASE = "__DEV_BASE__"
|
|
38
|
+
var TRANSPORT = "__DEV_TRANSPORT__"
|
|
39
|
+
var V = __DEV_V__
|
|
40
|
+
var wsId = null
|
|
41
|
+
var busy = false
|
|
42
|
+
var queued = null
|
|
43
|
+
|
|
44
|
+
function post(obj) {
|
|
45
|
+
var text = JSON.stringify(obj)
|
|
46
|
+
if (TRANSPORT === "ws") {
|
|
47
|
+
if (wsId !== null) { try { _creatorUtils.websocketSend(wsId, text) } catch (e) {} }
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
_creatorUtils.fetch(ORIGIN + BASE + "/log", { method: "POST", body: text, headers: { "content-type": "application/json" } },
|
|
52
|
+
function (id) { try { _creatorUtils.disposeFetch(id) } catch (e) {} }, function () {})
|
|
53
|
+
} catch (e) {}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// An Error carries its stack (JSON.stringify would send a useless "{}"); the dev server maps the
|
|
57
|
+
// frames back to project positions before printing.
|
|
58
|
+
function describe(a) {
|
|
59
|
+
if (typeof a === "string") return a
|
|
60
|
+
if (a && typeof a.stack === "string" && typeof a.message === "string") {
|
|
61
|
+
return a.stack ? String(a) + "\\n" + a.stack : String(a)
|
|
62
|
+
}
|
|
63
|
+
try { return JSON.stringify(a) } catch (e) { return String(a) }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Mirror console output to the dev terminal ('silent' guards recursion through wrapped calls).
|
|
67
|
+
var silent = false
|
|
68
|
+
;["log", "warn", "error"].forEach(function (level) {
|
|
69
|
+
var orig = console[level]
|
|
70
|
+
console[level] = function () {
|
|
71
|
+
var parts = []
|
|
72
|
+
for (var i = 0; i < arguments.length; i++) parts.push(describe(arguments[i]))
|
|
73
|
+
try { orig.apply(console, arguments) } catch (e) {}
|
|
74
|
+
if (!silent) { silent = true; try { post({ type: "log", level: level, text: parts.join(" ") }) } finally { silent = false } }
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
function report(err) { post({ type: "error", text: describe(err) }) }
|
|
79
|
+
|
|
80
|
+
function load(v) {
|
|
81
|
+
if (busy) { queued = v; return }
|
|
82
|
+
busy = true
|
|
83
|
+
_creatorUtils.fetch(ORIGIN + BASE + "/bundle.js?v=" + v + "&t=" + TRANSPORT, {}, function (id, status) {
|
|
84
|
+
if (status >= 400) { busy = false; report("bundle fetch: HTTP " + status); return }
|
|
85
|
+
// Same engine bringup the app launcher does for a fresh launch (see desktop initial.ts).
|
|
86
|
+
var head = ""
|
|
87
|
+
try { head = _creatorUtils.fetchSlice(id, 0, 300) } catch (e) {}
|
|
88
|
+
var preloads = head.match(/\\/\\/ preload: (.+)/)
|
|
89
|
+
if (head.indexOf("// gl:") < 0) {
|
|
90
|
+
try { _creator.createEngine(preloads ? preloads[1].split(";") : []) } catch (e) { report("createEngine: " + String(e)) }
|
|
91
|
+
}
|
|
92
|
+
_creatorUtils.run(id, function (err) {
|
|
93
|
+
// Eval failed → native rollback kept THIS world (and its connection) alive. Keep serving.
|
|
94
|
+
busy = false
|
|
95
|
+
report(err)
|
|
96
|
+
if (queued !== null) { var q = queued; queued = null; load(q) }
|
|
97
|
+
})
|
|
98
|
+
// On success this world is torn down next tick — the new bundle carries its own prelude.
|
|
99
|
+
}, function (err) { busy = false; report("bundle fetch failed: " + String(err)) })
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function onServer(data) {
|
|
103
|
+
var m = null
|
|
104
|
+
try { m = JSON.parse(data) } catch (e) { return }
|
|
105
|
+
if (m && m.type === "reload" && typeof m.v === "number" && m.v !== V) load(m.v)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (TRANSPORT === "ws") {
|
|
109
|
+
var url = ORIGIN.replace("http", "ws") + BASE + "/ws"
|
|
110
|
+
var open = function () {
|
|
111
|
+
try {
|
|
112
|
+
wsId = _creatorUtils.websocketOpen(url, function (channel, data) {
|
|
113
|
+
if (channel === "open") {
|
|
114
|
+
try { _creatorUtils.websocketSend(wsId, JSON.stringify({ type: "hello", v: V })) } catch (e) {}
|
|
115
|
+
} else if (channel === "message") {
|
|
116
|
+
onServer(data)
|
|
117
|
+
} else if (channel === "close" || channel === "error") {
|
|
118
|
+
wsId = null
|
|
119
|
+
setTimeout(open, 2000)
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
} catch (e) { setTimeout(open, 2000) }
|
|
123
|
+
}
|
|
124
|
+
open()
|
|
125
|
+
} else {
|
|
126
|
+
var poll = function () {
|
|
127
|
+
_creatorUtils.fetch(ORIGIN + BASE + "/version", {}, function (id, status) {
|
|
128
|
+
var v = null
|
|
129
|
+
try { v = _creatorUtils.fetchToJson(id).v } catch (e) {}
|
|
130
|
+
try { _creatorUtils.disposeFetch(id) } catch (e) {}
|
|
131
|
+
setTimeout(poll, 700)
|
|
132
|
+
if (status < 400 && typeof v === "number" && v !== V && !busy) load(v)
|
|
133
|
+
}, function () { setTimeout(poll, 1500) })
|
|
134
|
+
}
|
|
135
|
+
poll()
|
|
136
|
+
}
|
|
137
|
+
})();
|
|
138
|
+
`
|
|
139
|
+
|
|
140
|
+
export type PreludeParams = {
|
|
141
|
+
/** e.g. "http://192.168.1.5:4499" */
|
|
142
|
+
origin: string
|
|
143
|
+
/** e.g. "/t/a1b2c3d4" */
|
|
144
|
+
base: string
|
|
145
|
+
transport: "ws" | "poll"
|
|
146
|
+
/** The version baked into this bundle; -1 for dev.js (always loads the latest). */
|
|
147
|
+
v: number
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The prelude with its placeholders substituted, ready to append to a compiled bundle. */
|
|
151
|
+
export const renderPrelude = (p: PreludeParams): string =>
|
|
152
|
+
DEV_PRELUDE_JS
|
|
153
|
+
.split("__DEV_ORIGIN__").join(p.origin)
|
|
154
|
+
.split("__DEV_BASE__").join(p.base)
|
|
155
|
+
.split("__DEV_TRANSPORT__").join(p.transport)
|
|
156
|
+
.split("__DEV_V__").join(String(p.v))
|
|
157
|
+
|
|
158
|
+
/** The dev.js placeholder-screen source with the project name baked in (as a JS string literal). */
|
|
159
|
+
export const renderDevScreen = (name: string): string =>
|
|
160
|
+
DEV_SCREEN_TS.split("__DEV_NAME__").join(JSON.stringify(name))
|