lecodes-cli 2.4.2 → 2.4.3
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 +20169 -12323
- package/package.json +8 -8
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/VERSION.json +3 -3
- package/runtime/web/player.js +2 -2
- package/runtime/web-lite/lite.js +1 -1
- package/src/commands/app/android.ts +68 -19
- package/src/commands/app/index.ts +58 -8
- package/src/commands/app/shared.ts +85 -2
- package/src/commands/app/splash.ts +251 -0
- package/src/commands/app/templates/android.ts +148 -25
- package/src/commands/app/templates/ios.ts +123 -12
- package/src/commands/init/index.ts +195 -195
- package/src/commands/shared.ts +79 -79
- package/src/commands/thumbs.ts +185 -185
- package/src/compile/screenEntry.ts +123 -123
- package/src/plugin/emitPackage.ts +18 -5
- package/src/plugin/generate.ts +11 -3
- package/src/plugin/manifest.ts +33 -4
- package/src/project/ignore.ts +164 -164
|
@@ -636,6 +636,69 @@ export const LAUNCH_STORYBOARD = `<?xml version="1.0" encoding="UTF-8" standalon
|
|
|
636
636
|
</document>
|
|
637
637
|
`
|
|
638
638
|
|
|
639
|
+
/** App/Base.lproj/LaunchScreen.storyboard as sync writes it when app.json sets "splash" (the file
|
|
640
|
+
* is CLI-owned then; without the key it stays the person's, LAUNCH_STORYBOARD of init): the view's
|
|
641
|
+
* colour is the colorset LeCodesSplashBackground, the image — when there is one — a size×size pt
|
|
642
|
+
* UIImageView (aspect fit; "splash.size", 160 by default) of the imageset LeCodesSplash centred on
|
|
643
|
+
* the view. Both live in Assets.xcassets, which
|
|
644
|
+
* the project already compiles, so no pbxproj edit. `rgb` (0xRRGGBB) only feeds the storyboard's
|
|
645
|
+
* resource cache (Interface Builder's preview); the app reads the colorset. LeCodesShellViewController
|
|
646
|
+
* mounts this same storyboard over its container until the first frame. */
|
|
647
|
+
export const splashStoryboardTemplate = (image: boolean, rgb: number, size = 160): string => {
|
|
648
|
+
const channel = (shift: number) => String(((rgb >> shift) & 0xff) / 255)
|
|
649
|
+
// The frame is Interface Builder's preview on its 375×667 canvas; the constraints place the view.
|
|
650
|
+
const x = (375 - size) / 2, y = (667 - size) / 2
|
|
651
|
+
const imageView = image ? `
|
|
652
|
+
<subviews>
|
|
653
|
+
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="LeCodesSplash" translatesAutoresizingMaskIntoConstraints="NO" id="LcS-im-001">
|
|
654
|
+
<rect key="frame" x="${x}" y="${y}" width="${size}" height="${size}"/>
|
|
655
|
+
<constraints>
|
|
656
|
+
<constraint firstAttribute="width" constant="${size}" id="LcS-cw-001"/>
|
|
657
|
+
<constraint firstAttribute="height" constant="${size}" id="LcS-ch-001"/>
|
|
658
|
+
</constraints>
|
|
659
|
+
</imageView>
|
|
660
|
+
</subviews>` : ""
|
|
661
|
+
const centre = image ? `
|
|
662
|
+
<constraints>
|
|
663
|
+
<constraint firstItem="LcS-im-001" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="LcS-cx-001"/>
|
|
664
|
+
<constraint firstItem="LcS-im-001" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="LcS-cy-001"/>
|
|
665
|
+
</constraints>` : ""
|
|
666
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
667
|
+
<!-- Generated by \`lecodes app sync\` from app.json "splash" — regenerated on every sync, do not edit. -->
|
|
668
|
+
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
|
669
|
+
<dependencies>
|
|
670
|
+
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
|
671
|
+
<capability name="Named colors" minToolsVersion="9.0"/>
|
|
672
|
+
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
|
673
|
+
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
|
674
|
+
</dependencies>
|
|
675
|
+
<scenes>
|
|
676
|
+
<!--View Controller-->
|
|
677
|
+
<scene sceneID="EHf-IW-A2E">
|
|
678
|
+
<objects>
|
|
679
|
+
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
|
680
|
+
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
|
681
|
+
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
|
682
|
+
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>${imageView}
|
|
683
|
+
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
|
684
|
+
<color key="backgroundColor" name="LeCodesSplashBackground"/>${centre}
|
|
685
|
+
</view>
|
|
686
|
+
</viewController>
|
|
687
|
+
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
|
688
|
+
</objects>
|
|
689
|
+
<point key="canvasLocation" x="53" y="375"/>
|
|
690
|
+
</scene>
|
|
691
|
+
</scenes>
|
|
692
|
+
<resources>${image ? `
|
|
693
|
+
<image name="LeCodesSplash" width="${size}" height="${size}"/>` : ""}
|
|
694
|
+
<namedColor name="LeCodesSplashBackground">
|
|
695
|
+
<color red="${channel(16)}" green="${channel(8)}" blue="${channel(0)}" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
|
696
|
+
</namedColor>
|
|
697
|
+
</resources>
|
|
698
|
+
</document>
|
|
699
|
+
`
|
|
700
|
+
}
|
|
701
|
+
|
|
639
702
|
export const ASSETS_ROOT_JSON = `{
|
|
640
703
|
"info" : {
|
|
641
704
|
"author" : "xcode",
|
|
@@ -703,8 +766,16 @@ export interface RuntimePlugin {
|
|
|
703
766
|
id: string
|
|
704
767
|
/** Swift packages the target links (manifest ios.packages). */
|
|
705
768
|
packages?: IosPackage[]
|
|
769
|
+
/** The binary targets the target links (manifest ios.binaries), by name: the xcframeworks sync
|
|
770
|
+
* copies to `runtimeBinaryPath(id, name)`. */
|
|
771
|
+
binaries?: string[]
|
|
706
772
|
}
|
|
707
773
|
|
|
774
|
+
/** Where sync copies a plugin's prebuilt xcframework (manifest ios.binaries), relative to the
|
|
775
|
+
* LeCodesRuntime package: inside it, as SwiftPM takes a binary target's path, beside the vendored
|
|
776
|
+
* sources and with the same lifecycle (replaced on every sync, removed with the plugin). */
|
|
777
|
+
export const runtimeBinaryPath = (id: string, name: string): string => `Binaries/${id}/${name}.xcframework`
|
|
778
|
+
|
|
708
779
|
/** The SDK's engine variants — one tag of lecodes-ios-sdk per variant (`<version>` = full,
|
|
709
780
|
* `<version>-3d`, `-2d`, `-core`): core = UI only, 2d = + the 2D engine, 3d = + Filament with Jolt,
|
|
710
781
|
* full = everything. The Swift sources are the same; the engine binary differs. */
|
|
@@ -722,8 +793,9 @@ export const sdkProducts = (variant: SdkVariant): string[] =>
|
|
|
722
793
|
/** LeCodesRuntime/Package.swift — the CLI-owned dependency graph: the SDK (one tag per
|
|
723
794
|
* variant, pinned with `.exact(…)`: the variant is the engine binary the tag's manifest names,
|
|
724
795
|
* so SwiftPM must never drift on its own — moving the version is the CLI's job, `lecodes app
|
|
725
|
-
* update` or app.json ios.sdk) + one vendored target per installed plugin
|
|
726
|
-
* (
|
|
796
|
+
* update` or app.json ios.sdk) + one vendored target per installed plugin, + a binary target per
|
|
797
|
+
* xcframework a plugin carries (its copy under Binaries/<id>/). The SDK's assets (the 3D variants'
|
|
798
|
+
* shader archives) ride inside the package. */
|
|
727
799
|
export const runtimePackageTemplate = (sdkVersion: string, variant: SdkVariant, plugins: RuntimePlugin[], sdkPath?: string): string => {
|
|
728
800
|
const sdkDep = (name: string) => `.product(name: "${name}", package: "lecodes-ios-sdk")`
|
|
729
801
|
// LECODES_IOS_SDK_PATH: the SDK-development loop — a package tree on disk (release-ios-sdk.sh
|
|
@@ -746,18 +818,31 @@ export const runtimePackageTemplate = (sdkVersion: string, variant: SdkVariant,
|
|
|
746
818
|
}
|
|
747
819
|
const packageDecls = [...packages.values()].map(pkg =>
|
|
748
820
|
`\n .package(url: "${pkg.url}", ${pkg.exact ? `exact: "${pkg.exact}"` : `from: "${pkg.from}"`}),`).join("")
|
|
821
|
+
// The plugins' prebuilt xcframeworks (manifest ios.binaries): a binary target each, by its copy's
|
|
822
|
+
// path. A name is one target of the package — two plugins carrying one alike cannot both link.
|
|
823
|
+
const targetNames = new Map<string, string>([["LeCodesRuntime", "the runtime's own target"], ...plugins.map(p => [p.module, `the module of "${p.id}"`] as [string, string])])
|
|
824
|
+
for (const p of plugins) {
|
|
825
|
+
for (const name of p.binaries ?? []) {
|
|
826
|
+
const prior = targetNames.get(name)
|
|
827
|
+
if (prior) throw new Error(`Plugin "${p.id}" carries the binary ${name} and ${name} is ${prior} — one name is one target of the shell's package.`)
|
|
828
|
+
targetNames.set(name, `a binary of "${p.id}"`)
|
|
829
|
+
}
|
|
830
|
+
}
|
|
749
831
|
const pluginTargets = plugins.map(p => {
|
|
750
832
|
const deps = [
|
|
751
833
|
sdkDep("LeCodes"),
|
|
752
834
|
...(p.packages ?? []).flatMap(pkg => pkg.products.map(name => `.product(name: "${name}", package: "${swiftPackageName(pkg.url)}")`)),
|
|
835
|
+
...(p.binaries ?? []).map(name => `"${name}"`),
|
|
753
836
|
]
|
|
837
|
+
const binaries = (p.binaries ?? []).map(name => `
|
|
838
|
+
.binaryTarget(name: "${name}", path: "${runtimeBinaryPath(p.id, name)}"),`).join("")
|
|
754
839
|
return `
|
|
755
840
|
// plugin: ${p.id} (vendored — see plugins.lock.json)
|
|
756
841
|
.target(
|
|
757
842
|
name: "${p.module}",
|
|
758
843
|
dependencies: [${deps.join(", ")}],
|
|
759
844
|
path: "Sources/${p.module}"
|
|
760
|
-
)
|
|
845
|
+
),${binaries}`
|
|
761
846
|
}).join("")
|
|
762
847
|
const runtimeDeps = [...sdkProducts(variant).map(sdkDep), ...plugins.map(p => `"${p.module}"`)]
|
|
763
848
|
return `// swift-tools-version:5.9
|
|
@@ -877,7 +962,7 @@ Yours — \`lecodes app sync\` never touches these:
|
|
|
877
962
|
≥1024px image and every sync renders AppIcon.png from it (flattened over \`"iconBackground"\`,
|
|
878
963
|
default white — App Store icons must be opaque). Or leave the key unset and drop a
|
|
879
964
|
1024×1024 PNG into AppIcon.appiconset/ yourself — sync never touches it then.
|
|
880
|
-
- \`App/Base.lproj/LaunchScreen.storyboard\`
|
|
965
|
+
- \`App/Base.lproj/LaunchScreen.storyboard\` — unless \`app.json\` sets \`"splash"\` (below)
|
|
881
966
|
|
|
882
967
|
CLI-owned — regenerated on every sync, hand edits are overwritten:
|
|
883
968
|
|
|
@@ -888,10 +973,18 @@ CLI-owned — regenerated on every sync, hand edits are overwritten:
|
|
|
888
973
|
- \`App/App.entitlements\` — the capabilities the installed plugins declare (e.g. push)
|
|
889
974
|
- \`LeCodesRuntime/\` — local Swift package: the lecodes-ios-sdk dependency (one tag per engine
|
|
890
975
|
variant), vendored plugin sources (Sources/<Module>/), and LeCodesAppViewController — the SDK's
|
|
891
|
-
shell controller with this project's plugins registered (App/ViewController.swift subclasses it)
|
|
976
|
+
shell controller with this project's plugins registered (App/ViewController.swift subclasses it).
|
|
977
|
+
A plugin's prebuilt xcframeworks (its manifest's \`ios.binaries\`) are copied to
|
|
978
|
+
Binaries/<plugin id>/: binary targets its vendored target links
|
|
892
979
|
- the version values inside \`App.xcodeproj/project.pbxproj\` (from app.json)
|
|
893
980
|
- this README
|
|
894
981
|
|
|
982
|
+
CLI-owned **only when** \`../app.json\` sets \`"splash"\` — regenerated on every sync then;
|
|
983
|
+
without the key sync never touches them (see "Splash screen"):
|
|
984
|
+
|
|
985
|
+
- \`App/Base.lproj/LaunchScreen.storyboard\`
|
|
986
|
+
- \`App/Assets.xcassets/LeCodesSplashBackground.colorset/\`, \`App/Assets.xcassets/LeCodesSplash.imageset/\`
|
|
987
|
+
|
|
895
988
|
## SDK variant
|
|
896
989
|
|
|
897
990
|
Sync pins the SDK tag of the smallest engine variant that covers what the compiled bundle actually
|
|
@@ -900,30 +993,48 @@ physics, ~10 MB heavier), or \`full\` (everything) — \`<version>-core\`, \`-2d
|
|
|
900
993
|
bare \`<version>\` for full. Adding your first 3D scene or 2D game to the project switches it
|
|
901
994
|
automatically on the next sync; set \`ios.variant\` in \`../app.json\` to pin one instead.
|
|
902
995
|
|
|
996
|
+
## Splash screen
|
|
997
|
+
|
|
998
|
+
\`"splash": { "background": "#RRGGBB", "image": "./splash.png", "size": 160 }\` in \`../app.json\`
|
|
999
|
+
(every key optional) makes every sync write the launch screen: a full-screen view of the colour
|
|
1000
|
+
(\`background\`, default \`"iconBackground"\`, default white) with the image fitted centred into a
|
|
1001
|
+
\`size\`×\`size\` pt square (160 by default) — never at its pixel size — from @1x/@2x/@3x renderings
|
|
1002
|
+
(size × 1/2/3 px: 160/320/480 at the default) of the one master. Use a square mark of at least
|
|
1003
|
+
3 × size px (a non-square one is fitted into the square, a smaller one is upscaled); the same key
|
|
1004
|
+
draws the Android splash, which crops the mark to a circle on Android 12+ — above a \`size\` of 192
|
|
1005
|
+
its corners are cut there (sync warns; 288 is the most it takes). No image = the colour alone. The app holds this screen until its first frame — through the
|
|
1006
|
+
first-launch wait of an OTA shell too — so nothing flashes between the two. iOS caches launch
|
|
1007
|
+
screens: after a change, delete the app from the device (or restart the device) to see the new one.
|
|
1008
|
+
|
|
903
1009
|
## Over-the-air updates
|
|
904
1010
|
|
|
905
1011
|
With \`"update": true\` in \`../app.json\` (server-backed projects; or
|
|
906
1012
|
\`"update": {"url": "https://…/app.js"}\` for any static hosting), the app checks that URL
|
|
907
1013
|
at launch — natively, before any JS runs, so a broken bundle can never block its own fix.
|
|
908
|
-
Unchanged code costs a ~0-byte 304
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1014
|
+
Unchanged code costs a ~0-byte 304. The FIRST launch of an installed build waits for the answer
|
|
1015
|
+
(5 s at most, the launch screen held on screen) and runs the newest bundle at once, so a fresh
|
|
1016
|
+
install from the App Store never starts on the older code it shipped with; every later launch
|
|
1017
|
+
runs the local bundle at once, new code downloads in the background and runs on the NEXT launch.
|
|
1018
|
+
Offline it's a silent no-op with no wait — the app always launches from a local bundle. An App
|
|
1019
|
+
Store update keeps a downloaded bundle only when it is newer code than the one the update ships
|
|
1020
|
+
(by compile date); otherwise the store build wins. JS only: adding/removing native plugins still
|
|
1021
|
+
needs a store release.
|
|
912
1022
|
|
|
913
1023
|
Shipping an update is just \`lecodes push --publish\` — no rebuild, no store review:
|
|
914
1024
|
running apps download the new code on their next launch and run it on the launch after.
|
|
915
1025
|
|
|
916
1026
|
Shells generated before OTA support have a \`ViewController.swift\` that reads app.js from
|
|
917
1027
|
\`Bundle.main\` directly, so downloaded updates would never run — and since \`App/*.swift\`
|
|
918
|
-
is yours, sync won't rewrite it (\`lecodes app doctor\` flags this).
|
|
919
|
-
\`
|
|
1028
|
+
is yours, sync won't rewrite it (\`lecodes app doctor\` flags this). Make it subclass
|
|
1029
|
+
\`LeCodesAppViewController\` (the boot above, the first-launch wait included), or replace the
|
|
1030
|
+
\`Bundle.main\` load with (no wait):
|
|
920
1031
|
|
|
921
1032
|
\`\`\`swift
|
|
922
|
-
LeCodesUpdater.checkForUpdate()
|
|
923
1033
|
guard let code = LeCodesUpdater.activeBundle() else {
|
|
924
1034
|
print("LeCodes: bundled app.js is missing — run \`lecodes app sync\`")
|
|
925
1035
|
return
|
|
926
1036
|
}
|
|
1037
|
+
LeCodesUpdater.checkForUpdate()
|
|
927
1038
|
engine.run(code)
|
|
928
1039
|
\`\`\`
|
|
929
1040
|
|
|
@@ -1,195 +1,195 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs"
|
|
2
|
-
import { basename, dirname, join, resolve } from "node:path"
|
|
3
|
-
import { LECODES_DIR } from "../../project/manifest"
|
|
4
|
-
import { materializeTypesLocal, writeTsconfig } from "../../project/types"
|
|
5
|
-
import { DEFAULT_IGNORE_FILE, ensureGitignore } from "../../project/ignore"
|
|
6
|
-
import { appClaudeMd, CLAUDE_SETTINGS, STARTER_FLOW } from "./templates"
|
|
7
|
-
import { bool, c, defineCommand, log, note, success, warnErr } from "../../cli"
|
|
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("Taps: 0").style({ color: "#9fb0d0", fontSize: 22, marginTop: 18 })
|
|
29
|
-
|
|
30
|
-
const button = UIButton(UIText("Tap me").style({ color: "#ffffff", fontSize: 22 }))
|
|
31
|
-
.style({
|
|
32
|
-
backgroundColor: "#4f7cff",
|
|
33
|
-
borderRadius: 14,
|
|
34
|
-
paddingVertical: 16,
|
|
35
|
-
paddingHorizontal: 40,
|
|
36
|
-
marginTop: 24,
|
|
37
|
-
alignItems: "center",
|
|
38
|
-
justifyContent: "center",
|
|
39
|
-
$hovered: { backgroundColor: "#6b91ff" }, // the mouse over it (desktop); $pressed is the press
|
|
40
|
-
})
|
|
41
|
-
.named("tap") // names make elements targetable in tests/*.flow.json (see CLAUDE.md)
|
|
42
|
-
.onClick(() => {
|
|
43
|
-
count++
|
|
44
|
-
counter.text = "Taps: " + count
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
const screen = UIScreen(
|
|
48
|
-
UIColumn(UIText("Hello, LeCodes").style({ color: "#ffffff", fontSize: 32 }), button, counter)
|
|
49
|
-
.style({ backgroundColor: "#2b3550", borderRadius: 24, padding: 40, alignItems: "center" }),
|
|
50
|
-
)
|
|
51
|
-
.style({ backgroundColor: "#1e2230", alignItems: "center", justifyContent: "center", padding: 40 })
|
|
52
|
-
.named("home") // the screen root's name is what tests' \`expect: { screen: … }\` reads
|
|
53
|
-
|
|
54
|
-
screen.open()
|
|
55
|
-
`
|
|
56
|
-
|
|
57
|
-
/** `--desktop`: declare the desktop target up front. Window sizes are logical units (the host
|
|
58
|
-
* scales for HiDPI); `dev --desktop` / `desktop run` / `render --desktop` all read this, and a
|
|
59
|
-
* desktop-only app.json makes headless render/test default to the window size too. */
|
|
60
|
-
const appJsonDesktop = (name: string) => JSON.stringify({
|
|
61
|
-
name,
|
|
62
|
-
desktop: {
|
|
63
|
-
window: { width: 1280, height: 800, minWidth: 640, minHeight: 480 },
|
|
64
|
-
},
|
|
65
|
-
}, null, 2) + "\n"
|
|
66
|
-
|
|
67
|
-
const readmeTemplate = (name: string) => `# ${name}
|
|
68
|
-
|
|
69
|
-
A LeCodes app. The whole SDK is available as globals (\`UIScreen\`, \`UIText\`, \`Scene2D\`, …) —
|
|
70
|
-
no imports needed. \`main.ts\` is the entry point.
|
|
71
|
-
|
|
72
|
-
## Run it
|
|
73
|
-
|
|
74
|
-
\`\`\`
|
|
75
|
-
lecodes dev # live on your phone: scan the QR with the LeCodes app, hot reload
|
|
76
|
-
lecodes compile -o app.js # bundle to a single self-contained JS file
|
|
77
|
-
\`\`\`
|
|
78
|
-
|
|
79
|
-
## Test it (no device needed)
|
|
80
|
-
|
|
81
|
-
\`\`\`
|
|
82
|
-
lecodes render --png shot.png # headless screenshot of the running app
|
|
83
|
-
lecodes test # run tests/*.flow.json — the starter ships with a passing one
|
|
84
|
-
\`\`\`
|
|
85
|
-
|
|
86
|
-
Scenario format + conventions: see [CLAUDE.md](./CLAUDE.md) (written for AI assistants, useful
|
|
87
|
-
for humans too).
|
|
88
|
-
|
|
89
|
-
## Put it on the platform
|
|
90
|
-
|
|
91
|
-
This project is local-only. To give it a home on le.codes (web editor, sharing, OTA updates):
|
|
92
|
-
|
|
93
|
-
\`\`\`
|
|
94
|
-
lecodes login # once per machine
|
|
95
|
-
lecodes link # create a project from this folder and connect it
|
|
96
|
-
lecodes push -m "initial" # upload it as the first checkpoint (--publish to go live)
|
|
97
|
-
\`\`\`
|
|
98
|
-
|
|
99
|
-
Already made the project in the web editor? \`lecodes link --project <uuid>\` adopts it — it
|
|
100
|
-
shows what your next push would change there before connecting anything.
|
|
101
|
-
|
|
102
|
-
## Go native
|
|
103
|
-
|
|
104
|
-
\`\`\`
|
|
105
|
-
lecodes app init ios # generate a native Xcode shell in ios/
|
|
106
|
-
lecodes app sync # embed the compiled bundle + plugins into it
|
|
107
|
-
lecodes app open # open it in Xcode, build & run
|
|
108
|
-
\`\`\`
|
|
109
|
-
|
|
110
|
-
## Layout
|
|
111
|
-
|
|
112
|
-
- \`main.ts\` — the app's entry point. Add more .ts files freely and import them from
|
|
113
|
-
\`main.ts\`; a file named \`main.ts\` is always the entry.
|
|
114
|
-
- \`tests/\` — headless flow tests (\`lecodes test\`); failing steps dump the frame to
|
|
115
|
-
\`tests/.artifacts/\`
|
|
116
|
-
- \`CLAUDE.md\` — guidance for AI assistants (the headless feedback loop + testability conventions)
|
|
117
|
-
- \`assets/\` — images/fonts the app references (created on demand)
|
|
118
|
-
- \`tsconfig.json\` + \`.lecodes/types/\` — generated IDE types (rewritten by the CLI; don't edit).
|
|
119
|
-
They come from the SDK bundled in the CLI; \`lecodes update\` refreshes them from the platform.
|
|
120
|
-
- \`.lecodes/\` is gitignored — per-checkout state (those types + \`manifest.json\`, this machine's
|
|
121
|
-
sync cursor once linked). After a fresh git clone: \`lecodes update\`, then
|
|
122
|
-
\`lecodes link --project <uuid>\` to re-attach.
|
|
123
|
-
- \`.lecodesignore\` — files a future \`lecodes push\` would skip
|
|
124
|
-
|
|
125
|
-
Full command list: \`lecodes --help\`.
|
|
126
|
-
`
|
|
127
|
-
|
|
128
|
-
export default defineCommand({
|
|
129
|
-
name: "init",
|
|
130
|
-
summary: "Scaffold a new LOCAL project (no login, no server)",
|
|
131
|
-
usage: "[dir]",
|
|
132
|
-
description: "Writes a starter main.ts, a CLAUDE.md + a passing flow test (the headless render / test loop), the IDE types + tsconfig, and a .lecodesignore. Run it with `lecodes dev` or `lecodes compile`. Idempotent: existing files are kept.",
|
|
133
|
-
flags: { desktop: bool("also write an app.json declaring the desktop target (window size; `dev --desktop`, `desktop run` and headless render / test pick it up)") },
|
|
134
|
-
examples: ["lecodes init myapp", "lecodes init --desktop"],
|
|
135
|
-
run: ({ args, flags }) => init(args[0], flags.desktop),
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
const init = (dir: string | undefined, forDesktop: boolean): void => {
|
|
139
|
-
const dirArg = (dir ?? ".").replace(/[/\\]+$/, "") || "."
|
|
140
|
-
const root = resolve(dirArg)
|
|
141
|
-
const name = basename(root)
|
|
142
|
-
|
|
143
|
-
mkdirSync(root, { recursive: true })
|
|
144
|
-
|
|
145
|
-
let created = 0
|
|
146
|
-
const write = (rel: string, content: string) => {
|
|
147
|
-
const abs = join(root, rel)
|
|
148
|
-
if (existsSync(abs)) { note(`kept ${rel}`); return }
|
|
149
|
-
mkdirSync(dirname(abs), { recursive: true })
|
|
150
|
-
writeFileSync(abs, content)
|
|
151
|
-
log(`${c.green("created")} ${rel}`)
|
|
152
|
-
created++
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Starter entry — a root file named main.ts is always the detected entrypoint.
|
|
156
|
-
write("main.ts", STARTER)
|
|
157
|
-
write("README.md", readmeTemplate(name))
|
|
158
|
-
// --desktop: an app.json declaring the desktop target (window config in logical units).
|
|
159
|
-
if (forDesktop) write("app.json", appJsonDesktop(name))
|
|
160
|
-
// AI-assistant guidance + a passing flow test: the headless loop is discoverable from day one.
|
|
161
|
-
write("CLAUDE.md", appClaudeMd(name))
|
|
162
|
-
write(".claude/settings.json", CLAUDE_SETTINGS)
|
|
163
|
-
write("tests/counter.flow.json", STARTER_FLOW)
|
|
164
|
-
// Keep local render/preview outputs (screenshot.png, shots/) out of any future push.
|
|
165
|
-
write(".lecodesignore", DEFAULT_IGNORE_FILE)
|
|
166
|
-
// .lecodes/ is per-checkout state (generated types + the push cursor) — keep it out of git.
|
|
167
|
-
const gi = ensureGitignore(root)
|
|
168
|
-
if (gi === "created") { log(`${c.green("created")} .gitignore`); created++ }
|
|
169
|
-
else if (gi === "updated") log(`${c.green("updated")} .gitignore ${c.dim("(+ .lecodes/)")}`)
|
|
170
|
-
else note("kept .gitignore")
|
|
171
|
-
|
|
172
|
-
// IDE types: the SDK's import-free global surface + a tsconfig, so VS Code resolves UIScreen,
|
|
173
|
-
// Scene2D, setLoop, … with no server. Regenerated every run to pick up CLI upgrades.
|
|
174
|
-
const wroteTypes = materializeTypesLocal(root)
|
|
175
|
-
writeTsconfig(root)
|
|
176
|
-
log(`${c.green("written")} tsconfig.json${wroteTypes ? ` + ${LECODES_DIR}/types/` : ""} (IDE types)`)
|
|
177
|
-
if (!wroteTypes) {
|
|
178
|
-
warnErr("Bundled SDK types not found — tsconfig written, but globals may show unresolved until the CLI is rebuilt with vendored types.")
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
log("")
|
|
182
|
-
success(created
|
|
183
|
-
? `Local project ${c.bold(name)} ready${dirArg === "." ? "" : ` in ${c.bold(dirArg)}/`}.`
|
|
184
|
-
: `Project ${c.bold(name)} already initialized.`)
|
|
185
|
-
const cd = dirArg === "." ? "" : `cd ${dirArg} && `
|
|
186
|
-
if (forDesktop) {
|
|
187
|
-
log(` ${c.bold(cd + "lecodes dev --desktop")} run it in a desktop window with hot reload`)
|
|
188
|
-
log(` ${c.bold(cd + "lecodes test")} run the flow tests headless (desktop-sized)`)
|
|
189
|
-
log(` ${c.bold(cd + "lecodes desktop run")} open the compiled app in the desktop host`)
|
|
190
|
-
} else {
|
|
191
|
-
log(` ${c.bold(cd + "lecodes dev")} run it on a device with hot reload`)
|
|
192
|
-
log(` ${c.bold(cd + "lecodes test")} run the flow tests headless (no device)`)
|
|
193
|
-
log(` ${c.bold(cd + "lecodes compile -o app.js")} bundle it for LeCodes Desktop`)
|
|
194
|
-
}
|
|
195
|
-
}
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path"
|
|
3
|
+
import { LECODES_DIR } from "../../project/manifest"
|
|
4
|
+
import { materializeTypesLocal, writeTsconfig } from "../../project/types"
|
|
5
|
+
import { DEFAULT_IGNORE_FILE, ensureGitignore } from "../../project/ignore"
|
|
6
|
+
import { appClaudeMd, CLAUDE_SETTINGS, STARTER_FLOW } from "./templates"
|
|
7
|
+
import { bool, c, defineCommand, log, note, success, warnErr } from "../../cli"
|
|
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("Taps: 0").style({ color: "#9fb0d0", fontSize: 22, marginTop: 18 })
|
|
29
|
+
|
|
30
|
+
const button = UIButton(UIText("Tap me").style({ color: "#ffffff", fontSize: 22 }))
|
|
31
|
+
.style({
|
|
32
|
+
backgroundColor: "#4f7cff",
|
|
33
|
+
borderRadius: 14,
|
|
34
|
+
paddingVertical: 16,
|
|
35
|
+
paddingHorizontal: 40,
|
|
36
|
+
marginTop: 24,
|
|
37
|
+
alignItems: "center",
|
|
38
|
+
justifyContent: "center",
|
|
39
|
+
$hovered: { backgroundColor: "#6b91ff" }, // the mouse over it (desktop); $pressed is the press
|
|
40
|
+
})
|
|
41
|
+
.named("tap") // names make elements targetable in tests/*.flow.json (see CLAUDE.md)
|
|
42
|
+
.onClick(() => {
|
|
43
|
+
count++
|
|
44
|
+
counter.text = "Taps: " + count
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const screen = UIScreen(
|
|
48
|
+
UIColumn(UIText("Hello, LeCodes").style({ color: "#ffffff", fontSize: 32 }), button, counter)
|
|
49
|
+
.style({ backgroundColor: "#2b3550", borderRadius: 24, padding: 40, alignItems: "center" }),
|
|
50
|
+
)
|
|
51
|
+
.style({ backgroundColor: "#1e2230", alignItems: "center", justifyContent: "center", padding: 40 })
|
|
52
|
+
.named("home") // the screen root's name is what tests' \`expect: { screen: … }\` reads
|
|
53
|
+
|
|
54
|
+
screen.open()
|
|
55
|
+
`
|
|
56
|
+
|
|
57
|
+
/** `--desktop`: declare the desktop target up front. Window sizes are logical units (the host
|
|
58
|
+
* scales for HiDPI); `dev --desktop` / `desktop run` / `render --desktop` all read this, and a
|
|
59
|
+
* desktop-only app.json makes headless render/test default to the window size too. */
|
|
60
|
+
const appJsonDesktop = (name: string) => JSON.stringify({
|
|
61
|
+
name,
|
|
62
|
+
desktop: {
|
|
63
|
+
window: { width: 1280, height: 800, minWidth: 640, minHeight: 480 },
|
|
64
|
+
},
|
|
65
|
+
}, null, 2) + "\n"
|
|
66
|
+
|
|
67
|
+
const readmeTemplate = (name: string) => `# ${name}
|
|
68
|
+
|
|
69
|
+
A LeCodes app. The whole SDK is available as globals (\`UIScreen\`, \`UIText\`, \`Scene2D\`, …) —
|
|
70
|
+
no imports needed. \`main.ts\` is the entry point.
|
|
71
|
+
|
|
72
|
+
## Run it
|
|
73
|
+
|
|
74
|
+
\`\`\`
|
|
75
|
+
lecodes dev # live on your phone: scan the QR with the LeCodes app, hot reload
|
|
76
|
+
lecodes compile -o app.js # bundle to a single self-contained JS file
|
|
77
|
+
\`\`\`
|
|
78
|
+
|
|
79
|
+
## Test it (no device needed)
|
|
80
|
+
|
|
81
|
+
\`\`\`
|
|
82
|
+
lecodes render --png shot.png # headless screenshot of the running app
|
|
83
|
+
lecodes test # run tests/*.flow.json — the starter ships with a passing one
|
|
84
|
+
\`\`\`
|
|
85
|
+
|
|
86
|
+
Scenario format + conventions: see [CLAUDE.md](./CLAUDE.md) (written for AI assistants, useful
|
|
87
|
+
for humans too).
|
|
88
|
+
|
|
89
|
+
## Put it on the platform
|
|
90
|
+
|
|
91
|
+
This project is local-only. To give it a home on le.codes (web editor, sharing, OTA updates):
|
|
92
|
+
|
|
93
|
+
\`\`\`
|
|
94
|
+
lecodes login # once per machine
|
|
95
|
+
lecodes link # create a project from this folder and connect it
|
|
96
|
+
lecodes push -m "initial" # upload it as the first checkpoint (--publish to go live)
|
|
97
|
+
\`\`\`
|
|
98
|
+
|
|
99
|
+
Already made the project in the web editor? \`lecodes link --project <uuid>\` adopts it — it
|
|
100
|
+
shows what your next push would change there before connecting anything.
|
|
101
|
+
|
|
102
|
+
## Go native
|
|
103
|
+
|
|
104
|
+
\`\`\`
|
|
105
|
+
lecodes app init ios # generate a native Xcode shell in ios/
|
|
106
|
+
lecodes app sync # embed the compiled bundle + plugins into it
|
|
107
|
+
lecodes app open # open it in Xcode, build & run
|
|
108
|
+
\`\`\`
|
|
109
|
+
|
|
110
|
+
## Layout
|
|
111
|
+
|
|
112
|
+
- \`main.ts\` — the app's entry point. Add more .ts files freely and import them from
|
|
113
|
+
\`main.ts\`; a file named \`main.ts\` is always the entry.
|
|
114
|
+
- \`tests/\` — headless flow tests (\`lecodes test\`); failing steps dump the frame to
|
|
115
|
+
\`tests/.artifacts/\`
|
|
116
|
+
- \`CLAUDE.md\` — guidance for AI assistants (the headless feedback loop + testability conventions)
|
|
117
|
+
- \`assets/\` — images/fonts the app references (created on demand)
|
|
118
|
+
- \`tsconfig.json\` + \`.lecodes/types/\` — generated IDE types (rewritten by the CLI; don't edit).
|
|
119
|
+
They come from the SDK bundled in the CLI; \`lecodes update\` refreshes them from the platform.
|
|
120
|
+
- \`.lecodes/\` is gitignored — per-checkout state (those types + \`manifest.json\`, this machine's
|
|
121
|
+
sync cursor once linked). After a fresh git clone: \`lecodes update\`, then
|
|
122
|
+
\`lecodes link --project <uuid>\` to re-attach.
|
|
123
|
+
- \`.lecodesignore\` — files a future \`lecodes push\` would skip
|
|
124
|
+
|
|
125
|
+
Full command list: \`lecodes --help\`.
|
|
126
|
+
`
|
|
127
|
+
|
|
128
|
+
export default defineCommand({
|
|
129
|
+
name: "init",
|
|
130
|
+
summary: "Scaffold a new LOCAL project (no login, no server)",
|
|
131
|
+
usage: "[dir]",
|
|
132
|
+
description: "Writes a starter main.ts, a CLAUDE.md + a passing flow test (the headless render / test loop), the IDE types + tsconfig, and a .lecodesignore. Run it with `lecodes dev` or `lecodes compile`. Idempotent: existing files are kept.",
|
|
133
|
+
flags: { desktop: bool("also write an app.json declaring the desktop target (window size; `dev --desktop`, `desktop run` and headless render / test pick it up)") },
|
|
134
|
+
examples: ["lecodes init myapp", "lecodes init --desktop"],
|
|
135
|
+
run: ({ args, flags }) => init(args[0], flags.desktop),
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
const init = (dir: string | undefined, forDesktop: boolean): void => {
|
|
139
|
+
const dirArg = (dir ?? ".").replace(/[/\\]+$/, "") || "."
|
|
140
|
+
const root = resolve(dirArg)
|
|
141
|
+
const name = basename(root)
|
|
142
|
+
|
|
143
|
+
mkdirSync(root, { recursive: true })
|
|
144
|
+
|
|
145
|
+
let created = 0
|
|
146
|
+
const write = (rel: string, content: string) => {
|
|
147
|
+
const abs = join(root, rel)
|
|
148
|
+
if (existsSync(abs)) { note(`kept ${rel}`); return }
|
|
149
|
+
mkdirSync(dirname(abs), { recursive: true })
|
|
150
|
+
writeFileSync(abs, content)
|
|
151
|
+
log(`${c.green("created")} ${rel}`)
|
|
152
|
+
created++
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Starter entry — a root file named main.ts is always the detected entrypoint.
|
|
156
|
+
write("main.ts", STARTER)
|
|
157
|
+
write("README.md", readmeTemplate(name))
|
|
158
|
+
// --desktop: an app.json declaring the desktop target (window config in logical units).
|
|
159
|
+
if (forDesktop) write("app.json", appJsonDesktop(name))
|
|
160
|
+
// AI-assistant guidance + a passing flow test: the headless loop is discoverable from day one.
|
|
161
|
+
write("CLAUDE.md", appClaudeMd(name))
|
|
162
|
+
write(".claude/settings.json", CLAUDE_SETTINGS)
|
|
163
|
+
write("tests/counter.flow.json", STARTER_FLOW)
|
|
164
|
+
// Keep local render/preview outputs (screenshot.png, shots/) out of any future push.
|
|
165
|
+
write(".lecodesignore", DEFAULT_IGNORE_FILE)
|
|
166
|
+
// .lecodes/ is per-checkout state (generated types + the push cursor) — keep it out of git.
|
|
167
|
+
const gi = ensureGitignore(root)
|
|
168
|
+
if (gi === "created") { log(`${c.green("created")} .gitignore`); created++ }
|
|
169
|
+
else if (gi === "updated") log(`${c.green("updated")} .gitignore ${c.dim("(+ .lecodes/)")}`)
|
|
170
|
+
else note("kept .gitignore")
|
|
171
|
+
|
|
172
|
+
// IDE types: the SDK's import-free global surface + a tsconfig, so VS Code resolves UIScreen,
|
|
173
|
+
// Scene2D, setLoop, … with no server. Regenerated every run to pick up CLI upgrades.
|
|
174
|
+
const wroteTypes = materializeTypesLocal(root)
|
|
175
|
+
writeTsconfig(root)
|
|
176
|
+
log(`${c.green("written")} tsconfig.json${wroteTypes ? ` + ${LECODES_DIR}/types/` : ""} (IDE types)`)
|
|
177
|
+
if (!wroteTypes) {
|
|
178
|
+
warnErr("Bundled SDK types not found — tsconfig written, but globals may show unresolved until the CLI is rebuilt with vendored types.")
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
log("")
|
|
182
|
+
success(created
|
|
183
|
+
? `Local project ${c.bold(name)} ready${dirArg === "." ? "" : ` in ${c.bold(dirArg)}/`}.`
|
|
184
|
+
: `Project ${c.bold(name)} already initialized.`)
|
|
185
|
+
const cd = dirArg === "." ? "" : `cd ${dirArg} && `
|
|
186
|
+
if (forDesktop) {
|
|
187
|
+
log(` ${c.bold(cd + "lecodes dev --desktop")} run it in a desktop window with hot reload`)
|
|
188
|
+
log(` ${c.bold(cd + "lecodes test")} run the flow tests headless (desktop-sized)`)
|
|
189
|
+
log(` ${c.bold(cd + "lecodes desktop run")} open the compiled app in the desktop host`)
|
|
190
|
+
} else {
|
|
191
|
+
log(` ${c.bold(cd + "lecodes dev")} run it on a device with hot reload`)
|
|
192
|
+
log(` ${c.bold(cd + "lecodes test")} run the flow tests headless (no device)`)
|
|
193
|
+
log(` ${c.bold(cd + "lecodes compile -o app.js")} bundle it for LeCodes Desktop`)
|
|
194
|
+
}
|
|
195
|
+
}
|