lecodes-cli 0.19.0 → 0.19.2
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 +730 -235
- package/package.json +14 -13
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk-types.json +1 -1
- package/src/api.ts +302 -0
- package/src/browserAuth.ts +87 -0
- package/src/cmgenTool.ts +104 -0
- package/src/commands/app.ts +891 -0
- package/src/commands/appAndroid.ts +603 -0
- package/src/commands/appDesktop.ts +326 -0
- package/src/commands/appDesktopMac.ts +470 -0
- package/src/commands/appIcon.ts +187 -0
- package/src/commands/appShared.ts +448 -0
- package/src/commands/appTemplates.ts +883 -0
- package/src/commands/appTemplatesAndroid.ts +599 -0
- package/src/commands/appTemplatesGradlew.ts +9 -0
- package/src/commands/assets.ts +28 -0
- package/src/commands/clone.ts +60 -0
- package/src/commands/compile.ts +253 -0
- package/src/commands/create.ts +59 -0
- package/src/commands/design.ts +863 -0
- package/src/commands/designTemplates.ts +11 -0
- package/src/commands/desktop.ts +122 -0
- package/src/commands/dev.ts +214 -0
- package/src/commands/diff.ts +59 -0
- package/src/commands/init.ts +191 -0
- package/src/commands/install.ts +198 -0
- package/src/commands/lightmap.ts +290 -0
- package/src/commands/link.ts +147 -0
- package/src/commands/login.ts +24 -0
- package/src/commands/navmesh.ts +225 -0
- package/src/commands/pn.ts +305 -0
- package/src/commands/projectTemplates.ts +129 -0
- package/src/commands/pull.ts +109 -0
- package/src/commands/push.ts +173 -0
- package/src/commands/render.ts +414 -0
- package/src/commands/scene.ts +148 -0
- package/src/commands/shaders.ts +157 -0
- package/src/commands/status.ts +29 -0
- package/src/commands/test.ts +370 -0
- package/src/commands/thumbs.ts +178 -0
- package/src/commands/types.ts +87 -0
- package/src/commands/update.ts +190 -0
- package/src/compile/assetIcons.ts +214 -0
- package/src/compile/collect.ts +46 -0
- package/src/compile/collectLocal.ts +37 -0
- package/src/compile/designCompile.ts +104 -0
- package/src/compile/fonts.ts +156 -0
- package/src/compile/headlessBundle.ts +129 -0
- package/src/compile/nativeStack.ts +21 -0
- package/src/compile/projectCompile.ts +109 -0
- package/src/compile/sceneCompile.ts +113 -0
- package/src/compile/screenEntry.ts +127 -0
- package/src/compile/shaders.ts +245 -0
- package/src/config.ts +42 -0
- package/src/designMeta.ts +35 -0
- package/src/desktopRenderer.ts +532 -0
- package/src/desktopScript.ts +276 -0
- package/src/dev/clientTemplates.ts +160 -0
- package/src/dev/devServer.ts +292 -0
- package/src/dev/wsServer.ts +144 -0
- package/src/distRoot.ts +20 -0
- package/src/ignore.ts +163 -0
- package/src/index.ts +491 -0
- package/src/lecodes-3d-editor.d.ts +41 -0
- package/src/lecodes-assets.d.ts +7 -0
- package/src/lecodes-design.d.ts +191 -0
- package/src/lecodes-renderer.d.ts +131 -0
- package/src/localFiles.ts +144 -0
- package/src/manifest.ts +46 -0
- package/src/matcTool.ts +137 -0
- package/src/peers.ts +40 -0
- package/src/project.ts +61 -0
- package/src/projectEnv.ts +94 -0
- package/src/qrcode-terminal.d.ts +9 -0
- package/src/releases.ts +125 -0
- package/src/serverDiff.ts +78 -0
- package/src/textDiff.ts +103 -0
- package/src/types.ts +0 -0
- package/src/util.ts +146 -0
- package/runtime/sdk/animate/animate.ts +0 -238
- package/runtime/sdk/animate/bezier.ts +0 -138
- package/runtime/sdk/animate/easings.ts +0 -126
- package/runtime/sdk/canvas/Canvas.ts +0 -305
- package/runtime/sdk/compile/aspectMacro.ts +0 -86
- package/runtime/sdk/compile/assetIconMacro.ts +0 -384
- package/runtime/sdk/compile/assetMacro.ts +0 -146
- package/runtime/sdk/compile/assetName.ts +0 -50
- package/runtime/sdk/compile/bundler.ts +0 -287
- package/runtime/sdk/compile/compileProject.ts +0 -129
- package/runtime/sdk/compile/detectEntry.ts +0 -128
- package/runtime/sdk/compile/fontMacro.ts +0 -459
- package/runtime/sdk/compile/fontRegistry.ts +0 -78
- package/runtime/sdk/compile/header.ts +0 -67
- package/runtime/sdk/compile/index.ts +0 -100
- package/runtime/sdk/compile/libraryImports.ts +0 -52
- package/runtime/sdk/compile/liteMaterial.ts +0 -247
- package/runtime/sdk/compile/sceneEditor.ts +0 -88
- package/runtime/sdk/compile/serverSplit.ts +0 -233
- package/runtime/sdk/compile/serverTypes.ts +0 -227
- package/runtime/sdk/compile/sfnt.ts +0 -98
- package/runtime/sdk/compile/shaderSchema.ts +0 -202
- package/runtime/sdk/compile/shaderTargets.ts +0 -81
- package/runtime/sdk/compile/sourcemap.ts +0 -25
- package/runtime/sdk/core/Aspect.ts +0 -568
- package/runtime/sdk/core/InspectorUI.ts +0 -212
- package/runtime/sdk/core/StateMachine.ts +0 -308
- package/runtime/sdk/core/color.ts +0 -66
- package/runtime/sdk/core/compWrite.ts +0 -42
- package/runtime/sdk/core/events.ts +0 -38
- package/runtime/sdk/core/fields.ts +0 -120
- package/runtime/sdk/core/registry.ts +0 -23
- package/runtime/sdk/core/signals.ts +0 -277
- package/runtime/sdk/core/time.ts +0 -84
- package/runtime/sdk/g2/Camera2D.ts +0 -40
- package/runtime/sdk/g2/CharacterController2D.ts +0 -276
- package/runtime/sdk/g2/Node2D.ts +0 -267
- package/runtime/sdk/g2/OneWay2D.ts +0 -66
- package/runtime/sdk/g2/Physics2D.ts +0 -346
- package/runtime/sdk/g2/Scene2D.ts +0 -209
- package/runtime/sdk/g2/Shape2D.ts +0 -259
- package/runtime/sdk/g2/Sprite.ts +0 -89
- package/runtime/sdk/g2/SpriteAnimation.ts +0 -171
- package/runtime/sdk/g2/SpriteSheet.ts +0 -166
- package/runtime/sdk/g2/Texture2D.ts +0 -47
- package/runtime/sdk/g2/Tilemap.ts +0 -41
- package/runtime/sdk/g2/Tileset.ts +0 -71
- package/runtime/sdk/g2/Trigger2D.ts +0 -77
- package/runtime/sdk/g2/autotile.ts +0 -433
- package/runtime/sdk/g2/cells.ts +0 -91
- package/runtime/sdk/g2/defineScene2d.ts +0 -381
- package/runtime/sdk/g2/groups2d.ts +0 -106
- package/runtime/sdk/g2/loop.ts +0 -50
- package/runtime/sdk/g2/scenarios2d.ts +0 -69
- package/runtime/sdk/g2/touch.ts +0 -83
- package/runtime/sdk/gl/Camera.ts +0 -160
- package/runtime/sdk/gl/CameraPlace.ts +0 -52
- package/runtime/sdk/gl/CharacterController.ts +0 -238
- package/runtime/sdk/gl/Gearbox.ts +0 -212
- package/runtime/sdk/gl/Geometry.ts +0 -279
- package/runtime/sdk/gl/IK.ts +0 -193
- package/runtime/sdk/gl/InstancedMesh.ts +0 -132
- package/runtime/sdk/gl/Light.ts +0 -107
- package/runtime/sdk/gl/Lightmap.ts +0 -256
- package/runtime/sdk/gl/Material.ts +0 -260
- package/runtime/sdk/gl/Mesh.ts +0 -83
- package/runtime/sdk/gl/Model.ts +0 -124
- package/runtime/sdk/gl/NavAgent.ts +0 -337
- package/runtime/sdk/gl/NavMesh.ts +0 -397
- package/runtime/sdk/gl/Node.ts +0 -350
- package/runtime/sdk/gl/Noise.ts +0 -30
- package/runtime/sdk/gl/Particles.ts +0 -676
- package/runtime/sdk/gl/Physics.ts +0 -222
- package/runtime/sdk/gl/Plane.ts +0 -53
- package/runtime/sdk/gl/Ragdoll.ts +0 -270
- package/runtime/sdk/gl/Ray.ts +0 -16
- package/runtime/sdk/gl/Scene.ts +0 -486
- package/runtime/sdk/gl/Shape.ts +0 -409
- package/runtime/sdk/gl/Terrain.ts +0 -1092
- package/runtime/sdk/gl/Texture.ts +0 -63
- package/runtime/sdk/gl/Trigger.ts +0 -45
- package/runtime/sdk/gl/Vehicle.ts +0 -473
- package/runtime/sdk/gl/Wheel.ts +0 -240
- package/runtime/sdk/gl/animation/AnimationClip.ts +0 -204
- package/runtime/sdk/gl/animation/Animator.ts +0 -120
- package/runtime/sdk/gl/animation/Layer.ts +0 -31
- package/runtime/sdk/gl/animation/Loop.ts +0 -30
- package/runtime/sdk/gl/animation/Playback.ts +0 -44
- package/runtime/sdk/gl/animation/core.ts +0 -336
- package/runtime/sdk/gl/controls.ts +0 -95
- package/runtime/sdk/gl/physicsEvents.ts +0 -20
- package/runtime/sdk/gl/scenarios.ts +0 -291
- package/runtime/sdk/gl/state.ts +0 -6
- package/runtime/sdk/gl/terrainMesh.ts +0 -219
- package/runtime/sdk/gl/touch.ts +0 -68
- package/runtime/sdk/inject.ts +0 -197
- package/runtime/sdk/math/Mathf.ts +0 -118
- package/runtime/sdk/math/mat4.ts +0 -278
- package/runtime/sdk/math/quat.ts +0 -232
- package/runtime/sdk/math/vec.ts +0 -255
- package/runtime/sdk/net/codec.ts +0 -119
- package/runtime/sdk/net/core.ts +0 -384
- package/runtime/sdk/net/index.ts +0 -181
- package/runtime/sdk/net/replication.ts +0 -622
- package/runtime/sdk/plugins/camera.ts +0 -81
- package/runtime/sdk/plugins/geolocation.ts +0 -123
- package/runtime/sdk/plugins/oauth.ts +0 -61
- package/runtime/sdk/plugins/permission.ts +0 -7
- package/runtime/sdk/plugins/push.ts +0 -132
- package/runtime/sdk/plugins/qr.ts +0 -73
- package/runtime/sdk/plugins/service.ts +0 -47
- package/runtime/sdk/runtime/app.ts +0 -101
- package/runtime/sdk/runtime/appEvents.ts +0 -54
- package/runtime/sdk/runtime/channel.ts +0 -61
- package/runtime/sdk/runtime/clipboard.ts +0 -20
- package/runtime/sdk/runtime/datetime.ts +0 -329
- package/runtime/sdk/runtime/device.ts +0 -293
- package/runtime/sdk/runtime/fetch.ts +0 -77
- package/runtime/sdk/runtime/files.ts +0 -23
- package/runtime/sdk/runtime/input.ts +0 -175
- package/runtime/sdk/runtime/media.ts +0 -111
- package/runtime/sdk/runtime/misc.ts +0 -16
- package/runtime/sdk/runtime/net.ts +0 -36
- package/runtime/sdk/runtime/rpc.ts +0 -218
- package/runtime/sdk/runtime/service.ts +0 -83
- package/runtime/sdk/runtime/share.ts +0 -9
- package/runtime/sdk/runtime/storage.ts +0 -13
- package/runtime/sdk/runtime/touch.ts +0 -76
- package/runtime/sdk/scene/defineScene.ts +0 -1353
- package/runtime/sdk/scene/editorPlugins.ts +0 -110
- package/runtime/sdk/scene/gizmos.ts +0 -148
- package/runtime/sdk/scene/grammar.ts +0 -120
- package/runtime/sdk/scene/material.ts +0 -188
- package/runtime/sdk/server/auth/appConfig.ts +0 -12
- package/runtime/sdk/server/auth/global.ts +0 -80
- package/runtime/sdk/server/auth/host.ts +0 -318
- package/runtime/sdk/server/auth/models.ts +0 -83
- package/runtime/sdk/server/auth/types.ts +0 -50
- package/runtime/sdk/server/channel.ts +0 -56
- package/runtime/sdk/server/context.ts +0 -36
- package/runtime/sdk/server/db/defineDb.ts +0 -237
- package/runtime/sdk/server/db/fields.ts +0 -132
- package/runtime/sdk/server/db/httpTransport.ts +0 -93
- package/runtime/sdk/server/db/index.ts +0 -7
- package/runtime/sdk/server/db/marci/query.ts +0 -412
- package/runtime/sdk/server/db/types.ts +0 -202
- package/runtime/sdk/server/errors.ts +0 -12
- package/runtime/sdk/server/host.ts +0 -74
- package/runtime/sdk/server/inject.ts +0 -13
- package/runtime/sdk/server/runtime.ts +0 -133
- package/runtime/sdk/server/validate.ts +0 -87
- package/runtime/sdk/ui/NativeView.ts +0 -142
- package/runtime/sdk/ui/UI.ts +0 -39
- package/runtime/sdk/ui/UIBottomSheet.ts +0 -139
- package/runtime/sdk/ui/UIButton.ts +0 -101
- package/runtime/sdk/ui/UIContainer.ts +0 -60
- package/runtime/sdk/ui/UIImage.ts +0 -83
- package/runtime/sdk/ui/UIInput.ts +0 -185
- package/runtime/sdk/ui/UIModal.ts +0 -139
- package/runtime/sdk/ui/UINode.ts +0 -830
- package/runtime/sdk/ui/UIPager.ts +0 -362
- package/runtime/sdk/ui/UIPopover.ts +0 -100
- package/runtime/sdk/ui/UIScreen.ts +0 -123
- package/runtime/sdk/ui/UIScrollable.ts +0 -87
- package/runtime/sdk/ui/UISpacer.ts +0 -14
- package/runtime/sdk/ui/UITabs.ts +0 -236
- package/runtime/sdk/ui/UIText.ts +0 -51
- package/runtime/sdk/ui/UIVideo.ts +0 -88
- package/runtime/sdk/ui/UIVirtualizedList.ts +0 -241
- package/runtime/sdk/ui/UIWidget.ts +0 -127
- package/runtime/sdk/ui/fonts.ts +0 -13
- package/runtime/sdk/ui/presentable.ts +0 -117
- package/runtime/sdk/ui/router.ts +0 -132
- package/runtime/sdk/ui/theme.ts +0 -84
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* File templates `lecodes design init` scaffolds into the design folder. The strings live in
|
|
2
|
+
* lecodes-design (src/templates.ts, in the lecodes-tools repo) — the single source shared with the
|
|
3
|
+
* platform editor's "Create design" button. Imported by a RELATIVE path through the package's
|
|
4
|
+
* node_modules link (not as `lecodes-design/templates`) on purpose: the CLI build externalizes the
|
|
5
|
+
* `lecodes-design` package (it's an optional runtime install for `design serve`), but `init` must
|
|
6
|
+
* work without it — a relative import gets bundled into the binary like any local module. The link
|
|
7
|
+
* is the `link:lecodes-design` dev dependency (see the root `bun run link`). */
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
META_JSON, META_JSON_DESKTOP, SPEC_MD, TOKENS_TS, TABS_TS, HOME_SCREEN_TS, CLAUDE_MD, DESIGN_MACROS_DTS, README_HINT,
|
|
11
|
+
} from "../../node_modules/lecodes-design/src/templates"
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { CliError, c, flagBool, flagStr, log, note, type Args } from "../util"
|
|
2
|
+
import { findProjectRoot } from "../manifest"
|
|
3
|
+
import { buildBundle } from "./compile"
|
|
4
|
+
import { assertRendererSupportsBundle, readAppConfigAt, resolveDesktopRenderer } from "./appShared"
|
|
5
|
+
import { buildDesktop } from "./appDesktop"
|
|
6
|
+
import {
|
|
7
|
+
DESKTOP_RENDERER_VERSION,
|
|
8
|
+
fetchDesktopManifest,
|
|
9
|
+
findMacHostExe,
|
|
10
|
+
installedDesktopVersions,
|
|
11
|
+
runDesktopApp,
|
|
12
|
+
selectedDesktopVersion,
|
|
13
|
+
semverNewer,
|
|
14
|
+
updateDesktop,
|
|
15
|
+
} from "../desktopRenderer"
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
* `lecodes desktop` — everything about the desktop target in one place.
|
|
19
|
+
* (no args) status: the version renders use, what's cached, what's published
|
|
20
|
+
* run compile the current project and open it in a real desktop window
|
|
21
|
+
* build stage a portable app folder (the same command as `lecodes app build desktop`,
|
|
22
|
+
* which stays for symmetry with the ios/android shells)
|
|
23
|
+
* update move the host to the newest published release (downloads it if needed)
|
|
24
|
+
* The binary cache lives in ~/.lecodes/renderer/; LECODES_DESKTOP_EXE overrides everything.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** `--scale 2` / `--scale fit` / app.json desktop.window.scale → the host's scale knob
|
|
28
|
+
* ("system" and absent mean: let the host decide from the OS display scale). */
|
|
29
|
+
export const resolveScale = (flag: string | undefined, cfg: number | "fit" | "system" | undefined): number | "fit" | undefined => {
|
|
30
|
+
const raw = flag ?? cfg
|
|
31
|
+
if (raw === undefined || raw === "system") return undefined
|
|
32
|
+
if (raw === "fit") return "fit"
|
|
33
|
+
const n = Number(raw)
|
|
34
|
+
if (!Number.isFinite(n) || n < 0.5 || n > 4) {
|
|
35
|
+
throw new CliError(`scale must be a number 0.5..4, "fit" or "system" (got "${raw}")`)
|
|
36
|
+
}
|
|
37
|
+
return n
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const resolveRenderScale = (flag: string | undefined, cfg: number | undefined): number | undefined => {
|
|
41
|
+
const raw = flag ?? cfg
|
|
42
|
+
if (raw === undefined) return undefined
|
|
43
|
+
const n = Number(raw)
|
|
44
|
+
if (!Number.isFinite(n) || n < 0.25 || n > 1) throw new CliError(`render-scale must be a number 0.25..1 (got "${raw}")`)
|
|
45
|
+
return n
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const run = async (args: Args) => {
|
|
49
|
+
// Window defaults come from app.json's desktop block (flags win); standalone projects
|
|
50
|
+
// without an app.json keep the host's built-in 1280×720.
|
|
51
|
+
let root: string
|
|
52
|
+
try { root = findProjectRoot(process.cwd()) } catch { root = process.cwd() }
|
|
53
|
+
const appCfg = readAppConfigAt(root)
|
|
54
|
+
const win = appCfg?.desktop?.window
|
|
55
|
+
// ONE renderer decision per run: it picks the host binary AND which .filamat gets staged beside
|
|
56
|
+
// the bundle. Reading the config twice is how a Vulkan exe ends up next to an OpenGL shader.
|
|
57
|
+
const renderer = resolveDesktopRenderer(root, args)
|
|
58
|
+
const { js, resources } = await buildBundle(args, undefined, { localAssets: true, desktopRenderer: renderer })
|
|
59
|
+
assertRendererSupportsBundle(renderer, js)
|
|
60
|
+
note(`Launching LeCodes Desktop${renderer === "vulkan" ? " (Vulkan)" : ""}… (close the window to return)`)
|
|
61
|
+
await runDesktopApp(js, resources, {
|
|
62
|
+
renderer,
|
|
63
|
+
width: Number(flagStr(args, "width")) || win?.width,
|
|
64
|
+
height: Number(flagStr(args, "height")) || win?.height,
|
|
65
|
+
minWidth: win?.minWidth,
|
|
66
|
+
minHeight: win?.minHeight,
|
|
67
|
+
resizable: win?.resizable,
|
|
68
|
+
title: flagStr(args, "title") ?? win?.title ?? appCfg?.name,
|
|
69
|
+
// --fullscreen forces it on; --windowed forces it off (over app.json); else the config decides.
|
|
70
|
+
fullscreen: flagBool(args, "fullscreen") ? true : flagBool(args, "windowed") ? false : win?.fullscreen,
|
|
71
|
+
scale: resolveScale(flagStr(args, "scale"), win?.scale),
|
|
72
|
+
// 3D render resolution: --render-scale wins over app.json desktop.renderScale; the cap comes
|
|
73
|
+
// from app.json only. Both are desktop-level (the machine/app), not window-level.
|
|
74
|
+
renderScale: resolveRenderScale(flagStr(args, "render-scale"), appCfg?.desktop?.renderScale),
|
|
75
|
+
maxRenderSize: appCfg?.desktop?.maxRenderSize,
|
|
76
|
+
// --fps: the host's own corner counter. A flag and not app.json on purpose — it is something you
|
|
77
|
+
// switch on while looking at a frame, not a property of the app.
|
|
78
|
+
fps: flagBool(args, "fps"),
|
|
79
|
+
// Multiplayer (docs/multiplayer-plan.md): --server [--port N] [--max-clients N] starts the host as a
|
|
80
|
+
// dedicated server; --connect ip:port joins one. Both only set Net.launch — the app calls Net.listen/connect.
|
|
81
|
+
net: flagBool(args, "server")
|
|
82
|
+
? { role: "server", port: Number(flagStr(args, "port")) || undefined, maxClients: Number(flagStr(args, "max-clients")) || undefined }
|
|
83
|
+
: flagStr(args, "connect")
|
|
84
|
+
? { role: "client", address: flagStr(args, "connect") }
|
|
85
|
+
: undefined,
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const status = async () => {
|
|
90
|
+
const selected = selectedDesktopVersion()
|
|
91
|
+
const installed = installedDesktopVersions()
|
|
92
|
+
log(`renders with ${selected}${installed.includes(selected) ? "" : c.dim(" (not downloaded yet — fetched on first render)")}`)
|
|
93
|
+
log(`CLI minimum ${DESKTOP_RENDERER_VERSION}`)
|
|
94
|
+
log(`downloaded ${installed.length > 0 ? installed.join(", ") : c.dim("none")} ${c.dim("(~/.lecodes/renderer)")}`)
|
|
95
|
+
if (process.env.LECODES_DESKTOP_EXE) log(`override LECODES_DESKTOP_EXE=${process.env.LECODES_DESKTOP_EXE}`)
|
|
96
|
+
if (process.platform === "darwin") {
|
|
97
|
+
// The cache is what runs/builds use; a hand-installed app only steps in when the manifest is unreachable.
|
|
98
|
+
const installed = findMacHostExe()
|
|
99
|
+
log(`installed ${installed ?? c.dim("none")} ${c.dim("(offline fallback only)")}`)
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const { latest } = await fetchDesktopManifest()
|
|
103
|
+
const hint = semverNewer(selected, latest) > 0
|
|
104
|
+
? c.yellow("(newer — run `lecodes desktop update`)")
|
|
105
|
+
: latest === selected
|
|
106
|
+
? c.dim("(up to date)")
|
|
107
|
+
: c.dim("(older than the CLI's minimum — this desktop release isn't published yet)")
|
|
108
|
+
log(`published ${latest} ${hint}`)
|
|
109
|
+
} catch {
|
|
110
|
+
log(`published ${c.dim("unknown (couldn't reach the release manifest)")}`)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const desktop = async (args: Args) => {
|
|
115
|
+
const sub = args._[0]
|
|
116
|
+
if (sub === undefined) return status()
|
|
117
|
+
if (sub === "run") return run(args)
|
|
118
|
+
// `buildDesktop` reads only flags, never positionals, so it takes both spellings unchanged.
|
|
119
|
+
if (sub === "build") return buildDesktop(args)
|
|
120
|
+
if (sub === "update") return updateDesktop()
|
|
121
|
+
throw new CliError(`Unknown subcommand "desktop ${sub}". Use \`lecodes desktop\` (status), \`lecodes desktop run\`, \`lecodes desktop build\` or \`lecodes desktop update\`.`)
|
|
122
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, watch, writeFileSync, type FSWatcher } from "node:fs"
|
|
2
|
+
import { randomBytes } from "node:crypto"
|
|
3
|
+
import { networkInterfaces } from "node:os"
|
|
4
|
+
import { basename, join } from "node:path"
|
|
5
|
+
import { findProjectRoot, readManifest, LECODES_DIR, type Manifest } from "../manifest"
|
|
6
|
+
import { loadIgnore } from "../ignore"
|
|
7
|
+
import { startDevServer } from "../dev/devServer"
|
|
8
|
+
import { runDesktopDev } from "../desktopRenderer"
|
|
9
|
+
import { readAppConfigAt } from "./appShared"
|
|
10
|
+
import { resolveScale } from "./desktop"
|
|
11
|
+
import { CliError, c, flagBool, flagStr, log, note, toKey, warnErr, type Args } from "../util"
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* `lecodes dev` — run the project on a device straight from this machine, with hot reload.
|
|
15
|
+
*
|
|
16
|
+
* Starts a LAN dev server (compile + assets + reload channel), prints a QR code, and watches the
|
|
17
|
+
* working tree. The device opens the QR URL like any project URL; every save recompiles and
|
|
18
|
+
* hot-swaps the running app (`_creatorUtils.run` — fresh context, rollback on error). Device
|
|
19
|
+
* console output and errors stream back to this terminal. Fully local: no login, no le.codes
|
|
20
|
+
* backend. Custom shaders compile locally too (matc, downloaded on first use — matcTool.ts);
|
|
21
|
+
* only when matc can't be fetched do they fall back to server-compiled artifacts (push once).
|
|
22
|
+
*
|
|
23
|
+
* Works inside a cloned project or standalone in any folder with an entry .ts file.
|
|
24
|
+
* The desktop host speaks websocket since 1.0.0 (host_ws); `?t=poll` remains the fallback
|
|
25
|
+
* for older desktop builds (or any host without a websocket bridge).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const DEFAULT_PORT = 4499
|
|
29
|
+
const DEBOUNCE_MS = 120
|
|
30
|
+
|
|
31
|
+
// Never trigger a recompile (mirrors scanLocal's skip lists + our own artifacts).
|
|
32
|
+
const WATCH_SKIP_DIRS = new Set([".lecodes", ".git", "node_modules"])
|
|
33
|
+
const WATCH_SKIP_FILES = new Set(["tsconfig.json", "jsconfig.json", ".gitignore", ".lecodesignore", ".DS_Store"])
|
|
34
|
+
|
|
35
|
+
/*
|
|
36
|
+
* The origin a phone on the same Wi-Fi can reach, best first.
|
|
37
|
+
*
|
|
38
|
+
* `net.internal` is NOT enough on its own: in Node it only means loopback, so a WSL/Docker/Hyper-V
|
|
39
|
+
* bridge or a VPN tap counts as a normal external interface and wins whenever the OS enumerates it
|
|
40
|
+
* first. Those are host-only — the device dials them and hangs until the socket times out, which is
|
|
41
|
+
* a confusing way to learn your dev URL was never reachable. So rank instead of taking the first:
|
|
42
|
+
* de-prioritise virtual-looking adapter names, then Docker/WSL's 172.16–31 block, then plain
|
|
43
|
+
* 169.254 link-local. Ties keep OS order. Anything genuinely ambiguous (Ethernet AND Wi-Fi on
|
|
44
|
+
* different subnets, as here) is unknowable from this side — hence `lanCandidates` in the printout
|
|
45
|
+
* and `--host`.
|
|
46
|
+
*/
|
|
47
|
+
const VIRTUAL_IFACE = /wsl|docker|hyper-v|vethernet|virtualbox|vmware|tap|tun|zerotier|tailscale|loopback/i
|
|
48
|
+
|
|
49
|
+
const lanCandidates = (): { address: string, iface: string }[] => {
|
|
50
|
+
const nets = networkInterfaces()
|
|
51
|
+
const out: { address: string, iface: string, rank: number }[] = []
|
|
52
|
+
for (const iface of Object.keys(nets)) {
|
|
53
|
+
for (const net of nets[iface] ?? []) {
|
|
54
|
+
if (net.family !== "IPv4" || net.internal) continue
|
|
55
|
+
const a = net.address
|
|
56
|
+
let rank = 0
|
|
57
|
+
if (VIRTUAL_IFACE.test(iface)) rank += 4
|
|
58
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(a)) rank += 2 // Docker/WSL default pools live here
|
|
59
|
+
if (a.startsWith("169.254.")) rank += 8 // link-local: no DHCP, rarely routable
|
|
60
|
+
out.push({ address: a, iface, rank })
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return out.sort((x, y) => x.rank - y.rank).map(({ address, iface }) => ({ address, iface }))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The per-project dev token, persisted so the QR/URL stays stable across runs. */
|
|
67
|
+
const loadDevToken = (root: string): string => {
|
|
68
|
+
const path = join(root, LECODES_DIR, "dev.json")
|
|
69
|
+
try {
|
|
70
|
+
const token = (JSON.parse(readFileSync(path, "utf8")) as { token?: string }).token
|
|
71
|
+
if (token && /^[a-z0-9]{8,}$/.test(token)) return token
|
|
72
|
+
} catch { /* first run */ }
|
|
73
|
+
const token = randomBytes(6).toString("hex")
|
|
74
|
+
mkdirSync(join(root, LECODES_DIR), { recursive: true })
|
|
75
|
+
writeFileSync(path, JSON.stringify({ token }, null, 2) + "\n")
|
|
76
|
+
return token
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Print a scannable QR for the URL (qrcode-terminal is bundled; URL-only if it ever fails). */
|
|
80
|
+
const printQr = async (url: string) => {
|
|
81
|
+
try {
|
|
82
|
+
const qrcode = (await import("qrcode-terminal")).default
|
|
83
|
+
await new Promise<void>((resolve) => qrcode.generate(url, { small: true }, (out: string) => { log(out); resolve() }))
|
|
84
|
+
} catch {
|
|
85
|
+
note("(couldn't render a QR code — open the URL below on the device)")
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const dev = async (args: Args) => {
|
|
90
|
+
// Inside a cloned project the server runs at its root; anywhere else it runs standalone against
|
|
91
|
+
// the current directory (no login / manifest needed) — same rule as `lecodes design`.
|
|
92
|
+
let root: string
|
|
93
|
+
let manifest: Manifest | null
|
|
94
|
+
try {
|
|
95
|
+
root = findProjectRoot(process.cwd())
|
|
96
|
+
manifest = readManifest(root)
|
|
97
|
+
} catch {
|
|
98
|
+
root = process.cwd()
|
|
99
|
+
manifest = null
|
|
100
|
+
}
|
|
101
|
+
const name = manifest?.name ?? basename(root)
|
|
102
|
+
|
|
103
|
+
// Bun's node:http can't complete a websocket upgrade (writes to the upgraded socket are dropped),
|
|
104
|
+
// so the reload push would silently never reach devices. Node is the shipped runtime.
|
|
105
|
+
if (typeof (globalThis as { Bun?: unknown }).Bun !== "undefined") {
|
|
106
|
+
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).")
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const port = Number(flagStr(args, "port")) || DEFAULT_PORT
|
|
110
|
+
const candidates = lanCandidates()
|
|
111
|
+
const host = flagStr(args, "host") ?? candidates[0]?.address
|
|
112
|
+
if (!host) throw new CliError("No LAN address found — pass one with --host <ip>.")
|
|
113
|
+
// A machine with several NICs can only be disambiguated by the human: name the runners-up so a
|
|
114
|
+
// device that can't reach the printed URL is one --host away from working, not a debugging session.
|
|
115
|
+
const alternatives = candidates.filter((c) => c.address !== host)
|
|
116
|
+
const origin = `http://${host}:${port}`
|
|
117
|
+
const token = loadDevToken(root)
|
|
118
|
+
|
|
119
|
+
const server = await startDevServer({
|
|
120
|
+
root, manifest, name, origin, port, token,
|
|
121
|
+
entryOverride: flagStr(args, "entry"),
|
|
122
|
+
log: note,
|
|
123
|
+
}).catch((e: unknown) => {
|
|
124
|
+
const msg = e instanceof Error ? e.message : String(e)
|
|
125
|
+
throw new CliError(msg.includes("EADDRINUSE") ? `Port ${port} is already in use — pass --port <n>.` : msg)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
note("Compiling…")
|
|
129
|
+
const first = await server.recompile()
|
|
130
|
+
if (first.ok) {
|
|
131
|
+
for (const w of first.warnings) warnErr(w)
|
|
132
|
+
} else {
|
|
133
|
+
warnErr(`Compile failed — fix it and save; devices connect once it compiles:\n${first.error}`)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---- watcher: debounced whole-project recompile; a successful compile broadcasts the reload ----
|
|
137
|
+
const ignore = loadIgnore(root)
|
|
138
|
+
const shouldRecompile = (file: string | null): boolean => {
|
|
139
|
+
if (file === null) return true // event without a filename — assume project content
|
|
140
|
+
const parts = file.replace(/\\/g, "/").split("/")
|
|
141
|
+
if (parts.some((p) => WATCH_SKIP_DIRS.has(p))) return false
|
|
142
|
+
if (WATCH_SKIP_FILES.has(parts[parts.length - 1])) return false
|
|
143
|
+
try {
|
|
144
|
+
const key = toKey(file)
|
|
145
|
+
if (ignore(key, false)) return false
|
|
146
|
+
} catch { return false }
|
|
147
|
+
return true
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const debug = process.env.LECODES_DEV_DEBUG === "1"
|
|
151
|
+
let timer: NodeJS.Timeout | null = null
|
|
152
|
+
const onChange = (file: string | null) => {
|
|
153
|
+
if (debug) note(`[watch] event: ${file ?? "(null)"} → ${shouldRecompile(file) ? "recompile" : "skip"}`)
|
|
154
|
+
if (!shouldRecompile(file)) return
|
|
155
|
+
if (timer) clearTimeout(timer)
|
|
156
|
+
timer = setTimeout(async () => {
|
|
157
|
+
timer = null
|
|
158
|
+
const result = await server.recompile()
|
|
159
|
+
if (!result.ok) {
|
|
160
|
+
warnErr(`Compile failed (devices keep the last good version):\n${result.error}`)
|
|
161
|
+
} else if (result.changed) {
|
|
162
|
+
const devices = server.clientCount()
|
|
163
|
+
note(`✓ v${result.v} → ${devices} device${devices === 1 ? "" : "s"}`)
|
|
164
|
+
for (const w of result.warnings) warnErr(w)
|
|
165
|
+
}
|
|
166
|
+
}, DEBOUNCE_MS)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let watcher: FSWatcher | null = null
|
|
170
|
+
try {
|
|
171
|
+
watcher = watch(root, { recursive: true }, (_type, file) => onChange(file))
|
|
172
|
+
} catch {
|
|
173
|
+
warnErr("fs.watch(recursive) unavailable on this platform — hot reload disabled (restart to pick up changes).")
|
|
174
|
+
}
|
|
175
|
+
watcher?.on("error", (e) => warnErr(`Watcher error — hot reload may have stopped (restart lecodes dev): ${e.message}`))
|
|
176
|
+
|
|
177
|
+
// ---- banner ------------------------------------------------------------------------------------
|
|
178
|
+
log("")
|
|
179
|
+
log(`${c.bold("LeCodes Dev")} — ${name}`)
|
|
180
|
+
log("")
|
|
181
|
+
if (!flagBool(args, "no-qr")) await printQr(server.url)
|
|
182
|
+
log(`Scan the QR with the LeCodes app, or open: ${c.bold(server.url)}`)
|
|
183
|
+
note(`Older desktop builds (no websocket bridge): ${server.url}?t=poll`)
|
|
184
|
+
if (alternatives.length > 0) {
|
|
185
|
+
note(`Device can't reach it? This machine also has ${alternatives.map((a) => `${a.address} (${a.iface})`).join(", ")} — retry with --host <ip>.`)
|
|
186
|
+
}
|
|
187
|
+
if (existsSync(join(root, LECODES_DIR, "manifest.json"))) note(`Token stored in ${LECODES_DIR}/dev.json (delete it to rotate the URL).`)
|
|
188
|
+
log("")
|
|
189
|
+
log(`Watching ${root} — save a file to hot-reload. Ctrl+C to stop.`)
|
|
190
|
+
|
|
191
|
+
// --desktop: open the dev URL in the native host too (same server, same hot reload — the host
|
|
192
|
+
// speaks the websocket channel). Window config comes from app.json's desktop block. The window
|
|
193
|
+
// closing does NOT stop the server: phones stay connected, and re-running with --desktop is the
|
|
194
|
+
// reopen. Errors (host not downloadable, unsupported OS) degrade to a warning — the QR path
|
|
195
|
+
// must keep working.
|
|
196
|
+
if (flagBool(args, "desktop")) {
|
|
197
|
+
const appCfg = readAppConfigAt(root)
|
|
198
|
+
const win = appCfg?.desktop?.window
|
|
199
|
+
note("Opening LeCodes Desktop against the dev server…")
|
|
200
|
+
// scale IS forwarded (density changes how the app looks — dev should match production);
|
|
201
|
+
// fullscreen deliberately is NOT (a fullscreen dev window hides the terminal).
|
|
202
|
+
void runDesktopDev(server.url, {
|
|
203
|
+
width: win?.width, height: win?.height,
|
|
204
|
+
minWidth: win?.minWidth, minHeight: win?.minHeight,
|
|
205
|
+
resizable: win?.resizable,
|
|
206
|
+
title: win?.title ?? appCfg?.name ?? name,
|
|
207
|
+
scale: resolveScale(undefined, win?.scale),
|
|
208
|
+
fps: flagBool(args, "fps"),
|
|
209
|
+
}).then(
|
|
210
|
+
() => note("Desktop window closed — the dev server keeps running (Ctrl+C to stop)."),
|
|
211
|
+
(e: unknown) => warnErr(`Desktop host: ${e instanceof Error ? e.message : String(e)}`),
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { getProject } from "../api"
|
|
2
|
+
import { loadConfig, requireApiUrl, requireToken } from "../config"
|
|
3
|
+
import { findProjectRoot, readManifest } from "../manifest"
|
|
4
|
+
import { scanProject } from "../localFiles"
|
|
5
|
+
import { computeServerDiff, type ServerDiffEntry } from "../serverDiff"
|
|
6
|
+
import { unifiedDiff } from "../textDiff"
|
|
7
|
+
import { c, info, log, flagBool, toKey, type Args } from "../util"
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* `lecodes diff [path…]` — compare the local working tree against the project's CURRENT server state
|
|
11
|
+
* and print a unified diff. The "+" side is local, the "-" side is the server, so it answers "what
|
|
12
|
+
* would my push change?" — including any edits made on the server (e.g. in the web editor) since you
|
|
13
|
+
* cloned. The headline use is conflict resolution before `push --force`.
|
|
14
|
+
*
|
|
15
|
+
* --name-only list changed paths + status instead of full diffs
|
|
16
|
+
* [path…] limit to these files / folders
|
|
17
|
+
*/
|
|
18
|
+
export const diff = async (args: Args) => {
|
|
19
|
+
const root = findProjectRoot(process.cwd())
|
|
20
|
+
const manifest = readManifest(root)
|
|
21
|
+
const config = loadConfig()
|
|
22
|
+
const apiUrl = requireApiUrl(config)
|
|
23
|
+
const token = requireToken(config)
|
|
24
|
+
|
|
25
|
+
info(c.dim("Comparing with the server…"))
|
|
26
|
+
const project = await getProject(apiUrl, token, manifest.uuid)
|
|
27
|
+
const scan = scanProject(root, manifest)
|
|
28
|
+
let entries = computeServerDiff(project, scan)
|
|
29
|
+
|
|
30
|
+
// Optional path filter (a file, or a folder prefix).
|
|
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
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const e of entries) {
|
|
47
|
+
if (e.binary) {
|
|
48
|
+
log(statusLabel(e) + e.path + c.dim(" (binary — content not shown)"))
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
const d = unifiedDiff(e.serverText ?? "", e.localText ?? "", `server${e.path}`, `local${e.path}`)
|
|
52
|
+
if (d) log("\n" + d)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const statusLabel = (e: ServerDiffEntry): string =>
|
|
57
|
+
e.status === "added" ? c.green(" added ")
|
|
58
|
+
: e.status === "removed" ? c.red(" removed ")
|
|
59
|
+
: c.yellow(" modified ")
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path"
|
|
3
|
+
import { LECODES_DIR } from "../manifest"
|
|
4
|
+
import { materializeTypesLocal, writeTsconfig } from "../types"
|
|
5
|
+
import { DEFAULT_IGNORE_FILE, ensureGitignore } from "../ignore"
|
|
6
|
+
import { appClaudeMd, CLAUDE_SETTINGS, STARTER_FLOW } from "./projectTemplates"
|
|
7
|
+
import { c, flagBool, log, note, success, warnErr, type Args } from "../util"
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* `lecodes init [dir]` — scaffold a brand-new LOCAL project: no account, no login, no server.
|
|
11
|
+
*
|
|
12
|
+
* Writes a starter entry, a CLAUDE.md (the headless render/test workflow for AI assistants) with
|
|
13
|
+
* a passing starter flow test, the SDK's import-free type surface + a tsconfig (so VS Code gives
|
|
14
|
+
* full IntelliSense offline), and a `.lecodesignore`. The result runs immediately with `lecodes dev`
|
|
15
|
+
* (hot reload on a device) and compiles with `lecodes compile -o app.js` (a self-contained bundle
|
|
16
|
+
* for LeCodes Desktop). It's the offline counterpart to `lecodes create`, which makes a project on
|
|
17
|
+
* the server; a local project needs no manifest — `dev`/`compile` compile straight from the tree.
|
|
18
|
+
*
|
|
19
|
+
* Idempotent: existing files are kept (never clobbered), so it's safe to re-run or run in a folder
|
|
20
|
+
* that already has content. Only the generated tsconfig + `.lecodes/types/` are rewritten each run.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const STARTER = `// Welcome to LeCodes — the whole SDK is available as globals, no imports needed.
|
|
24
|
+
// Run it live on a device with \`lecodes dev\`, or bundle it with \`lecodes compile -o app.js\`.
|
|
25
|
+
|
|
26
|
+
let count = 0
|
|
27
|
+
|
|
28
|
+
const counter = UIText({ color: "#9fb0d0", fontSize: 22, marginTop: 18 }, "Taps: 0")
|
|
29
|
+
|
|
30
|
+
const button = UIButton(
|
|
31
|
+
{
|
|
32
|
+
backgroundColor: "#4f7cff",
|
|
33
|
+
borderRadius: 14,
|
|
34
|
+
paddingVertical: 16,
|
|
35
|
+
paddingHorizontal: 40,
|
|
36
|
+
marginTop: 24,
|
|
37
|
+
alignItems: "center",
|
|
38
|
+
justifyContent: "center",
|
|
39
|
+
},
|
|
40
|
+
[UIText({ color: "#ffffff", fontSize: 22 }, "Tap me")],
|
|
41
|
+
)
|
|
42
|
+
.style({ name: "tap" }) // names make elements targetable in tests/*.flow.json (see CLAUDE.md)
|
|
43
|
+
.onClick(() => {
|
|
44
|
+
count++
|
|
45
|
+
counter.text = "Taps: " + count
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const screen = UIScreen(
|
|
49
|
+
{ backgroundColor: "#1e2230", alignItems: "center", justifyContent: "center", padding: 40 },
|
|
50
|
+
[
|
|
51
|
+
UIColumn(
|
|
52
|
+
{ backgroundColor: "#2b3550", borderRadius: 24, padding: 40, alignItems: "center" },
|
|
53
|
+
[UIText({ color: "#ffffff", fontSize: 32 }, "Hello, LeCodes"), button, counter],
|
|
54
|
+
),
|
|
55
|
+
],
|
|
56
|
+
).style({ name: "home" }) // the screen root's name is what tests' \`expect: { screen: … }\` reads
|
|
57
|
+
|
|
58
|
+
screen.open()
|
|
59
|
+
`
|
|
60
|
+
|
|
61
|
+
/** `--desktop`: declare the desktop target up front. Window sizes are logical units (the host
|
|
62
|
+
* scales for HiDPI); `dev --desktop` / `desktop run` / `render --desktop` all read this, and a
|
|
63
|
+
* desktop-only app.json makes headless render/test default to the window size too. */
|
|
64
|
+
const appJsonDesktop = (name: string) => JSON.stringify({
|
|
65
|
+
name,
|
|
66
|
+
desktop: {
|
|
67
|
+
window: { width: 1280, height: 800, minWidth: 640, minHeight: 480 },
|
|
68
|
+
},
|
|
69
|
+
}, null, 2) + "\n"
|
|
70
|
+
|
|
71
|
+
const readmeTemplate = (name: string) => `# ${name}
|
|
72
|
+
|
|
73
|
+
A LeCodes app. The whole SDK is available as globals (\`UIScreen\`, \`UIText\`, \`Scene2D\`, …) —
|
|
74
|
+
no imports needed. \`main.ts\` is the entry point.
|
|
75
|
+
|
|
76
|
+
## Run it
|
|
77
|
+
|
|
78
|
+
\`\`\`
|
|
79
|
+
lecodes dev # live on your phone: scan the QR with the LeCodes app, hot reload
|
|
80
|
+
lecodes compile -o app.js # bundle to a single self-contained JS file
|
|
81
|
+
\`\`\`
|
|
82
|
+
|
|
83
|
+
## Test it (no device needed)
|
|
84
|
+
|
|
85
|
+
\`\`\`
|
|
86
|
+
lecodes render --png shot.png # headless screenshot of the running app
|
|
87
|
+
lecodes test # run tests/*.flow.json — the starter ships with a passing one
|
|
88
|
+
\`\`\`
|
|
89
|
+
|
|
90
|
+
Scenario format + conventions: see [CLAUDE.md](./CLAUDE.md) (written for AI assistants, useful
|
|
91
|
+
for humans too).
|
|
92
|
+
|
|
93
|
+
## Put it on the platform
|
|
94
|
+
|
|
95
|
+
This project is local-only. To give it a home on le.codes (web editor, sharing, OTA updates):
|
|
96
|
+
|
|
97
|
+
\`\`\`
|
|
98
|
+
lecodes login # once per machine
|
|
99
|
+
lecodes link # create a project from this folder and connect it
|
|
100
|
+
lecodes push -m "initial" # upload it as the first checkpoint (--publish to go live)
|
|
101
|
+
\`\`\`
|
|
102
|
+
|
|
103
|
+
Already made the project in the web editor? \`lecodes link --project <uuid>\` adopts it — it
|
|
104
|
+
shows what your next push would change there before connecting anything.
|
|
105
|
+
|
|
106
|
+
## Go native
|
|
107
|
+
|
|
108
|
+
\`\`\`
|
|
109
|
+
lecodes app init ios # generate a native Xcode shell in ios/
|
|
110
|
+
lecodes app sync # embed the compiled bundle + plugins into it
|
|
111
|
+
lecodes app open # open it in Xcode, build & run
|
|
112
|
+
\`\`\`
|
|
113
|
+
|
|
114
|
+
## Layout
|
|
115
|
+
|
|
116
|
+
- \`main.ts\` — the app's entry point. Add more .ts files freely and import them from
|
|
117
|
+
\`main.ts\`; a file named \`main.ts\` is always the entry.
|
|
118
|
+
- \`tests/\` — headless flow tests (\`lecodes test\`); failing steps dump the frame to
|
|
119
|
+
\`tests/.artifacts/\`
|
|
120
|
+
- \`CLAUDE.md\` — guidance for AI assistants (the headless feedback loop + testability conventions)
|
|
121
|
+
- \`assets/\` — images/fonts the app references (created on demand)
|
|
122
|
+
- \`tsconfig.json\` + \`.lecodes/types/\` — generated IDE types (rewritten by the CLI; don't edit).
|
|
123
|
+
They come from the SDK bundled in the CLI; \`lecodes update\` refreshes them from the platform.
|
|
124
|
+
- \`.lecodes/\` is gitignored — per-checkout state (those types + \`manifest.json\`, this machine's
|
|
125
|
+
sync cursor once linked). After a fresh git clone: \`lecodes update\`, then
|
|
126
|
+
\`lecodes link --project <uuid>\` to re-attach.
|
|
127
|
+
- \`.lecodesignore\` — files a future \`lecodes push\` would skip
|
|
128
|
+
|
|
129
|
+
Full command list: \`lecodes --help\`.
|
|
130
|
+
`
|
|
131
|
+
|
|
132
|
+
export const init = async (args: Args) => {
|
|
133
|
+
const dirArg = (args._[0] ?? ".").replace(/[/\\]+$/, "") || "."
|
|
134
|
+
const root = resolve(dirArg)
|
|
135
|
+
const name = basename(root)
|
|
136
|
+
|
|
137
|
+
mkdirSync(root, { recursive: true })
|
|
138
|
+
|
|
139
|
+
let created = 0
|
|
140
|
+
const write = (rel: string, content: string) => {
|
|
141
|
+
const abs = join(root, rel)
|
|
142
|
+
if (existsSync(abs)) { note(`kept ${rel}`); return }
|
|
143
|
+
mkdirSync(dirname(abs), { recursive: true })
|
|
144
|
+
writeFileSync(abs, content)
|
|
145
|
+
log(`${c.green("created")} ${rel}`)
|
|
146
|
+
created++
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const forDesktop = flagBool(args, "desktop")
|
|
150
|
+
|
|
151
|
+
// Starter entry — a root file named main.ts is always the detected entrypoint.
|
|
152
|
+
write("main.ts", STARTER)
|
|
153
|
+
write("README.md", readmeTemplate(name))
|
|
154
|
+
// --desktop: an app.json declaring the desktop target (window config in logical units).
|
|
155
|
+
if (forDesktop) write("app.json", appJsonDesktop(name))
|
|
156
|
+
// AI-assistant guidance + a passing flow test: the headless loop is discoverable from day one.
|
|
157
|
+
write("CLAUDE.md", appClaudeMd(name))
|
|
158
|
+
write(".claude/settings.json", CLAUDE_SETTINGS)
|
|
159
|
+
write("tests/counter.flow.json", STARTER_FLOW)
|
|
160
|
+
// Keep local render/preview outputs (screenshot.png, shots/) out of any future push.
|
|
161
|
+
write(".lecodesignore", DEFAULT_IGNORE_FILE)
|
|
162
|
+
// .lecodes/ is per-checkout state (generated types + the push cursor) — keep it out of git.
|
|
163
|
+
const gi = ensureGitignore(root)
|
|
164
|
+
if (gi === "created") { log(`${c.green("created")} .gitignore`); created++ }
|
|
165
|
+
else if (gi === "updated") log(`${c.green("updated")} .gitignore ${c.dim("(+ .lecodes/)")}`)
|
|
166
|
+
else note("kept .gitignore")
|
|
167
|
+
|
|
168
|
+
// IDE types: the SDK's import-free global surface + a tsconfig, so VS Code resolves UIScreen,
|
|
169
|
+
// Scene2D, setLoop, … with no server. Regenerated every run to pick up CLI upgrades.
|
|
170
|
+
const wroteTypes = materializeTypesLocal(root)
|
|
171
|
+
writeTsconfig(root)
|
|
172
|
+
log(`${c.green("written")} tsconfig.json${wroteTypes ? ` + ${LECODES_DIR}/types/` : ""} (IDE types)`)
|
|
173
|
+
if (!wroteTypes) {
|
|
174
|
+
warnErr("Bundled SDK types not found — tsconfig written, but globals may show unresolved until the CLI is rebuilt with vendored types.")
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
log("")
|
|
178
|
+
success(created
|
|
179
|
+
? `Local project ${c.bold(name)} ready${dirArg === "." ? "" : ` in ${c.bold(dirArg)}/`}.`
|
|
180
|
+
: `Project ${c.bold(name)} already initialized.`)
|
|
181
|
+
const cd = dirArg === "." ? "" : `cd ${dirArg} && `
|
|
182
|
+
if (forDesktop) {
|
|
183
|
+
log(` ${c.bold(cd + "lecodes dev --desktop")} run it in a desktop window with hot reload`)
|
|
184
|
+
log(` ${c.bold(cd + "lecodes test")} run the flow tests headless (desktop-sized)`)
|
|
185
|
+
log(` ${c.bold(cd + "lecodes desktop run")} open the compiled app in the desktop host`)
|
|
186
|
+
} else {
|
|
187
|
+
log(` ${c.bold(cd + "lecodes dev")} run it on a device with hot reload`)
|
|
188
|
+
log(` ${c.bold(cd + "lecodes test")} run the flow tests headless (no device)`)
|
|
189
|
+
log(` ${c.bold(cd + "lecodes compile -o app.js")} bundle it for LeCodes Desktop`)
|
|
190
|
+
}
|
|
191
|
+
}
|