lecodes-cli 0.20.2 → 1.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 +5918 -5020
- package/package.json +7 -5
- package/runtime/materials/decal-relief.mat +12 -1
- package/runtime/materials/decal.mat +17 -3
- package/runtime/materials/lightmap-baked-lite.mat +6 -1
- package/runtime/materials/lightmap-baked.mat +6 -1
- package/runtime/materials/lightmap.mat +3 -0
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/assets/createViewerLite-DHqTFhWB.js +866 -0
- package/runtime/web/assets/{index-BMt7AnC5.js → index-Q06c6oHx.js} +1 -1
- package/runtime/web/embed.js +1 -1
- 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 +133 -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} +451 -42
- package/src/commands/{appDesktop.ts → app/desktop.ts} +63 -32
- package/src/commands/{appDesktopMac.ts → app/desktopMac.ts} +24 -18
- package/src/commands/{appIcon.ts → app/icon.ts} +2 -2
- package/src/commands/{app.ts → app/index.ts} +397 -289
- package/src/commands/{appShared.ts → app/shared.ts} +30 -39
- package/src/commands/{appTemplatesAndroid.ts → app/templates/android.ts} +62 -28
- package/src/commands/{appTemplatesGradlew.ts → app/templates/gradlew.ts} +1 -1
- package/src/commands/{appTemplates.ts → app/templates/ios.ts} +1 -1
- package/src/commands/assets.ts +23 -21
- package/src/commands/clone.ts +48 -51
- package/src/commands/compile.ts +62 -88
- 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 +90 -82
- package/src/commands/dev.ts +201 -176
- package/src/commands/diff.ts +39 -51
- package/src/commands/index.ts +60 -0
- package/src/commands/{init.ts → init/index.ts} +199 -191
- package/src/commands/install.ts +80 -67
- package/src/commands/lightmap.ts +203 -171
- package/src/commands/link.ts +87 -94
- package/src/commands/login.ts +25 -23
- package/src/commands/navmesh.ts +168 -138
- 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 +158 -99
- package/src/commands/scene.ts +58 -55
- package/src/commands/{shaders.ts → shaders/index.ts} +61 -42
- package/src/commands/{shadersNew.ts → shaders/new.ts} +2 -2
- package/src/commands/shared.ts +79 -0
- package/src/commands/status.ts +22 -26
- package/src/commands/test.ts +398 -371
- 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 +3 -3
- package/src/compile/headlessBundle.ts +133 -129
- package/src/compile/projectCompile.ts +2 -2
- package/src/compile/sceneCompile.ts +10 -8
- package/src/compile/screenEntry.ts +123 -127
- package/src/compile/shaders.ts +3 -3
- package/src/dev/androidDev.ts +1 -1
- package/src/dev/devServer.ts +3 -3
- package/src/dev/webRunner.ts +1 -1
- package/src/{cmgenTool.ts → hosts/cmgenTool.ts} +1 -1
- package/src/{desktopRenderer.ts → hosts/desktopRenderer.ts} +6 -5
- package/src/{desktopScript.ts → hosts/desktopScript.ts} +1 -1
- package/src/{distRoot.ts → hosts/distRoot.ts} +9 -5
- package/src/{matcTool.ts → hosts/matcTool.ts} +1 -1
- package/src/{peerInstall.ts → hosts/peerInstall.ts} +2 -2
- package/src/{releases.ts → hosts/releases.ts} +1 -1
- package/src/index.ts +32 -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/{projectEnv.ts → project/env.ts} +33 -2
- 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/web/assets/createViewerLite-Ct_PZdof.js +0 -852
- package/src/commands/design.ts +0 -846
- package/src/commands/update.ts +0 -211
- package/src/util.ts +0 -146
- /package/src/commands/{projectTemplates.ts → init/templates.ts} +0 -0
- /package/src/{lecodes-3d-editor.d.ts → declarations/lecodes-3d-editor.d.ts} +0 -0
- /package/src/{lecodes-assets.d.ts → declarations/lecodes-assets.d.ts} +0 -0
- /package/src/{lecodes-design.d.ts → declarations/lecodes-design.d.ts} +0 -0
- /package/src/{lecodes-renderer.d.ts → declarations/lecodes-renderer.d.ts} +0 -0
- /package/src/{qrcode-terminal.d.ts → declarations/qrcode-terminal.d.ts} +0 -0
- /package/src/{peers.ts → hosts/peers.ts} +0 -0
- /package/src/{designMeta.ts → project/designMeta.ts} +0 -0
- /package/src/{ignore.ts → project/ignore.ts} +0 -0
package/src/commands/dev.ts
CHANGED
|
@@ -2,16 +2,18 @@ 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
8
|
import { resolveWebRunnerDir } from "../dev/webRunner"
|
|
9
|
-
import { openBrowser } from "../browserAuth"
|
|
10
|
-
import { runDesktopDev } from "../desktopRenderer"
|
|
9
|
+
import { openBrowser } from "../platform/browserAuth"
|
|
10
|
+
import { runDesktopDev } from "../hosts/desktopRenderer"
|
|
11
11
|
import { androidCrashLines, launchOnAndroid, type AndroidDevSession } from "../dev/androidDev"
|
|
12
|
-
import { readAppConfigAt } from "./
|
|
12
|
+
import { readAppConfigAt } from "./app/shared"
|
|
13
13
|
import { resolveScale } from "./desktop"
|
|
14
|
-
import { CliError,
|
|
14
|
+
import { CliError, bool, c, defineCommand, log, note, num, str, warnErr } from "../cli"
|
|
15
|
+
import { bundleFlags } from "./shared"
|
|
16
|
+
import { toKey } from "../project/paths"
|
|
15
17
|
|
|
16
18
|
/*
|
|
17
19
|
* `lecodes dev` — run the project on a device straight from this machine, with hot reload.
|
|
@@ -28,6 +30,17 @@ import { CliError, c, flagBool, flagStr, log, note, toKey, warnErr, type Args }
|
|
|
28
30
|
* web twins of the camera/map plugins, no wasm. Same bundle and hot reload; browser console
|
|
29
31
|
* output streams back here like a device's. The URL is printed whenever the runner is available.
|
|
30
32
|
*
|
|
33
|
+
* `--android` opens the dev URL in the LeCodes app on the phone plugged in over adb (adb reverse:
|
|
34
|
+
* no Wi-Fi/LAN address needed — see dev/androidDev.ts). With `--time <s>` the run is bounded —
|
|
35
|
+
* screenshot to --png, force-stop, exit — which is the shape a scripted check wants; without it
|
|
36
|
+
* the app runs until Ctrl+C, and Ctrl+C force-stops it (a device run has no frame cap, and a 3D
|
|
37
|
+
* scene left rendering heats a charging phone).
|
|
38
|
+
*
|
|
39
|
+
* `--desktop` opens the dev URL in the native host too (same server, same hot reload — the host
|
|
40
|
+
* speaks the websocket channel). The window closing does NOT stop the server: phones stay
|
|
41
|
+
* connected, and re-running with --desktop is the reopen. Host errors degrade to a warning — the
|
|
42
|
+
* QR path must keep working.
|
|
43
|
+
*
|
|
31
44
|
* Works inside a cloned project or standalone in any folder with an entry .ts file.
|
|
32
45
|
* The desktop host speaks websocket since 1.0.0 (host_ws); `?t=poll` remains the fallback
|
|
33
46
|
* for older desktop builds (or any host without a websocket bridge).
|
|
@@ -94,185 +107,197 @@ const printQr = async (url: string) => {
|
|
|
94
107
|
}
|
|
95
108
|
}
|
|
96
109
|
|
|
97
|
-
export
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
const server = await startDevServer({
|
|
132
|
-
root, manifest, name, origin, port, token,
|
|
133
|
-
entryOverride: flagStr(args, "entry"),
|
|
134
|
-
webDir,
|
|
135
|
-
log: note,
|
|
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
|
-
})
|
|
110
|
+
export default defineCommand({
|
|
111
|
+
name: "dev",
|
|
112
|
+
summary: "Run the project on a device / the desktop / the browser from this machine, with hot reload",
|
|
113
|
+
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 (Web Lite runner) is served always; --web, --android and --desktop each also open the app on that host.",
|
|
114
|
+
flags: {
|
|
115
|
+
port: num("server port", { default: DEFAULT_PORT }),
|
|
116
|
+
host: str("LAN address to advertise (default: auto-detect)", { value: "<ip>" }),
|
|
117
|
+
entry: bundleFlags.entry,
|
|
118
|
+
"no-qr": bool("don't render the QR code"),
|
|
119
|
+
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"),
|
|
120
|
+
serial: str("(--android) the device to use", { value: "<id>" }),
|
|
121
|
+
apk: str("(--android) install this build first", { value: "<file>" }),
|
|
122
|
+
package: str("(--android) the app's package (default com.letary.lecodes)", { value: "<name>" }),
|
|
123
|
+
time: num("(--android) bounded check: screenshot after this many seconds, force-stop, exit (exit 1 on a crash)", { value: "<s>" }),
|
|
124
|
+
png: str("(--android --time) where the device screenshot goes", { value: "<file>" }),
|
|
125
|
+
web: bool("also open the app in the browser (Web Lite host: same bundle + hot reload, camera/map plugins have web twins)"),
|
|
126
|
+
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)"),
|
|
127
|
+
fps: bool("(--desktop) draw the host's frame counter"),
|
|
128
|
+
},
|
|
129
|
+
examples: ["lecodes dev", "lecodes dev --web", "lecodes dev --android --time 12 --png phone.png", "lecodes dev --desktop --fps"],
|
|
130
|
+
run: async ({ flags }) => {
|
|
131
|
+
// Inside a cloned project the server runs at its root; anywhere else it runs standalone against
|
|
132
|
+
// the current directory (no login / manifest needed) — same rule as `lecodes design`.
|
|
133
|
+
let root: string
|
|
134
|
+
let manifest: Manifest | null
|
|
135
|
+
try {
|
|
136
|
+
root = findProjectRoot(process.cwd())
|
|
137
|
+
manifest = readManifest(root)
|
|
138
|
+
} catch {
|
|
139
|
+
root = process.cwd()
|
|
140
|
+
manifest = null
|
|
141
|
+
}
|
|
142
|
+
const name = manifest?.name ?? basename(root)
|
|
140
143
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
warnErr(`Compile failed — fix it and save; devices connect once it compiles:\n${first.error}`)
|
|
147
|
-
}
|
|
144
|
+
// Bun's node:http can't complete a websocket upgrade (writes to the upgraded socket are dropped),
|
|
145
|
+
// so the reload push would silently never reach devices. Node is the shipped runtime.
|
|
146
|
+
if (typeof (globalThis as { Bun?: unknown }).Bun !== "undefined") {
|
|
147
|
+
warnErr("Running under Bun: the websocket reload channel doesn't work here — run `lecodes dev` with Node (or use ?t=poll on the device URL).")
|
|
148
|
+
}
|
|
148
149
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (ignore(key, false)) return false
|
|
159
|
-
} catch { return false }
|
|
160
|
-
return true
|
|
161
|
-
}
|
|
150
|
+
const port = flags.port || DEFAULT_PORT
|
|
151
|
+
const candidates = lanCandidates()
|
|
152
|
+
const host = flags.host ?? candidates[0]?.address
|
|
153
|
+
if (!host) throw new CliError("No LAN address found — pass one with --host <ip>.")
|
|
154
|
+
// A machine with several NICs can only be disambiguated by the human: name the runners-up so a
|
|
155
|
+
// device that can't reach the printed URL is one --host away from working, not a debugging session.
|
|
156
|
+
const alternatives = candidates.filter((c) => c.address !== host)
|
|
157
|
+
const origin = `http://${host}:${port}`
|
|
158
|
+
const token = loadDevToken(root)
|
|
162
159
|
|
|
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
|
-
}
|
|
160
|
+
const webDir = resolveWebRunnerDir()
|
|
161
|
+
if (flags.web && !webDir) {
|
|
162
|
+
throw new CliError("No Web Lite runner found for --web: this CLI has no runtime/web (a monorepo checkout needs `bun run build:embed` in packages/viewer-lite).")
|
|
163
|
+
}
|
|
164
|
+
const server = await startDevServer({
|
|
165
|
+
root, manifest, name, origin, port, token,
|
|
166
|
+
entryOverride: flags.entry,
|
|
167
|
+
webDir,
|
|
168
|
+
log: note,
|
|
169
|
+
}).catch((e: unknown) => {
|
|
170
|
+
const msg = e instanceof Error ? e.message : String(e)
|
|
171
|
+
throw new CliError(msg.includes("EADDRINUSE") ? `Port ${port} is already in use — pass --port <n>.` : msg)
|
|
172
|
+
})
|
|
182
173
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
174
|
+
note("Compiling…")
|
|
175
|
+
const first = await server.recompile()
|
|
176
|
+
if (first.ok) {
|
|
177
|
+
for (const w of first.warnings) warnErr(w)
|
|
178
|
+
} else {
|
|
179
|
+
warnErr(`Compile failed — fix it and save; devices connect once it compiles:\n${first.error}`)
|
|
180
|
+
}
|
|
190
181
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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.`)
|
|
182
|
+
// ---- watcher: debounced whole-project recompile; a successful compile broadcasts the reload ----
|
|
183
|
+
const ignore = loadIgnore(root)
|
|
184
|
+
const shouldRecompile = (file: string | null): boolean => {
|
|
185
|
+
if (file === null) return true // event without a filename — assume project content
|
|
186
|
+
const parts = file.replace(/\\/g, "/").split("/")
|
|
187
|
+
if (parts.some((p) => WATCH_SKIP_DIRS.has(p))) return false
|
|
188
|
+
if (WATCH_SKIP_FILES.has(parts[parts.length - 1])) return false
|
|
189
|
+
try {
|
|
190
|
+
const key = toKey(file)
|
|
191
|
+
if (ignore(key, false)) return false
|
|
192
|
+
} catch { return false }
|
|
193
|
+
return true
|
|
194
|
+
}
|
|
208
195
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
196
|
+
const debug = process.env.LECODES_DEV_DEBUG === "1"
|
|
197
|
+
const underBun = typeof (globalThis as { Bun?: unknown }).Bun !== "undefined"
|
|
198
|
+
let timer: NodeJS.Timeout | null = null
|
|
199
|
+
const onChange = (file: string | null) => {
|
|
200
|
+
if (debug) note(`[watch] event: ${file ?? "(null)"} → ${shouldRecompile(file) ? "recompile" : "skip"}`)
|
|
201
|
+
if (!shouldRecompile(file)) return
|
|
202
|
+
if (timer) clearTimeout(timer)
|
|
203
|
+
timer = setTimeout(async () => {
|
|
204
|
+
timer = null
|
|
205
|
+
const result = await server.recompile()
|
|
206
|
+
if (!result.ok) {
|
|
207
|
+
warnErr(`Compile failed (devices keep the last good version):\n${result.error}`)
|
|
208
|
+
} else if (result.changed) {
|
|
209
|
+
const devices = server.clientCount()
|
|
210
|
+
note(`✓ v${result.v} → ${devices} device${devices === 1 ? "" : "s"}`)
|
|
211
|
+
for (const w of result.warnings) warnErr(w)
|
|
212
|
+
}
|
|
213
|
+
}, DEBOUNCE_MS)
|
|
214
|
+
}
|
|
214
215
|
|
|
215
|
-
|
|
216
|
-
// --time <s> the run is bounded — screenshot to --png, force-stop, exit — which is the shape a
|
|
217
|
-
// scripted check wants; without it the app runs until Ctrl+C, and Ctrl+C force-stops it (a
|
|
218
|
-
// device run has no frame cap, and a 3D scene left rendering heats a charging phone).
|
|
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
|
|
216
|
+
let watcher: FSWatcher | null = null
|
|
226
217
|
try {
|
|
227
|
-
|
|
228
|
-
} catch
|
|
229
|
-
|
|
230
|
-
throw e
|
|
218
|
+
watcher = watch(root, { recursive: true }, (_type, file) => onChange(file))
|
|
219
|
+
} catch {
|
|
220
|
+
warnErr("fs.watch(recursive) unavailable on this platform — hot reload disabled (restart to pick up changes).")
|
|
231
221
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
222
|
+
watcher?.on("error", (e) => warnErr(`Watcher error — hot reload may have stopped (restart lecodes dev): ${e.message}`))
|
|
223
|
+
|
|
224
|
+
// ---- banner ------------------------------------------------------------------------------------
|
|
225
|
+
log("")
|
|
226
|
+
log(`${c.bold("LeCodes Dev")} — ${name}`)
|
|
227
|
+
log("")
|
|
228
|
+
if (!flags["no-qr"]) await printQr(server.url)
|
|
229
|
+
log(`Scan the QR with the LeCodes app, or open: ${c.bold(server.url)}`)
|
|
230
|
+
// The browser runner: localhost is a secure context (camera works there), the LAN address is
|
|
231
|
+
// what a phone's browser can reach.
|
|
232
|
+
const webLocal = server.webUrl ? server.webUrl.replace(`http://${host}:`, "http://localhost:") : null
|
|
233
|
+
if (server.webUrl && webLocal) log(`In the browser (Web Lite): ${c.bold(webLocal)} (LAN: ${server.webUrl})`)
|
|
234
|
+
note(`Older desktop builds (no websocket bridge): ${server.url}?t=poll`)
|
|
235
|
+
if (alternatives.length > 0) {
|
|
236
|
+
note(`Device can't reach it? This machine also has ${alternatives.map((a) => `${a.address} (${a.iface})`).join(", ")} — retry with --host <ip>.`)
|
|
237
237
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (crashes.length > 0) {
|
|
247
|
-
warnErr(`The app crashed on the phone:\n${crashes.join("\n")}`)
|
|
248
|
-
finish(1)
|
|
249
|
-
}
|
|
250
|
-
finish(0)
|
|
251
|
-
}, seconds * 1000)
|
|
238
|
+
if (existsSync(join(root, LECODES_DIR, "manifest.json"))) note(`Token stored in ${LECODES_DIR}/dev.json (delete it to rotate the URL).`)
|
|
239
|
+
log("")
|
|
240
|
+
log(`Watching ${root} — save a file to hot-reload. Ctrl+C to stop.`)
|
|
241
|
+
|
|
242
|
+
// --web: open the browser runner (the server keeps serving it either way).
|
|
243
|
+
if (flags.web && webLocal) {
|
|
244
|
+
note("Opening the browser…")
|
|
245
|
+
openBrowser(webLocal)
|
|
252
246
|
}
|
|
253
|
-
}
|
|
254
247
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
() => note("
|
|
275
|
-
(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
248
|
+
if (flags.android) {
|
|
249
|
+
const seconds = flags.time ?? 0
|
|
250
|
+
const png = flags.png
|
|
251
|
+
// Under Bun the websocket channel never delivers (see the warning above): the phone would sit on
|
|
252
|
+
// "Connected — loading the project…" forever. The path-form poll url works on both runtimes.
|
|
253
|
+
const deviceUrl = `http://127.0.0.1:${port}${server.url.slice(origin.length)}`.replace(/\/dev\.js$/, underBun ? "/dev-poll.js" : "/dev.js")
|
|
254
|
+
let session: AndroidDevSession
|
|
255
|
+
try {
|
|
256
|
+
session = launchOnAndroid({ port, url: deviceUrl, serial: flags.serial, apk: flags.apk, packageName: flags.package })
|
|
257
|
+
} catch (e) {
|
|
258
|
+
server.close(); watcher?.close()
|
|
259
|
+
throw e
|
|
260
|
+
}
|
|
261
|
+
note(`Android: ${session.serial} → ${deviceUrl} (adb reverse; device logs stream here as [device:log])`)
|
|
262
|
+
const finish = (code: number) => {
|
|
263
|
+
session.stop()
|
|
264
|
+
server.close(); watcher?.close()
|
|
265
|
+
process.exit(code)
|
|
266
|
+
}
|
|
267
|
+
process.on("SIGINT", () => { note("Stopping the app on the phone…"); finish(0) })
|
|
268
|
+
if (seconds > 0) {
|
|
269
|
+
setTimeout(() => {
|
|
270
|
+
const crashes = androidCrashLines(session)
|
|
271
|
+
if (png) {
|
|
272
|
+
if (session.screenshot(png)) note(`Wrote ${png} (device screenshot after ${seconds} s)`)
|
|
273
|
+
else warnErr(`Screenshot failed (adb screencap).`)
|
|
274
|
+
}
|
|
275
|
+
if (crashes.length > 0) {
|
|
276
|
+
warnErr(`The app crashed on the phone:\n${crashes.join("\n")}`)
|
|
277
|
+
finish(1)
|
|
278
|
+
}
|
|
279
|
+
finish(0)
|
|
280
|
+
}, seconds * 1000)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (flags.desktop) {
|
|
285
|
+
const appCfg = readAppConfigAt(root)
|
|
286
|
+
const win = appCfg?.desktop?.window
|
|
287
|
+
note("Opening LeCodes Desktop against the dev server…")
|
|
288
|
+
// scale IS forwarded (density changes how the app looks — dev should match production);
|
|
289
|
+
// fullscreen deliberately is NOT (a fullscreen dev window hides the terminal).
|
|
290
|
+
void runDesktopDev(server.url, {
|
|
291
|
+
width: win?.width, height: win?.height,
|
|
292
|
+
minWidth: win?.minWidth, minHeight: win?.minHeight,
|
|
293
|
+
resizable: win?.resizable,
|
|
294
|
+
title: win?.title ?? appCfg?.name ?? name,
|
|
295
|
+
scale: resolveScale(undefined, win?.scale),
|
|
296
|
+
fps: flags.fps,
|
|
297
|
+
}).then(
|
|
298
|
+
() => note("Desktop window closed — the dev server keeps running (Ctrl+C to stop)."),
|
|
299
|
+
(e: unknown) => warnErr(`Desktop host: ${e instanceof Error ? e.message : String(e)}`),
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
})
|
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 ")
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Group } from "../cli"
|
|
2
|
+
import app from "./app"
|
|
3
|
+
import assets from "./assets"
|
|
4
|
+
import clone from "./clone"
|
|
5
|
+
import compile from "./compile"
|
|
6
|
+
import create from "./create"
|
|
7
|
+
import design from "./design"
|
|
8
|
+
import desktop from "./desktop"
|
|
9
|
+
import dev from "./dev"
|
|
10
|
+
import diff from "./diff"
|
|
11
|
+
import init from "./init"
|
|
12
|
+
import install from "./install"
|
|
13
|
+
import lightmap from "./lightmap"
|
|
14
|
+
import link from "./link"
|
|
15
|
+
import login from "./login"
|
|
16
|
+
import navmesh from "./navmesh"
|
|
17
|
+
import pn from "./pn"
|
|
18
|
+
import pull from "./pull"
|
|
19
|
+
import push from "./push"
|
|
20
|
+
import render from "./render"
|
|
21
|
+
import scene from "./scene"
|
|
22
|
+
import shaders from "./shaders"
|
|
23
|
+
import status from "./status"
|
|
24
|
+
import test from "./test"
|
|
25
|
+
import thumbs from "./thumbs"
|
|
26
|
+
import update from "./update"
|
|
27
|
+
|
|
28
|
+
/*
|
|
29
|
+
* THE command list. Adding a command = one file with a `defineCommand` default export + one line
|
|
30
|
+
* here, in the group it belongs to (CONTRIBUTING.md). Order within a group is the help's order.
|
|
31
|
+
*/
|
|
32
|
+
export const commandGroups = (opts: { install: boolean }): Group[] => [
|
|
33
|
+
{ title: "Project", commands: [init, create, clone, link, status, diff, pull, push, login] },
|
|
34
|
+
{ title: "Run & test", commands: [
|
|
35
|
+
dev,
|
|
36
|
+
compile,
|
|
37
|
+
render,
|
|
38
|
+
test,
|
|
39
|
+
desktop,
|
|
40
|
+
] },
|
|
41
|
+
{ title: "Editors", commands: [
|
|
42
|
+
design,
|
|
43
|
+
scene,
|
|
44
|
+
thumbs,
|
|
45
|
+
] },
|
|
46
|
+
{ title: "Content", commands: [
|
|
47
|
+
assets,
|
|
48
|
+
shaders,
|
|
49
|
+
lightmap,
|
|
50
|
+
navmesh,
|
|
51
|
+
] },
|
|
52
|
+
{ title: "Native apps", commands: [
|
|
53
|
+
app,
|
|
54
|
+
pn,
|
|
55
|
+
] },
|
|
56
|
+
{ title: "Maintenance", commands: [
|
|
57
|
+
update,
|
|
58
|
+
{ ...install, hidden: !opts.install },
|
|
59
|
+
] },
|
|
60
|
+
]
|