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
package/src/commands/dev.ts
CHANGED
|
@@ -2,31 +2,51 @@ import { existsSync, mkdirSync, readFileSync, watch, writeFileSync, type FSWatch
|
|
|
2
2
|
import { randomBytes } from "node:crypto"
|
|
3
3
|
import { networkInterfaces } from "node:os"
|
|
4
4
|
import { basename, join } from "node:path"
|
|
5
|
-
import { findProjectRoot, readManifest, LECODES_DIR, type Manifest } from "../manifest"
|
|
6
|
-
import { loadIgnore } from "../ignore"
|
|
5
|
+
import { findProjectRoot, readManifest, LECODES_DIR, type Manifest } from "../project/manifest"
|
|
6
|
+
import { loadIgnore } from "../project/ignore"
|
|
7
7
|
import { startDevServer } from "../dev/devServer"
|
|
8
|
-
import {
|
|
9
|
-
import { openBrowser } from "../browserAuth"
|
|
10
|
-
import { runDesktopDev } from "../desktopRenderer"
|
|
8
|
+
import { resolveWebRunner } from "../dev/webRunner"
|
|
9
|
+
import { openBrowser } from "../platform/browserAuth"
|
|
10
|
+
import { runDesktopDev } from "../hosts/desktopRenderer"
|
|
11
11
|
import { androidCrashLines, launchOnAndroid, type AndroidDevSession } from "../dev/androidDev"
|
|
12
|
-
import {
|
|
12
|
+
import { launchOnIos, type IosDevSession } from "../dev/iosDev"
|
|
13
|
+
import { readAppConfigAt } from "./app/shared"
|
|
13
14
|
import { resolveScale } from "./desktop"
|
|
14
|
-
import { CliError,
|
|
15
|
+
import { CliError, bool, c, defineCommand, log, note, num, str, warnErr } from "../cli"
|
|
16
|
+
import { bundleFlags } from "./shared"
|
|
17
|
+
import { toKey } from "../project/paths"
|
|
15
18
|
|
|
16
19
|
/*
|
|
17
20
|
* `lecodes dev` — run the project on a device straight from this machine, with hot reload.
|
|
18
21
|
*
|
|
19
22
|
* Starts a LAN dev server (compile + assets + reload channel), prints a QR code, and watches the
|
|
20
23
|
* working tree. The device opens the QR URL like any project URL; every save recompiles and
|
|
21
|
-
* hot-swaps the running app (`
|
|
24
|
+
* hot-swaps the running app (`_creatorApp.run` — fresh context, rollback on error). Device
|
|
22
25
|
* console output and errors stream back to this terminal. Fully local: no login, no le.codes
|
|
23
26
|
* backend. Custom shaders compile locally too (matc, downloaded on first use — matcTool.ts);
|
|
24
27
|
* only when matc can't be fetched do they fall back to server-compiled artifacts (push once).
|
|
25
28
|
*
|
|
26
|
-
* `--web` opens the same project in the browser: the dev server also serves
|
|
27
|
-
*
|
|
28
|
-
* web
|
|
29
|
-
*
|
|
29
|
+
* `--web` opens the same project in the browser: the dev server also serves, under /web/, the
|
|
30
|
+
* app in a device (lecodes-web-frame's page: the device, the orientation, the console) around the
|
|
31
|
+
* web player (lecodes-web-player's dist — the runtime as wasm: Filament, creator-2d, the tree UI,
|
|
32
|
+
* and the web halves of the plugins). A phone that opens the url gets the player alone, the app
|
|
33
|
+
* filling its window. Same bundle and hot reload; the browser's console output streams back here
|
|
34
|
+
* like a device's. The URL is printed whenever the player is available.
|
|
35
|
+
*
|
|
36
|
+
* `--android` opens the dev URL in the LeCodes app on the phone plugged in over adb (adb reverse:
|
|
37
|
+
* no Wi-Fi/LAN address needed — see dev/androidDev.ts). With `--time <s>` the run is bounded —
|
|
38
|
+
* screenshot to --png, force-stop, exit — which is the shape a scripted check wants; without it
|
|
39
|
+
* the app runs until Ctrl+C, and Ctrl+C force-stops it (a device run has no frame cap, and a 3D
|
|
40
|
+
* scene left rendering heats a charging phone).
|
|
41
|
+
*
|
|
42
|
+
* `--ios` is the Apple twin (dev/iosDev.ts): the LeCodes app on a booted simulator or a plugged-in
|
|
43
|
+
* iPhone through `xcrun simctl` / `devicectl`, the url as a launch argument. Same `--time` / `--png`
|
|
44
|
+
* shape (screenshots on a simulator).
|
|
45
|
+
*
|
|
46
|
+
* `--desktop` opens the dev URL in the native host too (same server, same hot reload — the host
|
|
47
|
+
* speaks the websocket channel). The window closing does NOT stop the server: phones stay
|
|
48
|
+
* connected, and re-running with --desktop is the reopen. Host errors degrade to a warning — the
|
|
49
|
+
* QR path must keep working.
|
|
30
50
|
*
|
|
31
51
|
* Works inside a cloned project or standalone in any folder with an entry .ts file.
|
|
32
52
|
* The desktop host speaks websocket since 1.0.0 (host_ws); `?t=poll` remains the fallback
|
|
@@ -54,7 +74,7 @@ const WATCH_SKIP_FILES = new Set(["tsconfig.json", "jsconfig.json", ".gitignore"
|
|
|
54
74
|
*/
|
|
55
75
|
const VIRTUAL_IFACE = /wsl|docker|hyper-v|vethernet|virtualbox|vmware|tap|tun|zerotier|tailscale|loopback/i
|
|
56
76
|
|
|
57
|
-
const lanCandidates = (): { address: string, iface: string }[] => {
|
|
77
|
+
export const lanCandidates = (): { address: string, iface: string }[] => {
|
|
58
78
|
const nets = networkInterfaces()
|
|
59
79
|
const out: { address: string, iface: string, rank: number }[] = []
|
|
60
80
|
for (const iface of Object.keys(nets)) {
|
|
@@ -94,185 +114,243 @@ const printQr = async (url: string) => {
|
|
|
94
114
|
}
|
|
95
115
|
}
|
|
96
116
|
|
|
97
|
-
export
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
117
|
+
export default defineCommand({
|
|
118
|
+
name: "dev",
|
|
119
|
+
summary: "Run the project on a device / the desktop / the browser from this machine, with hot reload",
|
|
120
|
+
description: "Starts a LAN dev server + prints a QR code; scan it with the LeCodes app. Saves recompile + hot-swap the running app; device logs/errors stream back here. No login needed. The /web/ URL (the web player) is served always; --web, --android and --desktop each also open the app on that host.",
|
|
121
|
+
flags: {
|
|
122
|
+
port: num("server port", { default: DEFAULT_PORT }),
|
|
123
|
+
host: str("LAN address to advertise (default: auto-detect)", { value: "<ip>" }),
|
|
124
|
+
entry: bundleFlags.entry,
|
|
125
|
+
"no-qr": bool("don't render the QR code"),
|
|
126
|
+
android: bool("open the dev url in the LeCodes app on the phone plugged in over adb (adb reverse: no Wi-Fi/LAN address needed); device logs stream here; Ctrl+C force-stops the app"),
|
|
127
|
+
serial: str("(--android) the device to use", { value: "<id>" }),
|
|
128
|
+
apk: str("(--android) install this build first", { value: "<file>" }),
|
|
129
|
+
package: str("(--android) the app's package (default com.letary.lecodes)", { value: "<name>" }),
|
|
130
|
+
time: num("(--android | --ios) bounded check: screenshot after this many seconds, force-stop, exit (exit 1 on a crash)", { value: "<s>" }),
|
|
131
|
+
png: str("(--android | --ios, with --time) where the device screenshot goes", { value: "<file>" }),
|
|
132
|
+
ios: bool("open the dev url in the LeCodes app on a booted simulator or a plugged-in iPhone (Mac + Xcode: simctl / devicectl); a simulator gets the 127.0.0.1 url, a phone the LAN url; Ctrl+C terminates the app"),
|
|
133
|
+
udid: str("(--ios) the simulator (udid or name) or device (udid or name) to use", { value: "<id>" }),
|
|
134
|
+
app: str("(--ios) install this LeCodesApp.app first", { value: "<dir>" }),
|
|
135
|
+
bundle: str("(--ios) the app's bundle id (default com.letary.lecodes)", { value: "<id>" }),
|
|
136
|
+
web: bool("also open the app in the browser (the web player in a device: same bundle + hot reload, the console)"),
|
|
137
|
+
desktop: bool("also open the app in the native desktop host (same hot reload; window from app.json's desktop block; closing it keeps the server)"),
|
|
138
|
+
fps: bool("(--desktop) draw the host's frame counter"),
|
|
139
|
+
},
|
|
140
|
+
examples: ["lecodes dev", "lecodes dev --web", "lecodes dev --android --time 12 --png phone.png", "lecodes dev --ios --time 12 --png sim.png", "lecodes dev --desktop --fps"],
|
|
141
|
+
run: async ({ flags }) => {
|
|
142
|
+
// Inside a cloned project the server runs at its root; anywhere else it runs standalone against
|
|
143
|
+
// the current directory (no login / manifest needed) — same rule as `lecodes design`.
|
|
144
|
+
let root: string
|
|
145
|
+
let manifest: Manifest | null
|
|
146
|
+
try {
|
|
147
|
+
root = findProjectRoot(process.cwd())
|
|
148
|
+
manifest = readManifest(root)
|
|
149
|
+
} catch {
|
|
150
|
+
root = process.cwd()
|
|
151
|
+
manifest = null
|
|
152
|
+
}
|
|
153
|
+
const name = manifest?.name ?? basename(root)
|
|
116
154
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
155
|
+
// Bun's node:http can't complete a websocket upgrade (writes to the upgraded socket are dropped),
|
|
156
|
+
// so the reload push would silently never reach devices: a phone that scanned a websocket url
|
|
157
|
+
// sat on "Connected — loading the project…" forever. Under Bun every device url — the QR, the
|
|
158
|
+
// printed one, what --ios / --android open — is the POLL form (/dev-poll.js: a path, since the
|
|
159
|
+
// launcher only fetches urls that end in .js). Node is the shipped runtime.
|
|
160
|
+
const underBun = typeof (globalThis as { Bun?: unknown }).Bun !== "undefined"
|
|
161
|
+
if (underBun) {
|
|
162
|
+
warnErr("Running under Bun: no websocket reload channel here — devices poll instead (the QR points at /dev-poll.js). Node is the shipped runtime.")
|
|
163
|
+
}
|
|
126
164
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}).catch((e: unknown) => {
|
|
137
|
-
const msg = e instanceof Error ? e.message : String(e)
|
|
138
|
-
throw new CliError(msg.includes("EADDRINUSE") ? `Port ${port} is already in use — pass --port <n>.` : msg)
|
|
139
|
-
})
|
|
165
|
+
const port = flags.port || DEFAULT_PORT
|
|
166
|
+
const candidates = lanCandidates()
|
|
167
|
+
const host = flags.host ?? candidates[0]?.address
|
|
168
|
+
if (!host) throw new CliError("No LAN address found — pass one with --host <ip>.")
|
|
169
|
+
// A machine with several NICs can only be disambiguated by the human: name the runners-up so a
|
|
170
|
+
// device that can't reach the printed URL is one --host away from working, not a debugging session.
|
|
171
|
+
const alternatives = candidates.filter((c) => c.address !== host)
|
|
172
|
+
const origin = `http://${host}:${port}`
|
|
173
|
+
const token = loadDevToken(root)
|
|
140
174
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
175
|
+
const web = resolveWebRunner(note)
|
|
176
|
+
if (flags.web && !web) {
|
|
177
|
+
throw new CliError("No web player found for --web: this CLI has no runtime/web (a monorepo checkout needs `bun run build` in hosts/web-player).")
|
|
178
|
+
}
|
|
179
|
+
const server = await startDevServer({
|
|
180
|
+
root, manifest, name, origin, port, token,
|
|
181
|
+
entryOverride: flags.entry,
|
|
182
|
+
web,
|
|
183
|
+
log: note,
|
|
184
|
+
}).catch((e: unknown) => {
|
|
185
|
+
const msg = e instanceof Error ? e.message : String(e)
|
|
186
|
+
throw new CliError(msg.includes("EADDRINUSE") ? `Port ${port} is already in use — pass --port <n>.` : msg)
|
|
187
|
+
})
|
|
148
188
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
try {
|
|
157
|
-
const key = toKey(file)
|
|
158
|
-
if (ignore(key, false)) return false
|
|
159
|
-
} catch { return false }
|
|
160
|
-
return true
|
|
161
|
-
}
|
|
189
|
+
note("Compiling…")
|
|
190
|
+
const first = await server.recompile()
|
|
191
|
+
if (first.ok) {
|
|
192
|
+
for (const w of first.warnings) warnErr(w)
|
|
193
|
+
} else {
|
|
194
|
+
warnErr(`Compile failed — fix it and save; devices connect once it compiles:\n${first.error}`)
|
|
195
|
+
}
|
|
162
196
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const devices = server.clientCount()
|
|
177
|
-
note(`✓ v${result.v} → ${devices} device${devices === 1 ? "" : "s"}`)
|
|
178
|
-
for (const w of result.warnings) warnErr(w)
|
|
179
|
-
}
|
|
180
|
-
}, DEBOUNCE_MS)
|
|
181
|
-
}
|
|
197
|
+
// ---- watcher: debounced whole-project recompile; a successful compile broadcasts the reload ----
|
|
198
|
+
const ignore = loadIgnore(root)
|
|
199
|
+
const shouldRecompile = (file: string | null): boolean => {
|
|
200
|
+
if (file === null) return true // event without a filename — assume project content
|
|
201
|
+
const parts = file.replace(/\\/g, "/").split("/")
|
|
202
|
+
if (parts.some((p) => WATCH_SKIP_DIRS.has(p))) return false
|
|
203
|
+
if (WATCH_SKIP_FILES.has(parts[parts.length - 1])) return false
|
|
204
|
+
try {
|
|
205
|
+
const key = toKey(file)
|
|
206
|
+
if (ignore(key, false)) return false
|
|
207
|
+
} catch { return false }
|
|
208
|
+
return true
|
|
209
|
+
}
|
|
182
210
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
211
|
+
const debug = process.env.LECODES_DEV_DEBUG === "1"
|
|
212
|
+
let timer: NodeJS.Timeout | null = null
|
|
213
|
+
const onChange = (file: string | null) => {
|
|
214
|
+
if (debug) note(`[watch] event: ${file ?? "(null)"} → ${shouldRecompile(file) ? "recompile" : "skip"}`)
|
|
215
|
+
if (!shouldRecompile(file)) return
|
|
216
|
+
if (timer) clearTimeout(timer)
|
|
217
|
+
timer = setTimeout(async () => {
|
|
218
|
+
timer = null
|
|
219
|
+
const result = await server.recompile()
|
|
220
|
+
if (!result.ok) {
|
|
221
|
+
warnErr(`Compile failed (devices keep the last good version):\n${result.error}`)
|
|
222
|
+
} else if (result.changed) {
|
|
223
|
+
const devices = server.clientCount()
|
|
224
|
+
note(`✓ v${result.v} → ${devices} device${devices === 1 ? "" : "s"}`)
|
|
225
|
+
for (const w of result.warnings) warnErr(w)
|
|
226
|
+
}
|
|
227
|
+
}, DEBOUNCE_MS)
|
|
228
|
+
}
|
|
190
229
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
// what a phone's browser can reach.
|
|
199
|
-
const webLocal = server.webUrl ? server.webUrl.replace(`http://${host}:`, "http://localhost:") : null
|
|
200
|
-
if (server.webUrl && webLocal) log(`In the browser (Web Lite): ${c.bold(webLocal)} (LAN: ${server.webUrl})`)
|
|
201
|
-
note(`Older desktop builds (no websocket bridge): ${server.url}?t=poll`)
|
|
202
|
-
if (alternatives.length > 0) {
|
|
203
|
-
note(`Device can't reach it? This machine also has ${alternatives.map((a) => `${a.address} (${a.iface})`).join(", ")} — retry with --host <ip>.`)
|
|
204
|
-
}
|
|
205
|
-
if (existsSync(join(root, LECODES_DIR, "manifest.json"))) note(`Token stored in ${LECODES_DIR}/dev.json (delete it to rotate the URL).`)
|
|
206
|
-
log("")
|
|
207
|
-
log(`Watching ${root} — save a file to hot-reload. Ctrl+C to stop.`)
|
|
230
|
+
let watcher: FSWatcher | null = null
|
|
231
|
+
try {
|
|
232
|
+
watcher = watch(root, { recursive: true }, (_type, file) => onChange(file))
|
|
233
|
+
} catch {
|
|
234
|
+
warnErr("fs.watch(recursive) unavailable on this platform — hot reload disabled (restart to pick up changes).")
|
|
235
|
+
}
|
|
236
|
+
watcher?.on("error", (e) => warnErr(`Watcher error — hot reload may have stopped (restart lecodes dev): ${e.message}`))
|
|
208
237
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
238
|
+
// ---- banner ------------------------------------------------------------------------------------
|
|
239
|
+
log("")
|
|
240
|
+
log(`${c.bold("LeCodes Dev")} — ${name}`)
|
|
241
|
+
log("")
|
|
242
|
+
// The url a device opens: the websocket form, or the poll form under Bun (see above).
|
|
243
|
+
const deviceUrl = underBun ? server.url.replace(/\/dev\.js$/, "/dev-poll.js") : server.url
|
|
244
|
+
if (!flags["no-qr"]) await printQr(deviceUrl)
|
|
245
|
+
log(`Scan the QR with the LeCodes app, or open: ${c.bold(deviceUrl)}`)
|
|
246
|
+
// The browser runner: localhost is a secure context (camera works there), the LAN address is
|
|
247
|
+
// what a phone's browser can reach.
|
|
248
|
+
const webLocal = server.webUrl ? server.webUrl.replace(`http://${host}:`, "http://localhost:") : null
|
|
249
|
+
if (server.webUrl && webLocal) log(`In the browser: ${c.bold(webLocal)} (LAN: ${server.webUrl})`)
|
|
250
|
+
if (!underBun) note(`Older desktop builds (no websocket bridge): ${server.url}?t=poll`)
|
|
251
|
+
if (alternatives.length > 0) {
|
|
252
|
+
note(`Device can't reach it? This machine also has ${alternatives.map((a) => `${a.address} (${a.iface})`).join(", ")} — retry with --host <ip>.`)
|
|
253
|
+
}
|
|
254
|
+
if (existsSync(join(root, LECODES_DIR, "manifest.json"))) note(`Token stored in ${LECODES_DIR}/dev.json (delete it to rotate the URL).`)
|
|
255
|
+
log("")
|
|
256
|
+
log(`Watching ${root} — save a file to hot-reload. Ctrl+C to stop.`)
|
|
214
257
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (flagBool(args, "android")) {
|
|
220
|
-
const seconds = Number(flagStr(args, "time")) || 0
|
|
221
|
-
const png = flagStr(args, "png")
|
|
222
|
-
// Under Bun the websocket channel never delivers (see the warning above): the phone would sit on
|
|
223
|
-
// "Connected — loading the project…" forever. The path-form poll url works on both runtimes.
|
|
224
|
-
const deviceUrl = `http://127.0.0.1:${port}${server.url.slice(origin.length)}`.replace(/\/dev\.js$/, underBun ? "/dev-poll.js" : "/dev.js")
|
|
225
|
-
let session: AndroidDevSession
|
|
226
|
-
try {
|
|
227
|
-
session = launchOnAndroid({ port, url: deviceUrl, serial: flagStr(args, "serial"), apk: flagStr(args, "apk"), packageName: flagStr(args, "package") })
|
|
228
|
-
} catch (e) {
|
|
229
|
-
server.close(); watcher?.close()
|
|
230
|
-
throw e
|
|
258
|
+
// --web: open the browser runner (the server keeps serving it either way).
|
|
259
|
+
if (flags.web && webLocal) {
|
|
260
|
+
note("Opening the browser…")
|
|
261
|
+
openBrowser(webLocal)
|
|
231
262
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
263
|
+
|
|
264
|
+
if (flags.android) {
|
|
265
|
+
const seconds = flags.time ?? 0
|
|
266
|
+
const png = flags.png
|
|
267
|
+
// adb reverse: the phone reaches the server through its own loopback.
|
|
268
|
+
const androidUrl = `http://127.0.0.1:${port}${deviceUrl.slice(origin.length)}`
|
|
269
|
+
let session: AndroidDevSession
|
|
270
|
+
try {
|
|
271
|
+
session = launchOnAndroid({ port, url: androidUrl, serial: flags.serial, apk: flags.apk, packageName: flags.package })
|
|
272
|
+
} catch (e) {
|
|
273
|
+
server.close(); watcher?.close()
|
|
274
|
+
throw e
|
|
275
|
+
}
|
|
276
|
+
note(`Android: ${session.serial} → ${androidUrl} (adb reverse; device logs stream here as [device:log])`)
|
|
277
|
+
const finish = (code: number) => {
|
|
278
|
+
session.stop()
|
|
279
|
+
server.close(); watcher?.close()
|
|
280
|
+
process.exit(code)
|
|
281
|
+
}
|
|
282
|
+
process.on("SIGINT", () => { note("Stopping the app on the phone…"); finish(0) })
|
|
283
|
+
if (seconds > 0) {
|
|
284
|
+
setTimeout(() => {
|
|
285
|
+
const crashes = androidCrashLines(session)
|
|
286
|
+
if (png) {
|
|
287
|
+
if (session.screenshot(png)) note(`Wrote ${png} (device screenshot after ${seconds} s)`)
|
|
288
|
+
else warnErr(`Screenshot failed (adb screencap).`)
|
|
289
|
+
}
|
|
290
|
+
if (crashes.length > 0) {
|
|
291
|
+
warnErr(`The app crashed on the phone:\n${crashes.join("\n")}`)
|
|
292
|
+
finish(1)
|
|
293
|
+
}
|
|
294
|
+
finish(0)
|
|
295
|
+
}, seconds * 1000)
|
|
296
|
+
}
|
|
237
297
|
}
|
|
238
|
-
|
|
239
|
-
if (
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
298
|
+
|
|
299
|
+
if (flags.ios) {
|
|
300
|
+
const seconds = flags.time ?? 0
|
|
301
|
+
const png = flags.png
|
|
302
|
+
// A simulator shares the Mac's network: the loopback url, no Wi-Fi involved. A phone gets the
|
|
303
|
+
// LAN url the QR shows.
|
|
304
|
+
const path = deviceUrl.slice(origin.length)
|
|
305
|
+
let session: IosDevSession
|
|
306
|
+
try {
|
|
307
|
+
session = launchOnIos({ simulatorUrl: `http://127.0.0.1:${port}${path}`, deviceUrl, udid: flags.udid, app: flags.app, bundleId: flags.bundle })
|
|
308
|
+
} catch (e) {
|
|
309
|
+
server.close(); watcher?.close()
|
|
310
|
+
throw e
|
|
311
|
+
}
|
|
312
|
+
note(`iOS: ${session.name} → ${session.kind === "simulator" ? `http://127.0.0.1:${port}${path}` : deviceUrl} (device logs stream here as [device:log])`)
|
|
313
|
+
const finish = (code: number) => {
|
|
314
|
+
session.stop()
|
|
315
|
+
server.close(); watcher?.close()
|
|
316
|
+
process.exit(code)
|
|
317
|
+
}
|
|
318
|
+
process.on("SIGINT", () => { note(`Stopping the app on ${session.name}…`); finish(0) })
|
|
319
|
+
if (seconds > 0) {
|
|
320
|
+
setTimeout(() => {
|
|
321
|
+
const crashes = session.crashLines()
|
|
322
|
+
if (png) {
|
|
323
|
+
if (session.screenshot(png)) note(`Wrote ${png} (screenshot after ${seconds} s)`)
|
|
324
|
+
else warnErr(session.kind === "simulator" ? "Screenshot failed (simctl io screenshot)." : "Screenshots need a simulator (no capture channel to a phone).")
|
|
325
|
+
}
|
|
326
|
+
if (crashes.length > 0) {
|
|
327
|
+
warnErr(`The app crashed:\n${crashes.join("\n")}`)
|
|
328
|
+
finish(1)
|
|
329
|
+
}
|
|
330
|
+
finish(0)
|
|
331
|
+
}, seconds * 1000)
|
|
332
|
+
}
|
|
252
333
|
}
|
|
253
|
-
}
|
|
254
334
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
}
|
|
335
|
+
if (flags.desktop) {
|
|
336
|
+
const appCfg = readAppConfigAt(root)
|
|
337
|
+
const win = appCfg?.desktop?.window
|
|
338
|
+
note("Opening LeCodes Desktop against the dev server…")
|
|
339
|
+
// scale IS forwarded (density changes how the app looks — dev should match production);
|
|
340
|
+
// fullscreen deliberately is NOT (a fullscreen dev window hides the terminal).
|
|
341
|
+
void runDesktopDev(server.url, {
|
|
342
|
+
width: win?.width, height: win?.height,
|
|
343
|
+
minWidth: win?.minWidth, minHeight: win?.minHeight,
|
|
344
|
+
resizable: win?.resizable,
|
|
345
|
+
title: win?.title ?? appCfg?.name ?? name,
|
|
346
|
+
scale: resolveScale(undefined, win?.scale),
|
|
347
|
+
// The frame-rate cap is forwarded too: it changes how the game FEELS, and dev should match production.
|
|
348
|
+
frameRate: appCfg?.desktop?.frameRate,
|
|
349
|
+
fps: flags.fps,
|
|
350
|
+
}).then(
|
|
351
|
+
() => note("Desktop window closed — the dev server keeps running (Ctrl+C to stop)."),
|
|
352
|
+
(e: unknown) => warnErr(`Desktop host: ${e instanceof Error ? e.message : String(e)}`),
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
})
|
package/src/commands/diff.ts
CHANGED
|
@@ -1,59 +1,47 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { scanProject } from "../localFiles"
|
|
5
|
-
import {
|
|
6
|
-
import { unifiedDiff } from "../textDiff"
|
|
7
|
-
import {
|
|
1
|
+
import { bool, c, defineCommand, info, log } from "../cli"
|
|
2
|
+
import { getProject } from "../platform/api"
|
|
3
|
+
import { computeServerDiff, type ServerDiffEntry } from "../platform/serverDiff"
|
|
4
|
+
import { scanProject } from "../project/localFiles"
|
|
5
|
+
import { toKey } from "../project/paths"
|
|
6
|
+
import { unifiedDiff } from "../project/textDiff"
|
|
7
|
+
import { linkedProject } from "./shared"
|
|
8
8
|
|
|
9
9
|
/*
|
|
10
|
-
* `lecodes diff [path…]` —
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* --name-only list changed paths + status instead of full diffs
|
|
16
|
-
* [path…] limit to these files / folders
|
|
10
|
+
* `lecodes diff [path…]` — the local working tree against the project's CURRENT server state as a
|
|
11
|
+
* unified diff. "+" is local, "-" is the server, so it answers "what would my push change?" —
|
|
12
|
+
* including edits made on the server (the web editor) since the clone. The headline use is
|
|
13
|
+
* conflict resolution before `push --force`.
|
|
17
14
|
*/
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
export default defineCommand({
|
|
16
|
+
name: "diff",
|
|
17
|
+
summary: "Show a unified diff of local vs the current server state",
|
|
18
|
+
usage: "[path…]",
|
|
19
|
+
flags: { "name-only": bool("list changed paths + status only") },
|
|
20
|
+
run: async ({ args, flags }) => {
|
|
21
|
+
const { root, manifest, apiUrl, token } = linkedProject()
|
|
22
|
+
info(c.dim("Comparing with the server…"))
|
|
23
|
+
const project = await getProject(apiUrl, token, manifest.uuid)
|
|
24
|
+
const scan = scanProject(root, manifest)
|
|
25
|
+
let entries = computeServerDiff(project, scan)
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (args._.length > 0) {
|
|
32
|
-
const keys = args._.map(toKey)
|
|
33
|
-
entries = entries.filter(e => keys.some(k => e.path === k || e.path.startsWith(k + "/")))
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (entries.length === 0) {
|
|
37
|
-
info(c.dim("No differences between local and server."))
|
|
38
|
-
return
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (flagBool(args, "name-only")) {
|
|
42
|
-
for (const e of entries) log(statusLabel(e) + e.path + (e.binary ? c.dim(" (binary)") : ""))
|
|
43
|
-
return
|
|
44
|
-
}
|
|
27
|
+
// Optional path filter (a file, or a folder prefix).
|
|
28
|
+
if (args.length > 0) {
|
|
29
|
+
const keys = args.map(toKey)
|
|
30
|
+
entries = entries.filter(e => keys.some(k => e.path === k || e.path.startsWith(k + "/")))
|
|
31
|
+
}
|
|
32
|
+
if (entries.length === 0) { info(c.dim("No differences between local and server.")); return }
|
|
45
33
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
34
|
+
if (flags["name-only"]) {
|
|
35
|
+
for (const e of entries) log(statusLabel(e) + e.path + (e.binary ? c.dim(" (binary)") : ""))
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
for (const e of entries) {
|
|
39
|
+
if (e.binary) { log(statusLabel(e) + e.path + c.dim(" (binary — content not shown)")); continue }
|
|
40
|
+
const d = unifiedDiff(e.serverText ?? "", e.localText ?? "", `server${e.path}`, `local${e.path}`)
|
|
41
|
+
if (d) log("\n" + d)
|
|
50
42
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
}
|
|
43
|
+
},
|
|
44
|
+
})
|
|
55
45
|
|
|
56
46
|
const statusLabel = (e: ServerDiffEntry): string =>
|
|
57
|
-
e.status === "added" ? c.green(" added ")
|
|
58
|
-
: e.status === "removed" ? c.red(" removed ")
|
|
59
|
-
: c.yellow(" modified ")
|
|
47
|
+
e.status === "added" ? c.green(" added ") : e.status === "removed" ? c.red(" removed ") : c.yellow(" modified ")
|