lecodes-cli 0.20.0 → 0.20.1
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/dist/index.js +1065 -577
- package/package.json +8 -8
- package/runtime/materials/decal-relief.mat +178 -0
- package/runtime/materials/decal.mat +172 -0
- package/runtime/materials/lightmap-baked-lite.mat +175 -0
- package/runtime/materials/lightmap-baked.mat +176 -0
- package/runtime/materials/lightmap.mat +181 -0
- package/runtime/materials/lit.mat +43 -0
- package/runtime/materials/particles-quad.mat +171 -0
- package/runtime/materials/particles.mat +121 -0
- package/runtime/materials/shadow.mat +21 -0
- package/runtime/materials/terrain.mat +134 -0
- package/runtime/materials/unlit-transparent.mat +42 -0
- package/runtime/materials/unlit.mat +44 -0
- package/runtime/materials/video.mat +31 -0
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/assets/{createViewerLite-BvHobVhp.js → createViewerLite-C_bkKXLg.js} +22 -22
- package/runtime/web/assets/{index-CfNCCBjN.js → index-BTB5KV9x.js} +1 -1
- package/runtime/web/embed.js +1 -1
- package/src/cmgenTool.ts +17 -9
- package/src/commands/appDesktop.ts +12 -2
- package/src/commands/appShared.ts +48 -0
- package/src/commands/assets.ts +3 -7
- package/src/commands/design.ts +9 -26
- package/src/commands/desktop.ts +3 -0
- package/src/commands/dev.ts +42 -0
- package/src/commands/projectTemplates.ts +13 -1
- package/src/commands/render.ts +4 -8
- package/src/commands/scene.ts +2 -6
- package/src/commands/shaders.ts +17 -1
- package/src/commands/shadersNew.ts +133 -0
- package/src/commands/thumbs.ts +3 -6
- package/src/commands/update.ts +42 -21
- package/src/desktopRenderer.ts +17 -0
- package/src/desktopScript.ts +1 -1
- package/src/dev/androidDev.ts +154 -0
- package/src/dev/devServer.ts +8 -2
- package/src/index.ts +21 -4
- package/src/matcTool.ts +31 -19
- package/src/peerInstall.ts +160 -0
- package/src/peers.ts +48 -14
- package/src/projectEnv.ts +4 -0
- package/src/releases.ts +16 -0
package/src/commands/update.ts
CHANGED
|
@@ -4,7 +4,8 @@ import { homedir } from "node:os"
|
|
|
4
4
|
import { dirname, join, resolve } from "node:path"
|
|
5
5
|
import pkg from "../../package.json"
|
|
6
6
|
import { CliError, c, flagBool, log, note, type Args } from "../util"
|
|
7
|
-
import { OPTIONAL_PEERS, peerVersion } from "../peers"
|
|
7
|
+
import { OPTIONAL_PEERS, peerSource, peerVersion } from "../peers"
|
|
8
|
+
import { installPeers } from "../peerInstall"
|
|
8
9
|
import { updateDesktop } from "../desktopRenderer"
|
|
9
10
|
import { updateMatc } from "../matcTool"
|
|
10
11
|
import { types } from "./types"
|
|
@@ -20,10 +21,12 @@ import { app } from "./app"
|
|
|
20
21
|
* 3. shaders recompile the project's .mat files with that matc (= `shaders compile`)
|
|
21
22
|
* 4. types the IDE types (.lecodes/types + tsconfig) from the server (= `types`)
|
|
22
23
|
* 5. shells ios/ + android/ pins → newest SDK release + plugin tags, re-sync (= `app update`)
|
|
23
|
-
* 6. cli the
|
|
24
|
-
* lecodes-
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* 6. cli the npm installs — every installed optional peer (lecodes-renderer, lecodes-design,
|
|
25
|
+
* lecodes-3d-editor, lecodes-assets, @napi-rs/canvas) and the CLI itself that is behind:
|
|
26
|
+
* peers in ~/.lecodes/peers (first-use installs, peerInstall.ts) are re-installed there
|
|
27
|
+
* — the renderer at THIS CLI's version, the rest at npm's latest in range; global ones
|
|
28
|
+
* (`npm install -g`, the old way) via `npm install -g …@latest`; the CLI itself last
|
|
29
|
+
* (the standalone binary can't come from npm — it says so and does the peers only)
|
|
27
30
|
*
|
|
28
31
|
* Every step is the existing subcommand, so nothing here can drift from what the individual
|
|
29
32
|
* commands do; this only sequences them and keeps going when one fails (the summary says which).
|
|
@@ -148,35 +151,53 @@ export const update = async (args: Args) => {
|
|
|
148
151
|
await run("cli", "lecodes-cli + its optional peers (global npm installs)", async () => {
|
|
149
152
|
if (flagBool(args, "no-cli")) return ["skipped", "--no-cli"]
|
|
150
153
|
if (FROM_SOURCE) return ["skipped", "running from the repo checkout — the CLI and its peers are the repo's own"]
|
|
151
|
-
|
|
154
|
+
// Where each peer lives decides how it moves: the CLI's own prefix (~/.lecodes/peers, filled on
|
|
155
|
+
// first use) is re-installed by peerInstall — the renderer there tracks THIS CLI's version, not
|
|
156
|
+
// npm's latest, because compiler and headless host are one ABI set; a global install
|
|
157
|
+
// (`npm install -g`, the old way) moves through npm like before.
|
|
158
|
+
const rows: { name: string, installed: string | null, latest: string | null, where: "global" | "prefix" | null }[] = []
|
|
152
159
|
for (const [name, probe] of OPTIONAL_PEERS) {
|
|
160
|
+
const where = peerSource(name, probe)
|
|
153
161
|
const installed = peerVersion(name, probe)
|
|
154
|
-
|
|
162
|
+
const latest = !installed ? null
|
|
163
|
+
: where === "prefix" && name === "lecodes-renderer" ? pkg.version
|
|
164
|
+
: await npmLatest(name)
|
|
165
|
+
rows.push({ name, installed, latest, where })
|
|
155
166
|
}
|
|
156
|
-
rows.push({ name: "lecodes-cli", installed: pkg.version, latest: await npmLatest("lecodes-cli") })
|
|
167
|
+
rows.push({ name: "lecodes-cli", installed: pkg.version, latest: await npmLatest("lecodes-cli"), where: "global" })
|
|
157
168
|
if (rows.every((r) => r.latest === null)) return ["skipped", "npm unreachable"]
|
|
158
169
|
const behind = rows.filter((r) => r.installed && r.latest && semverNewer(r.latest, r.installed) > 0)
|
|
159
170
|
for (const r of rows) {
|
|
160
|
-
const state = !r.installed ? c.dim("not installed (optional —
|
|
171
|
+
const state = !r.installed ? c.dim("not installed (optional — installed on first use)")
|
|
161
172
|
: !r.latest ? c.dim("(npm: unknown)")
|
|
162
173
|
: behind.includes(r) ? c.yellow("→ " + r.latest)
|
|
163
174
|
: c.dim("(up to date)")
|
|
164
|
-
log(" " + r.name.padEnd(18) + " " + (r.installed ?? "").padEnd(8) + " " + state)
|
|
175
|
+
log(" " + r.name.padEnd(18) + " " + (r.installed ?? "").padEnd(8) + " " + (r.where === "prefix" ? c.dim("peers ") : "") + state)
|
|
165
176
|
}
|
|
166
177
|
if (behind.length === 0) return "ok"
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
if (
|
|
178
|
+
const prefixBehind = behind.filter((r) => r.where === "prefix")
|
|
179
|
+
const globalSpecs = behind.filter((r) => r.where !== "prefix").map((r) => r.name + "@latest")
|
|
180
|
+
if (flagBool(args, "check")) {
|
|
181
|
+
return ["updated", behind.length + " behind — run: lecodes update" + (globalSpecs.length ? " (global: npm install -g " + globalSpecs.join(" ") + ")" : "")]
|
|
182
|
+
}
|
|
183
|
+
const done: string[] = []
|
|
184
|
+
if (prefixBehind.length) {
|
|
185
|
+
await installPeers(prefixBehind.map((r) => r.name), "lecodes update", { yes: true })
|
|
186
|
+
done.push(...prefixBehind.map((r) => r.name))
|
|
187
|
+
}
|
|
188
|
+
if (STANDALONE && globalSpecs.some((s) => s.startsWith("lecodes-cli@"))) {
|
|
170
189
|
note("this is the standalone build — the CLI updates by re-installing the release archive, not through npm; peers still go through npm")
|
|
171
190
|
}
|
|
172
|
-
const toInstall = STANDALONE ?
|
|
173
|
-
if (toInstall.length
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return ["
|
|
191
|
+
const toInstall = STANDALONE ? globalSpecs.filter((s) => !s.startsWith("lecodes-cli@")) : globalSpecs
|
|
192
|
+
if (toInstall.length) {
|
|
193
|
+
log(" " + c.dim("$") + " npm install -g " + toInstall.join(" "))
|
|
194
|
+
const r = spawnSync("npm", ["install", "-g", ...toInstall], { stdio: "inherit", shell: true })
|
|
195
|
+
if (r.status !== 0) throw new CliError("npm install -g exited with " + (r.status ?? "a signal"))
|
|
196
|
+
done.push(...toInstall.map((s) => s.replace("@latest", "")))
|
|
197
|
+
}
|
|
198
|
+
if (done.length === 0) return ["skipped", "only the standalone CLI itself is behind — download the release"]
|
|
199
|
+
const cliToo = done.includes("lecodes-cli")
|
|
200
|
+
return ["updated", done.join(", ") + " → latest" + (cliToo ? " (the new CLI runs from your next command)" : "")]
|
|
180
201
|
})
|
|
181
202
|
|
|
182
203
|
// Summary — one line per step so a scrolled-away failure can't hide.
|
package/src/desktopRenderer.ts
CHANGED
|
@@ -284,6 +284,10 @@ export type DesktopWindowOpts = {
|
|
|
284
284
|
minHeight?: number
|
|
285
285
|
resizable?: boolean
|
|
286
286
|
title?: string
|
|
287
|
+
/** localStorage scope (CREATOR_STORAGE_SCOPE, host ≥1.5.0): the app's stable identity — app.json
|
|
288
|
+
* `name` — so a `desktop run` keeps its saved state across runs (the host would otherwise key the
|
|
289
|
+
* storage by the launch path, which is a fresh temp dir every run). */
|
|
290
|
+
storageScope?: string
|
|
287
291
|
/** Borderless fullscreen on the primary monitor (host ≥1.3.0; older hosts ignore the env). */
|
|
288
292
|
fullscreen?: boolean
|
|
289
293
|
/** Display scale: a fixed density (0.5–4) or "fit" (derive from design frame vs monitor/work
|
|
@@ -293,6 +297,12 @@ export type DesktopWindowOpts = {
|
|
|
293
297
|
renderScale?: number
|
|
294
298
|
/** 3D buffer cap [W, H] in px (host ≥1.4.0) — e.g. [1920, 1080] keeps a 4K fullscreen at 1080p 3D. */
|
|
295
299
|
maxRenderSize?: [number, number]
|
|
300
|
+
/** The mouse button acts as a finger — click-drag scrolls/flings like touch (CREATOR_MOUSE_TOUCH;
|
|
301
|
+
* every host reads the env). A CREATOR_MOUSE_TOUCH already in the environment (shell / .env) wins. */
|
|
302
|
+
mouseEmulateTouch?: boolean
|
|
303
|
+
/** On-screen keyboard for every input (app.json desktop.virtualKeyboard, host ≥1.5.5): `true` or
|
|
304
|
+
* the block, forwarded verbatim as CREATOR_VIRTUAL_KEYBOARD JSON. A set env wins, like the mouse knob. */
|
|
305
|
+
virtualKeyboard?: boolean | object
|
|
296
306
|
/** Draw the host's frame counter in the corner (host ≥1.5.0; older hosts ignore the env). Costs
|
|
297
307
|
* nothing to leave off and next to nothing on — the digits are rectangles, no UI behind them. */
|
|
298
308
|
fps?: boolean
|
|
@@ -319,6 +329,7 @@ const windowEnv = (opts: DesktopWindowOpts): Record<string, string> => {
|
|
|
319
329
|
if (width && height) env.CREATOR_WINDOW_SIZE = `${width}x${height}`
|
|
320
330
|
if (opts.minWidth && opts.minHeight) env.CREATOR_MIN_WINDOW_SIZE = `${opts.minWidth}x${opts.minHeight}`
|
|
321
331
|
if (opts.title) env.CREATOR_WINDOW_TITLE = opts.title
|
|
332
|
+
if (opts.storageScope) env.CREATOR_STORAGE_SCOPE = opts.storageScope
|
|
322
333
|
if (opts.resizable === false) env.CREATOR_WINDOW_FIXED = "1"
|
|
323
334
|
if (opts.fullscreen !== undefined) env.CREATOR_WINDOW_FULLSCREEN = opts.fullscreen ? "1" : "0"
|
|
324
335
|
if (opts.scale === "fit") env.CREATOR_UI_SCALE = "fit"
|
|
@@ -328,6 +339,12 @@ const windowEnv = (opts: DesktopWindowOpts): Record<string, string> => {
|
|
|
328
339
|
env.CREATOR_RENDER_MAX = `${Math.round(opts.maxRenderSize[0])}x${Math.round(opts.maxRenderSize[1])}`
|
|
329
340
|
}
|
|
330
341
|
if (opts.fps) env.CREATOR_FPS = "1"
|
|
342
|
+
if (opts.mouseEmulateTouch !== undefined && process.env.CREATOR_MOUSE_TOUCH === undefined) {
|
|
343
|
+
env.CREATOR_MOUSE_TOUCH = opts.mouseEmulateTouch ? "1" : "0"
|
|
344
|
+
}
|
|
345
|
+
if (opts.virtualKeyboard !== undefined && process.env.CREATOR_VIRTUAL_KEYBOARD === undefined) {
|
|
346
|
+
env.CREATOR_VIRTUAL_KEYBOARD = typeof opts.virtualKeyboard === "boolean" ? (opts.virtualKeyboard ? "1" : "0") : JSON.stringify(opts.virtualKeyboard)
|
|
347
|
+
}
|
|
331
348
|
return env
|
|
332
349
|
}
|
|
333
350
|
|
package/src/desktopScript.ts
CHANGED
|
@@ -40,7 +40,7 @@ export type DesktopPlan = {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
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"]
|
|
43
|
+
const SEMANTIC_EXPECT = ["text", "notText", "node", "notNode", "screen", "value", "audio"]
|
|
44
44
|
const VIEW_NAMES = ["iso", "top", "front", "side", "back"]
|
|
45
45
|
const isVec3 = (v: unknown): v is [number, number, number] => Array.isArray(v) && v.length === 3 && v.every((n) => typeof n === "number")
|
|
46
46
|
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { existsSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { spawnSync } from "node:child_process"
|
|
4
|
+
import { CliError, note, warnErr } from "../util"
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* `lecodes dev --android` — open the dev URL in the LeCodes app on the connected phone, through adb.
|
|
8
|
+
*
|
|
9
|
+
* Why adb instead of "scan the QR": the phone reaches this machine over `adb reverse` (its
|
|
10
|
+
* 127.0.0.1:<port> is our port), so it works over USB with no Wi-Fi, no LAN-address guessing
|
|
11
|
+
* (WSL/Docker/VPN adapters), no firewall prompt. The launch is an EXPLICIT component intent — the
|
|
12
|
+
* app's VIEW filter is pinned to le.codes/qr, and an explicit start bypasses filter matching — with
|
|
13
|
+
* the plain dev.js url: the app's initial.js only fetches a url that ends in ".js", anything else is
|
|
14
|
+
* sent to le.codes/api/code, which answers 400 for a dev bundle (the "Произошла ошибка при открытии
|
|
15
|
+
* проекта" screen). That is also why the poll transport is a PATH (/dev-poll.js), never `?t=poll`.
|
|
16
|
+
*
|
|
17
|
+
* A device run has no frame cap (unlike the desktop host), so every exit path force-stops the app:
|
|
18
|
+
* a 3D scene left rendering is a sustained GPU load on a charging phone.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_ANDROID_PACKAGE = "com.letary.lecodes"
|
|
22
|
+
|
|
23
|
+
export type AndroidDevOptions = {
|
|
24
|
+
port: number
|
|
25
|
+
/** The device-side url to open (with `adb reverse` in place, 127.0.0.1:<port> is this machine). */
|
|
26
|
+
url: string
|
|
27
|
+
serial?: string
|
|
28
|
+
apk?: string
|
|
29
|
+
packageName?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type AndroidDevSession = {
|
|
33
|
+
serial: string
|
|
34
|
+
packageName: string
|
|
35
|
+
adb: string
|
|
36
|
+
/** `adb exec-out screencap -p` → the file. */
|
|
37
|
+
screenshot: (file: string) => boolean
|
|
38
|
+
/** force-stop the app + drop the reverse forward. Idempotent. */
|
|
39
|
+
stop: () => void
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const adbCandidates = (): string[] => {
|
|
43
|
+
const out: string[] = []
|
|
44
|
+
if (process.env.LECODES_ADB) out.push(process.env.LECODES_ADB)
|
|
45
|
+
out.push("adb")
|
|
46
|
+
const exe = process.platform === "win32" ? "adb.exe" : "adb"
|
|
47
|
+
for (const home of [process.env.ANDROID_HOME, process.env.ANDROID_SDK_ROOT]) {
|
|
48
|
+
if (home) out.push(join(home, "platform-tools", exe))
|
|
49
|
+
}
|
|
50
|
+
if (process.platform === "win32" && process.env.LOCALAPPDATA) out.push(join(process.env.LOCALAPPDATA, "Android", "Sdk", "platform-tools", exe))
|
|
51
|
+
if (process.env.HOME) out.push(join(process.env.HOME, "Android", "Sdk", "platform-tools", exe), join(process.env.HOME, "Library", "Android", "sdk", "platform-tools", exe))
|
|
52
|
+
return out
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const run = (adb: string, args: string[], input?: string): { ok: boolean, out: string } => {
|
|
56
|
+
const r = spawnSync(adb, args, { encoding: "utf8", input, windowsHide: true, maxBuffer: 64 * 1024 * 1024 })
|
|
57
|
+
const out = `${r.stdout ?? ""}${r.stderr ?? ""}`.trim()
|
|
58
|
+
return { ok: r.status === 0, out }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The adb binary: LECODES_ADB, PATH, then the usual SDK locations. */
|
|
62
|
+
export const resolveAdb = (): string => {
|
|
63
|
+
for (const cand of adbCandidates()) {
|
|
64
|
+
if (cand !== "adb" && !existsSync(cand)) continue
|
|
65
|
+
const r = spawnSync(cand, ["version"], { encoding: "utf8", windowsHide: true })
|
|
66
|
+
if (r.status === 0) return cand
|
|
67
|
+
}
|
|
68
|
+
throw new CliError("adb not found — install Android platform-tools (or point LECODES_ADB at adb).")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Connected devices in the `device` state (unauthorized/offline ones are reported, not used). */
|
|
72
|
+
export const listDevices = (adb: string): { serial: string, state: string, model?: string }[] => {
|
|
73
|
+
const { out } = run(adb, ["devices", "-l"])
|
|
74
|
+
const devices: { serial: string, state: string, model?: string }[] = []
|
|
75
|
+
for (const line of out.split(/\r?\n/).slice(1)) {
|
|
76
|
+
const m = /^(\S+)\s+(\S+)(.*)$/.exec(line.trim())
|
|
77
|
+
if (!m) continue
|
|
78
|
+
const model = /model:(\S+)/.exec(m[3] ?? "")?.[1]
|
|
79
|
+
devices.push({ serial: m[1], state: m[2], model })
|
|
80
|
+
}
|
|
81
|
+
return devices
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const pickDevice = (adb: string, serial: string | undefined): string => {
|
|
85
|
+
const all = listDevices(adb)
|
|
86
|
+
if (serial) {
|
|
87
|
+
const d = all.find((x) => x.serial === serial)
|
|
88
|
+
if (!d) throw new CliError(`No device "${serial}" — adb sees: ${all.map((x) => `${x.serial} (${x.state})`).join(", ") || "nothing"}.`)
|
|
89
|
+
if (d.state !== "device") throw new CliError(`Device ${serial} is "${d.state}" — accept the USB-debugging prompt on the phone.`)
|
|
90
|
+
return serial
|
|
91
|
+
}
|
|
92
|
+
const ready = all.filter((d) => d.state === "device")
|
|
93
|
+
if (ready.length === 1) return ready[0].serial
|
|
94
|
+
if (ready.length === 0) {
|
|
95
|
+
const other = all.map((d) => `${d.serial} (${d.state})`).join(", ")
|
|
96
|
+
throw new CliError(other
|
|
97
|
+
? `No usable device: adb sees ${other}. "unauthorized" = accept the USB-debugging prompt on the phone; "offline" = replug.`
|
|
98
|
+
: "No Android device: enable USB debugging (Developer options), plug the phone in, accept the prompt — or `adb connect <ip>:5555` for Wi-Fi debugging.")
|
|
99
|
+
}
|
|
100
|
+
throw new CliError(`Several devices: ${ready.map((d) => `${d.serial}${d.model ? ` (${d.model})` : ""}`).join(", ")} — pick one with --serial <id>.`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Install (optional), forward the port, launch the app on the dev url. Returns the session handle. */
|
|
104
|
+
export const launchOnAndroid = (opts: AndroidDevOptions): AndroidDevSession => {
|
|
105
|
+
const adb = resolveAdb()
|
|
106
|
+
const serial = pickDevice(adb, opts.serial)
|
|
107
|
+
const packageName = opts.packageName ?? process.env.LECODES_ANDROID_PACKAGE ?? DEFAULT_ANDROID_PACKAGE
|
|
108
|
+
const dev = (args: string[]) => run(adb, ["-s", serial, ...args])
|
|
109
|
+
|
|
110
|
+
if (opts.apk) {
|
|
111
|
+
if (!existsSync(opts.apk)) throw new CliError(`--apk: ${opts.apk} does not exist.`)
|
|
112
|
+
note(`Installing ${opts.apk}…`)
|
|
113
|
+
const r = dev(["install", "-r", opts.apk])
|
|
114
|
+
if (!r.ok || !/Success/.test(r.out)) {
|
|
115
|
+
throw new CliError(`adb install failed: ${r.out}${/USER_RESTRICTED/.test(r.out) ? "\n(MIUI: enable \"Install via USB\" in Developer options.)" : ""}`)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const installed = dev(["shell", "pm", "path", packageName])
|
|
119
|
+
if (!installed.ok || !installed.out.includes("package:")) {
|
|
120
|
+
throw new CliError(`${packageName} is not installed on ${serial} — pass --apk <file> (lecodes-android: .\\gradlew :app:assembleDebug → app/build/outputs/apk/debug/app-debug.apk).`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// The phone's 127.0.0.1:<port> → this machine's dev server. Survives Wi-Fi changes and VPNs.
|
|
124
|
+
const rev = dev(["reverse", `tcp:${opts.port}`, `tcp:${opts.port}`])
|
|
125
|
+
if (!rev.ok) warnErr(`adb reverse failed (${rev.out}) — the phone must reach the LAN url instead.`)
|
|
126
|
+
|
|
127
|
+
dev(["shell", "am", "force-stop", packageName])
|
|
128
|
+
dev(["logcat", "-c"])
|
|
129
|
+
const start = dev(["shell", "am", "start", "-n", `${packageName}/.MainActivity`, "-a", "android.intent.action.VIEW", "-d", opts.url])
|
|
130
|
+
if (!start.ok || /Error/.test(start.out)) throw new CliError(`Could not launch ${packageName}: ${start.out}`)
|
|
131
|
+
|
|
132
|
+
let stopped = false
|
|
133
|
+
return {
|
|
134
|
+
serial, packageName, adb,
|
|
135
|
+
screenshot: (file: string) => {
|
|
136
|
+
const r = spawnSync(adb, ["-s", serial, "exec-out", "screencap", "-p"], { windowsHide: true, maxBuffer: 64 * 1024 * 1024 })
|
|
137
|
+
if (r.status !== 0 || !r.stdout || r.stdout.length === 0) return false
|
|
138
|
+
writeFileSync(file, r.stdout)
|
|
139
|
+
return true
|
|
140
|
+
},
|
|
141
|
+
stop: () => {
|
|
142
|
+
if (stopped) return
|
|
143
|
+
stopped = true
|
|
144
|
+
dev(["shell", "am", "force-stop", packageName])
|
|
145
|
+
dev(["reverse", "--remove", `tcp:${opts.port}`])
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Crash lines for the app since the launch (logcat was cleared then): the native + Java fatals. */
|
|
151
|
+
export const androidCrashLines = (session: AndroidDevSession): string[] => {
|
|
152
|
+
const { out } = run(session.adb, ["-s", session.serial, "logcat", "-d", "-v", "brief"])
|
|
153
|
+
return out.split(/\r?\n/).filter((l) => /FATAL EXCEPTION|Fatal signal|\[crash\]/.test(l) || (/AndroidRuntime/.test(l) && l.includes(session.packageName))).slice(-20)
|
|
154
|
+
}
|
package/src/dev/devServer.ts
CHANGED
|
@@ -195,8 +195,8 @@ export const startDevServer = async (options: DevServerOptions): Promise<DevServ
|
|
|
195
195
|
const t = url.searchParams.get("t")
|
|
196
196
|
return t === "poll" ? "poll" : t === "web" ? "web" : "ws"
|
|
197
197
|
}
|
|
198
|
-
const withPrelude = (js: string, url: URL, v: number) => {
|
|
199
|
-
const transport = transportOf(url)
|
|
198
|
+
const withPrelude = (js: string, url: URL, v: number, forced?: "poll") => {
|
|
199
|
+
const transport = forced ?? transportOf(url)
|
|
200
200
|
return transport === "web" ? js : js + "\n" + renderPrelude({ origin, base, transport, v })
|
|
201
201
|
}
|
|
202
202
|
const webDir = options.webDir ?? null
|
|
@@ -212,6 +212,12 @@ export const startDevServer = async (options: DevServerOptions): Promise<DevServ
|
|
|
212
212
|
if (route === "/dev.js") {
|
|
213
213
|
return sendText(res, 200, "text/javascript", withPrelude(devJs, url, -1))
|
|
214
214
|
}
|
|
215
|
+
// The poll transport as a PATH: the Android app's initial.js only fetches a launch url that ends
|
|
216
|
+
// in ".js" (anything else goes to le.codes/api/code), so `?t=poll` can't reach it. Same bundle,
|
|
217
|
+
// prelude polls instead of the websocket — what `dev --android` opens under Bun (no ws there).
|
|
218
|
+
if (route === "/dev-poll.js") {
|
|
219
|
+
return sendText(res, 200, "text/javascript", withPrelude(devJs, url, -1, "poll"))
|
|
220
|
+
}
|
|
215
221
|
if (route === "/bundle.js") {
|
|
216
222
|
if (bundleJs === null) return sendText(res, 503, "text/plain", "No successful compile yet — check the terminal.")
|
|
217
223
|
res.setHeader("x-lecodes-v", String(v)) // the browser runner learns the version it runs
|
package/src/index.ts
CHANGED
|
@@ -31,7 +31,7 @@ import { CliError, c, log, parseArgs, type Args } from "./util"
|
|
|
31
31
|
import { installedDesktopVersions, selectedDesktopVersion } from "./desktopRenderer"
|
|
32
32
|
import { installedMatcVersions } from "./matcTool"
|
|
33
33
|
import { loadProjectEnv } from "./projectEnv"
|
|
34
|
-
import { OPTIONAL_PEERS, peerVersion } from "./peers"
|
|
34
|
+
import { OPTIONAL_PEERS, peerSource, peerVersion } from "./peers"
|
|
35
35
|
|
|
36
36
|
/* `install` is a standalone-binary-only command: it installs the shipped bundle onto PATH, so it's only
|
|
37
37
|
* meaningful when that bundle actually sits next to the executable — the `chisel` bundler + `runtime/`
|
|
@@ -120,6 +120,14 @@ ${c.bold("Commands:")}
|
|
|
120
120
|
--host <ip> LAN address to advertise (default: auto-detect)
|
|
121
121
|
--entry <path> override the entrypoint
|
|
122
122
|
--no-qr don't render the QR code
|
|
123
|
+
--android open the dev url in the LeCodes app on the phone
|
|
124
|
+
plugged in over adb (adb reverse: no Wi-Fi/LAN
|
|
125
|
+
address needed). --serial <id> picks a device,
|
|
126
|
+
--apk <file> installs first, --package <name>
|
|
127
|
+
(default com.letary.lecodes). Device logs stream
|
|
128
|
+
here. --time <s> --png <file>: bounded check —
|
|
129
|
+
screenshot after <s> seconds, force-stop, exit
|
|
130
|
+
(exit 1 on a crash). Ctrl+C force-stops the app.
|
|
123
131
|
--web also open the app in the browser (Web Lite host:
|
|
124
132
|
same bundle + hot reload, camera/map plugins
|
|
125
133
|
have web twins; the /web/ URL is served always)
|
|
@@ -315,12 +323,15 @@ ${c.bold("Commands:")}
|
|
|
315
323
|
--cell <m> (0.25) --cell-height <m> (0.1) --tile <cells> (48)
|
|
316
324
|
--obj also write <n>.navmesh.obj to inspect
|
|
317
325
|
--frames <n> load headroom (900) --logs host output
|
|
318
|
-
shaders [compile|update]
|
|
326
|
+
shaders [compile|update|new] The project's custom shaders (.mat) and their local compile:
|
|
319
327
|
no args lists the shaders + their compile state; compile
|
|
320
328
|
compiles them all into .lecodes/shaders (the same pass
|
|
321
329
|
'lecodes dev' runs on the fly — via Filament's matc,
|
|
322
330
|
downloaded on first use to ~/.lecodes/matc); update
|
|
323
|
-
downloads the newest matc (run after an engine upgrade)
|
|
331
|
+
downloads the newest matc (run after an engine upgrade);
|
|
332
|
+
new <name> [--ref lit] [--dir d] [--force] writes a fresh
|
|
333
|
+
.mat + .material.ts: the starter, or a copy of the engine's
|
|
334
|
+
own lit / unlit / particles / … source to start from
|
|
324
335
|
scene [path] LeCodes Scene Editor — visually edit a .scene.ts (tree, inspector,
|
|
325
336
|
gizmo) on local files (needs the optional 'lecodes-3d-editor' package;
|
|
326
337
|
works inside a cloned project or standalone in any folder)
|
|
@@ -446,7 +457,10 @@ const printVersion = () => {
|
|
|
446
457
|
const rows: [label: string, value: string][] = [["lecodes-cli", readVersion()]]
|
|
447
458
|
for (const [name, probe] of OPTIONAL_PEERS) {
|
|
448
459
|
const version = peerVersion(name, probe)
|
|
449
|
-
|
|
460
|
+
const where = peerSource(name, probe)
|
|
461
|
+
rows.push([name, version
|
|
462
|
+
? (where === "prefix" ? `${version} ${c.dim("(~/.lecodes/peers)")}` : version)
|
|
463
|
+
: c.dim("not installed — installed on first use")])
|
|
450
464
|
}
|
|
451
465
|
const desktopVersion = selectedDesktopVersion()
|
|
452
466
|
rows.push(["lecodes-desktop", installedDesktopVersions().includes(desktopVersion)
|
|
@@ -464,6 +478,9 @@ const main = async () => {
|
|
|
464
478
|
// The project's .env (LECODES_* / CREATOR_* next to app.json) — before any command reads the
|
|
465
479
|
// environment; the real environment keeps precedence. See projectEnv.ts.
|
|
466
480
|
loadProjectEnv()
|
|
481
|
+
// `--yes` / `-y` anywhere on the line also pre-approves first-use installs of optional peers
|
|
482
|
+
// (peerInstall.ts) — the same knob LECODES_YES=1 gives CI.
|
|
483
|
+
if (argv.includes("--yes") || argv.includes("-y")) process.env.LECODES_YES = "1"
|
|
467
484
|
|
|
468
485
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
469
486
|
log(HELP)
|
package/src/matcTool.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync } from "node:fs"
|
|
2
2
|
import { homedir } from "node:os"
|
|
3
3
|
import { join, resolve } from "node:path"
|
|
4
|
-
import { downloadReleaseArchive, fetchReleaseProduct, semverNewer, type ManifestVersion } from "./releases"
|
|
4
|
+
import { downloadReleaseArchive, fetchReleaseProduct, newestForPlatform, semverNewer, type ManifestFile, type ManifestVersion } from "./releases"
|
|
5
5
|
import { CliError, note } from "./util"
|
|
6
6
|
|
|
7
7
|
/*
|
|
@@ -16,8 +16,11 @@ import { CliError, note } from "./util"
|
|
|
16
16
|
* hosts ship). Cache: ~/.lecodes/matc/matc-<version>/. LECODES_MATC overrides resolution entirely
|
|
17
17
|
* (local dev: point it at packages/desktop/build/creator-gl/filament/tools/matc/matc.exe).
|
|
18
18
|
*
|
|
19
|
-
* No version pin: a filamat's compatibility is tied to the vendored Filament, which changes
|
|
20
|
-
*
|
|
19
|
+
* No version pin: a filamat's compatibility is tied to the vendored Filament, which changes rarely —
|
|
20
|
+
* resolution takes the newest cached copy, else downloads the newest release PUBLISHED FOR THIS
|
|
21
|
+
* PLATFORM (releases.ts newestForPlatform), NOT the product's `latest`: a release only ships the
|
|
22
|
+
* platforms whose machine ran it, so `latest` is often Windows-only and every other platform would
|
|
23
|
+
* read "not published" with a perfectly good older build sitting right there. `lecodes shaders
|
|
21
24
|
* update` moves an existing cache forward after an engine upgrade.
|
|
22
25
|
*
|
|
23
26
|
* Platforms: windows-x64, linux-x64, macos-arm64. On Windows/Linux matc rides the DESKTOP build
|
|
@@ -80,15 +83,21 @@ export const installedMatcVersions = (): string[] => {
|
|
|
80
83
|
export const fetchMatcManifest = (): Promise<{ latest: string, versions: ManifestVersion[] }> =>
|
|
81
84
|
fetchReleaseProduct("matc", "matc (shader compiler)")
|
|
82
85
|
|
|
83
|
-
|
|
86
|
+
/** The published matc this platform should get: the newest version carrying a build for it (see
|
|
87
|
+
* newestForPlatform — a release often ships one platform, so `latest` is the wrong question). */
|
|
88
|
+
const pickMatc = (versions: ManifestVersion[]): { version: string, file: ManifestFile } => {
|
|
84
89
|
const key = platformKey()
|
|
85
|
-
const
|
|
86
|
-
if (!
|
|
90
|
+
const hit = newestForPlatform(versions, key)
|
|
91
|
+
if (!hit) {
|
|
87
92
|
throw new CliError(
|
|
88
|
-
`matc
|
|
93
|
+
`matc isn't published for ${key} — push the project once so shaders compile on the ` +
|
|
89
94
|
`server, or set LECODES_MATC to a locally built matc.`,
|
|
90
95
|
)
|
|
91
96
|
}
|
|
97
|
+
return hit
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const downloadMatc = async (version: string, file: ManifestFile, dest: string): Promise<void> => {
|
|
92
101
|
note(`Downloading the shader compiler (matc ${version}, ${(file.size / 1024 / 1024).toFixed(1)} MB)…`)
|
|
93
102
|
await downloadReleaseArchive(file, {
|
|
94
103
|
cacheRoot: cacheRoot(),
|
|
@@ -109,29 +118,32 @@ export const resolveMatcExeCached = (): string | null => {
|
|
|
109
118
|
return cached ? findMatc(join(cacheRoot(), `matc-${cached}`)) : null
|
|
110
119
|
}
|
|
111
120
|
|
|
112
|
-
/** The matc binary: LECODES_MATC override → newest cached version → download the
|
|
113
|
-
* Throws CliError when unavailable (callers degrade to the push-once
|
|
121
|
+
/** The matc binary: LECODES_MATC override → newest cached version → download the newest release
|
|
122
|
+
* published for this platform. Throws CliError when unavailable (callers degrade to the push-once
|
|
123
|
+
* server-compile warning). */
|
|
114
124
|
export const resolveMatcExe = async (): Promise<string> => {
|
|
115
125
|
const cached = resolveMatcExeCached()
|
|
116
126
|
if (cached) return cached
|
|
117
|
-
const {
|
|
118
|
-
const
|
|
119
|
-
|
|
127
|
+
const { versions } = await fetchMatcManifest()
|
|
128
|
+
const { version, file } = pickMatc(versions)
|
|
129
|
+
const dir = join(cacheRoot(), `matc-${version}`)
|
|
130
|
+
await downloadMatc(version, file, dir)
|
|
120
131
|
const exe = findMatc(dir)
|
|
121
132
|
if (!exe) throw new CliError(`matc cache at ${dir} is broken — delete the folder and retry.`)
|
|
122
133
|
return exe
|
|
123
134
|
}
|
|
124
135
|
|
|
125
|
-
/** `lecodes shaders update`: download the newest published
|
|
126
|
-
* wins resolution). Run it after an engine upgrade so local
|
|
136
|
+
/** `lecodes shaders update`: download the newest matc published for this platform (cached versions
|
|
137
|
+
* stay; the newest wins resolution). Run it after an engine upgrade so local artifacts match the hosts. */
|
|
127
138
|
export const updateMatc = async (): Promise<void> => {
|
|
128
|
-
const {
|
|
139
|
+
const { versions } = await fetchMatcManifest()
|
|
140
|
+
const { version, file } = pickMatc(versions)
|
|
129
141
|
const before = installedMatcVersions()[0]
|
|
130
|
-
const dir = join(cacheRoot(), `matc-${
|
|
142
|
+
const dir = join(cacheRoot(), `matc-${version}`)
|
|
131
143
|
if (existsSync(dir) && findMatc(dir) !== null) {
|
|
132
|
-
note(`Already up to date: matc ${
|
|
144
|
+
note(`Already up to date: matc ${version}.`)
|
|
133
145
|
return
|
|
134
146
|
}
|
|
135
|
-
await downloadMatc(
|
|
136
|
-
note(before ? `matc updated to ${
|
|
147
|
+
await downloadMatc(version, file, dir)
|
|
148
|
+
note(before ? `matc updated to ${version} (was ${before}).` : `matc ${version} installed.`)
|
|
137
149
|
}
|