lecodes-cli 0.20.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -57
- package/dist/index.js +11052 -7592
- package/package.json +14 -8
- package/runtime/materials/decal-relief.mat +12 -1
- package/runtime/materials/decal.mat +17 -3
- package/runtime/materials/lightmap.mat +117 -138
- package/runtime/materials/particles-quad.mat +6 -2
- package/runtime/materials/terrain-lightmap.mat +123 -0
- package/runtime/materials/terrain.mat +36 -55
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/VERSION.json +5 -0
- package/runtime/web/assets/camera-5XOM8eHl.js +1 -0
- package/runtime/web/assets/creator-2d-D9WKYM6h.js +1 -0
- package/runtime/web/assets/creator-2d-ukvEmNEp.wasm +0 -0
- package/runtime/web/assets/creator-full-BBXzQlhq.js +1 -0
- package/runtime/web/assets/creator-full-D2dSDdDj.wasm +0 -0
- package/runtime/web/assets/creator-full.js-BYswGsqR.symbols +8124 -0
- package/runtime/web/assets/creator-ui-DIc92ion.wasm +0 -0
- package/runtime/web/assets/creator-ui-_1Z_AX2M.js +1 -0
- package/runtime/web/assets/geolocation-BjTKN9_P.js +1 -0
- package/runtime/web/assets/inspect-C7x78PNs.js +2 -0
- package/runtime/web/assets/map-CyBxOSV0.js +1 -0
- package/runtime/web/assets/map-XzoOFt0E.js +2 -0
- package/runtime/web/assets/mapImpl-bxvQ2h1Z.js +823 -0
- package/runtime/web/assets/maplibre-gl-worker-B7LXXUkR.js +8 -0
- package/runtime/web/assets/materials-uri4FvPE.bin +0 -0
- package/runtime/web/assets/materials-world-CQWt8TAm.bin +0 -0
- package/runtime/web/assets/qr-scanner-CK_cYoJR.js +1 -0
- package/runtime/web/assets/uberarchive-DLH-kBhM.bin +0 -0
- package/runtime/web/index.html +36 -0
- package/runtime/web/player.js +21 -0
- package/runtime/web-page/index.html +41 -0
- package/runtime/web-page/page.css +1 -0
- package/runtime/web-page/page.js +31 -0
- package/src/api.ts +1 -302
- package/src/cli/args.ts +37 -0
- package/src/cli/command.ts +113 -0
- package/src/cli/errors.ts +13 -0
- package/src/cli/help.ts +158 -0
- package/src/cli/index.ts +7 -0
- package/src/cli/output.ts +102 -0
- package/src/cli/run.ts +106 -0
- package/src/commands/{appAndroid.ts → app/android.ts} +454 -43
- package/src/commands/{appDesktop.ts → app/desktop.ts} +64 -33
- package/src/commands/{appDesktopMac.ts → app/desktopMac.ts} +34 -27
- package/src/commands/{appIcon.ts → app/icon.ts} +11 -8
- package/src/commands/{app.ts → app/index.ts} +400 -290
- package/src/commands/{appShared.ts → app/shared.ts} +43 -44
- package/src/commands/{appTemplatesAndroid.ts → app/templates/android.ts} +72 -39
- package/src/commands/{appTemplatesGradlew.ts → app/templates/gradlew.ts} +1 -1
- package/src/commands/{appTemplates.ts → app/templates/ios.ts} +2 -2
- package/src/commands/assets.ts +23 -21
- package/src/commands/clone.ts +48 -51
- package/src/commands/compile.ts +76 -89
- package/src/commands/create.ts +46 -47
- package/src/commands/design/comments.ts +361 -0
- package/src/commands/design/context.ts +101 -0
- package/src/commands/design/index.ts +391 -0
- package/src/commands/design/snapshot.ts +134 -0
- package/src/commands/{designTemplates.ts → design/templates.ts} +4 -3
- package/src/commands/desktop.ts +94 -82
- package/src/commands/dev.ts +260 -182
- package/src/commands/diff.ts +39 -51
- package/src/commands/index.ts +64 -0
- package/src/commands/{init.ts → init/index.ts} +195 -191
- package/src/commands/{projectTemplates.ts → init/templates.ts} +64 -21
- package/src/commands/install.ts +80 -67
- package/src/commands/lightmap.ts +543 -252
- package/src/commands/link.ts +87 -94
- package/src/commands/login.ts +25 -23
- package/src/commands/navmesh.ts +169 -139
- package/src/commands/plugin.ts +46 -0
- package/src/commands/pn.ts +201 -244
- package/src/commands/pull.ts +49 -56
- package/src/commands/push.ts +48 -54
- package/src/commands/render.ts +176 -111
- package/src/commands/scene.ts +63 -55
- package/src/commands/serve.ts +58 -0
- package/src/commands/{shaders.ts → shaders/index.ts} +61 -42
- package/src/commands/{shadersNew.ts → shaders/new.ts} +3 -3
- package/src/commands/shared.ts +79 -0
- package/src/commands/status.ts +22 -26
- package/src/commands/test.ts +94 -63
- package/src/commands/thumbs.ts +185 -175
- package/src/commands/update/index.ts +226 -0
- package/src/commands/{types.ts → update/types.ts} +31 -22
- package/src/compile/collect.ts +5 -5
- package/src/compile/collectLocal.ts +3 -3
- package/src/compile/designCompile.ts +4 -4
- package/src/compile/headlessBundle.ts +16 -12
- package/src/compile/nativeStack.ts +1 -1
- package/src/compile/projectCompile.ts +12 -4
- package/src/compile/sceneCompile.ts +14 -26
- package/src/compile/screenEntry.ts +123 -127
- package/src/compile/shaders.ts +3 -3
- package/src/{lecodes-3d-editor.d.ts → declarations/lecodes-3d-editor.d.ts} +4 -0
- package/src/{lecodes-assets.d.ts → declarations/lecodes-assets.d.ts} +1 -1
- package/src/{lecodes-renderer.d.ts → declarations/lecodes-headless.d.ts} +3 -3
- package/src/dev/androidDev.ts +1 -1
- package/src/dev/clientTemplates.ts +18 -14
- package/src/dev/devServer.ts +78 -41
- package/src/dev/iosDev.ts +180 -0
- package/src/dev/webRunner.ts +83 -15
- package/src/{cmgenTool.ts → hosts/cmgenTool.ts} +2 -2
- package/src/{desktopRenderer.ts → hosts/desktopRenderer.ts} +104 -32
- package/src/{desktopScript.ts → hosts/desktopScript.ts} +151 -36
- package/src/{distRoot.ts → hosts/distRoot.ts} +9 -5
- package/src/{matcTool.ts → hosts/matcTool.ts} +2 -2
- package/src/{peerInstall.ts → hosts/peerInstall.ts} +7 -7
- package/src/{peers.ts → hosts/peers.ts} +2 -2
- package/src/{releases.ts → hosts/releases.ts} +1 -1
- package/src/index.ts +35 -480
- package/src/platform/api.ts +302 -0
- package/src/{browserAuth.ts → platform/browserAuth.ts} +1 -1
- package/src/{config.ts → platform/config.ts} +1 -1
- package/src/{serverDiff.ts → platform/serverDiff.ts} +3 -3
- package/src/plugin/contract.ts +438 -0
- package/src/plugin/emitKotlin.ts +357 -0
- package/src/plugin/emitSdk.ts +259 -0
- package/src/plugin/emitSwift.ts +336 -0
- package/src/plugin/emitWeb.ts +383 -0
- package/src/plugin/generate.ts +157 -0
- package/src/plugin/lines.ts +28 -0
- package/src/plugin/lower.ts +231 -0
- package/src/plugin/manifest.ts +192 -0
- package/src/{projectEnv.ts → project/env.ts} +33 -2
- package/src/{ignore.ts → project/ignore.ts} +164 -163
- package/src/{localFiles.ts → project/localFiles.ts} +1 -1
- package/src/{manifest.ts → project/manifest.ts} +1 -1
- package/src/{project.ts → project/materialize.ts} +2 -2
- package/src/project/paths.ts +20 -0
- package/src/{textDiff.ts → project/textDiff.ts} +1 -1
- package/src/{types.ts → project/types.ts} +0 -0
- package/runtime/materials/lightmap-baked-lite.mat +0 -175
- package/runtime/materials/lightmap-baked.mat +0 -176
- package/runtime/scene-harness.json +0 -1
- package/runtime/web/assets/__vite-browser-external-BIHI7g3E.js +0 -1
- package/runtime/web/assets/basis-C64VHDVD.js +0 -1
- package/runtime/web/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
- package/runtime/web/assets/createViewerLite-Ct_PZdof.js +0 -852
- package/runtime/web/assets/draco-BiISTFcR.js +0 -118
- package/runtime/web/assets/draco_decoder-DsQ12WqX.wasm +0 -0
- package/runtime/web/assets/index-BMt7AnC5.js +0 -2
- package/runtime/web/assets/mapViewImpl-B2JcES8l.js +0 -810
- package/runtime/web/assets/maplibre-gl-worker-CJfwIrte.js +0 -8
- package/runtime/web/assets/worker-Caf-yYEI.js +0 -2
- package/runtime/web/embed.html +0 -32
- package/runtime/web/embed.js +0 -171
- package/src/commands/design.ts +0 -846
- package/src/commands/update.ts +0 -211
- package/src/util.ts +0 -146
- /package/src/{lecodes-design.d.ts → declarations/lecodes-design.d.ts} +0 -0
- /package/src/{qrcode-terminal.d.ts → declarations/qrcode-terminal.d.ts} +0 -0
- /package/src/{designMeta.ts → project/designMeta.ts} +0 -0
|
@@ -2,14 +2,14 @@ import { execFileSync } from "node:child_process"
|
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
|
|
3
3
|
import { homedir } from "node:os"
|
|
4
4
|
import { dirname, join, resolve } from "node:path"
|
|
5
|
-
import { findProjectRoot } from "
|
|
6
|
-
import { DEFAULT_IGNORE_FILE } from "
|
|
7
|
-
import { CliError, c,
|
|
5
|
+
import { findProjectRoot } from "../../project/manifest"
|
|
6
|
+
import { DEFAULT_IGNORE_FILE } from "../../project/ignore"
|
|
7
|
+
import { CliError, c, log, note, warn } from "../../cli"
|
|
8
8
|
|
|
9
|
-
/* Shared plumbing for the `lecodes app` platform arms (
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* (lecodes-ios-sdk vs lecodes-android-sdk) and resolvePlugin a platform one. */
|
|
9
|
+
/* Shared plumbing for the `lecodes app` platform arms (index.ts = iOS + the command tree,
|
|
10
|
+
* android.ts = Android, desktop.ts = the portable desktop build) and for every other command that
|
|
11
|
+
* reads app.json (the desktop window, the renderer, native plugin libraries). The SDK helpers take
|
|
12
|
+
* a repo parameter (lecodes-ios-sdk vs lecodes-android-sdk) and resolvePlugin a platform one. */
|
|
13
13
|
|
|
14
14
|
/** First-party plugin registry: id → GitHub repo holding plugins/<id>/. Third-party plugins
|
|
15
15
|
* use a `github:owner/repo[@tag]` spec in app.json instead (manifest at the repo root). */
|
|
@@ -53,13 +53,16 @@ export interface AppConfig {
|
|
|
53
53
|
* `variant`: SDK artifact — "auto" (default: sync picks the smallest one covering the engines
|
|
54
54
|
* the compiled bundle actually uses) or core|2d|3d|full to force one, like ios.variant
|
|
55
55
|
* (needs lecodes-android-sdk >= 1.3.0 — older releases ship only the full AAR).
|
|
56
|
-
* `signing`: keystore METADATA only
|
|
57
|
-
*
|
|
56
|
+
* `signing`: keystore METADATA only (written by `lecodes app keystore create|use`) — the
|
|
57
|
+
* path + passwords live in the project .env as LECODES_ANDROID_* (or android/
|
|
58
|
+
* keystore.properties for Studio-only builds), never in app.json.
|
|
59
|
+
* `minify`: R8 on release builds (default true; sync restamps the values in app/build.gradle.kts). */
|
|
58
60
|
android?: {
|
|
59
61
|
applicationId?: string, versionCode?: number, sdk?: string, variant?: string,
|
|
60
62
|
signing?: { storeFile?: string, keyAlias?: string },
|
|
63
|
+
minify?: boolean,
|
|
61
64
|
}
|
|
62
|
-
/** Desktop host (
|
|
65
|
+
/** Desktop host (hosts/desktop). `window` sizes are LOGICAL units — the host multiplies by
|
|
63
66
|
* the display scale. `entry` picks a per-target entry file (default: the detected project
|
|
64
67
|
* entry) — see docs/desktop-target-plan.md. */
|
|
65
68
|
desktop?: {
|
|
@@ -67,8 +70,8 @@ export interface AppConfig {
|
|
|
67
70
|
window?: DesktopWindowConfig,
|
|
68
71
|
/** Which desktop host binary to run: "gl" (default) or "vulkan" (Windows only, host >= 1.4.0).
|
|
69
72
|
* A BUILD/DEV-TIME host selection, not a runtime switch — the two are separate binaries with
|
|
70
|
-
* their own shaders and different features (the Vulkan one has no
|
|
71
|
-
*
|
|
73
|
+
* their own shaders and different features (the Vulkan one has no native-view plugins).
|
|
74
|
+
* Overridable per run with `--renderer` / LECODES_DESKTOP_RENDERER. */
|
|
72
75
|
renderer?: "gl" | "vulkan",
|
|
73
76
|
/** 3D buffer scale vs the window (0.25–1, host ≥1.4.0). The UI always renders at native
|
|
74
77
|
* pixels; only the 3D scene is rendered smaller and upscaled by the compositor. Multiplies
|
|
@@ -77,6 +80,14 @@ export interface AppConfig {
|
|
|
77
80
|
/** 3D buffer cap in px, e.g. [1920, 1080]: on a bigger window/monitor the 3D renders at most
|
|
78
81
|
* this size (aspect kept) — the per-machine answer to "fullscreen on a 4K display is slow". */
|
|
79
82
|
maxRenderSize?: [number, number],
|
|
83
|
+
/** Frame-rate CAP for the host's render loop: `"display"` (default — follow the screen's refresh
|
|
84
|
+
* rate, 120 on a ProMotion Mac) or a number, meaning "at most this many fps" (the screen's
|
|
85
|
+
* maximum still wins when it is lower — vsync is the real ceiling, so this can never be a
|
|
86
|
+
* target above it). For an app that wants to spend less: a UI-heavy app on a laptop, a game
|
|
87
|
+
* whose feel is tuned for 60. `desktop run` forwards it as CREATOR_FRAME_RATE (a set env wins);
|
|
88
|
+
* a built .app's host reads it from the staged app.json. Honoured by the macOS host
|
|
89
|
+
* (2026-09-09); the Windows/Linux host is vsync-locked and ignores it for now. */
|
|
90
|
+
frameRate?: number | "display",
|
|
80
91
|
/** The mouse button acts as a finger: a left click-drag scrolls + flings like touch (the wheel
|
|
81
92
|
* always scrolls, clicks are unaffected; a button inside a scrollable then gets the touch-style
|
|
82
93
|
* press feedback delay). For testing the touch experience without a touchscreen, or an app
|
|
@@ -88,13 +99,13 @@ export interface AppConfig {
|
|
|
88
99
|
* a built folder's host reads it from the staged app.json. Headless render/test ignore it. */
|
|
89
100
|
virtualKeyboard?: boolean | DesktopVirtualKeyboard,
|
|
90
101
|
/** The macOS .app build (`lecodes app build desktop` on a Mac) — identity + signing +
|
|
91
|
-
* notarization. See
|
|
102
|
+
* notarization. See desktopMac.ts for the whole story. */
|
|
92
103
|
macos?: MacDesktopConfig,
|
|
93
104
|
}
|
|
94
105
|
}
|
|
95
106
|
|
|
96
107
|
/** One style slot of the desktop on-screen keyboard — the SDK's style words, the subset the drawn
|
|
97
|
-
* keyboard reads. Unset fields keep the theme preset. */
|
|
108
|
+
* keyboard reads (colors: any CSS color string). Unset fields keep the theme preset. */
|
|
98
109
|
export interface DesktopKeyboardSlot {
|
|
99
110
|
backgroundColor?: string
|
|
100
111
|
color?: string
|
|
@@ -181,13 +192,14 @@ export const readAppConfigAt = (root: string): AppConfig | null =>
|
|
|
181
192
|
export const DESKTOP_DEFAULT_WINDOW = { width: 1280, height: 720 }
|
|
182
193
|
|
|
183
194
|
/**
|
|
184
|
-
* The renderer for this run: `--renderer` > LECODES_DESKTOP_RENDERER > app.json
|
|
185
|
-
* > "gl". Resolve it ONCE per command and pass the result to both
|
|
186
|
-
* shader staging — two independent reads of the config is how a build
|
|
187
|
-
* next to an OpenGL .filamat, which fails at the first material with a
|
|
195
|
+
* The renderer for this run: the `--renderer` flag > LECODES_DESKTOP_RENDERER > app.json
|
|
196
|
+
* `desktop.renderer` > "gl". Resolve it ONCE per command and pass the result to both
|
|
197
|
+
* `resolveDesktopExe` and the shader staging — two independent reads of the config is how a build
|
|
198
|
+
* ends up with a Vulkan exe next to an OpenGL .filamat, which fails at the first material with a
|
|
199
|
+
* Filament abort.
|
|
188
200
|
*/
|
|
189
|
-
export const resolveDesktopRenderer = (root: string,
|
|
190
|
-
const raw =
|
|
201
|
+
export const resolveDesktopRenderer = (root: string, renderer?: string): "gl" | "vulkan" => {
|
|
202
|
+
const raw = renderer
|
|
191
203
|
?? process.env.LECODES_DESKTOP_RENDERER
|
|
192
204
|
?? readAppConfigAt(root)?.desktop?.renderer
|
|
193
205
|
if (raw === undefined) return "gl"
|
|
@@ -270,8 +282,8 @@ export interface PluginManifest {
|
|
|
270
282
|
* Bare ids resolve through the CLI's known-version table; "id:version" pins explicitly. */
|
|
271
283
|
gradlePlugins?: string[],
|
|
272
284
|
} | null
|
|
273
|
-
/** Desktop host (
|
|
274
|
-
* `
|
|
285
|
+
/** Desktop host (hosts/desktop): a PREBUILT shared library per OS/arch behind the C ABI in
|
|
286
|
+
* `hosts/desktop/plugin-sdk/lecodes_plugin.h` — no host rebuild, no source vendoring.
|
|
275
287
|
* `library` keys are `${process.platform}-${process.arch}` ("win32-x64", "linux-x64",
|
|
276
288
|
* "linux-arm64"), values plugin-dir-relative paths. `lecodes desktop run` / `render --desktop`
|
|
277
289
|
* forward the library's dir to the host (CREATOR_PLUGIN_DIRS); `app build desktop` stages the
|
|
@@ -303,10 +315,16 @@ export const git = (cwd: string, ...args: string[]): string =>
|
|
|
303
315
|
|
|
304
316
|
export const slugify = (s: string) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "app"
|
|
305
317
|
|
|
318
|
+
/** The environment for a child process. Spelled out on every spawn on purpose: Bun hands an
|
|
319
|
+
* inherited child the environment SNAPSHOT taken at startup, so anything the project .env
|
|
320
|
+
* contributed (projectEnv.ts — keystore passwords, CREATOR_* tracing) would never arrive. */
|
|
321
|
+
export const childEnv = (extra: Record<string, string> = {}): Record<string, string | undefined> =>
|
|
322
|
+
({ ...process.env, ...extra })
|
|
323
|
+
|
|
306
324
|
/** Run a tool, capturing output; on failure surface the tail (xcodebuild/gradle logs are huge). */
|
|
307
325
|
export const runTool = (cwd: string, cmd: string, args: string[]): string => {
|
|
308
326
|
try {
|
|
309
|
-
return execFileSync(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 128 * 1024 * 1024 })
|
|
327
|
+
return execFileSync(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 128 * 1024 * 1024, env: childEnv() })
|
|
310
328
|
} catch (e) {
|
|
311
329
|
const err = e as { stdout?: string, stderr?: string }
|
|
312
330
|
const tail = ((err.stdout ?? "") + "\n" + (err.stderr ?? "")).split("\n").filter(l => l.trim()).slice(-25).join("\n")
|
|
@@ -329,7 +347,7 @@ export const ensureShellIgnored = (root: string, dir: string) => {
|
|
|
329
347
|
/* ----------------------------------- SDK variants ----------------------------------- */
|
|
330
348
|
|
|
331
349
|
// Both native SDKs ship the same variant matrix (full / 3d / 2d / core), so the detection and
|
|
332
|
-
// resolution logic is shared between the iOS (
|
|
350
|
+
// resolution logic is shared between the iOS (index.ts) and Android (android.ts) syncs.
|
|
333
351
|
export const SDK_VARIANTS = ["core", "2d", "3d", "full"] as const
|
|
334
352
|
type SdkVariantShared = (typeof SDK_VARIANTS)[number]
|
|
335
353
|
|
|
@@ -346,25 +364,6 @@ export const detectEngines = (bundleJs: string): { gl: boolean, g2: boolean } =>
|
|
|
346
364
|
return { gl: !/^\/\/ gl: disable$/m.test(header), g2: /^\/\/ 2d: enable$/m.test(header) }
|
|
347
365
|
}
|
|
348
366
|
|
|
349
|
-
/**
|
|
350
|
-
* Refuse a Vulkan desktop build for a project that uses the 2D engine. The Vulkan host is built with
|
|
351
|
-
* CREATOR_PKG_2D=OFF (creator-2d is sokol-on-GL, with no Vulkan twin), so those scenes don't render
|
|
352
|
-
* — a blank window at runtime with nothing in the log to explain it. Fail at build time instead,
|
|
353
|
-
* where the fix ("use the default renderer") is one line away.
|
|
354
|
-
*
|
|
355
|
-
* Detection is the same `// 2d: enable` bundle header `lecodes app sync` picks its SDK variant from,
|
|
356
|
-
* so it can't drift from what the engine actually needs.
|
|
357
|
-
*/
|
|
358
|
-
export const assertRendererSupportsBundle = (renderer: "gl" | "vulkan", bundleJs: string): void => {
|
|
359
|
-
if (renderer !== "vulkan") return
|
|
360
|
-
if (!detectEngines(bundleJs).g2) return
|
|
361
|
-
throw new CliError(
|
|
362
|
-
"This project uses the 2D engine, which the Vulkan desktop host doesn't include " +
|
|
363
|
-
"(creator-2d renders through OpenGL and has no Vulkan path yet).\n" +
|
|
364
|
-
" Build with the default renderer: drop `desktop.renderer` from app.json, or pass `--renderer gl`.",
|
|
365
|
-
)
|
|
366
|
-
}
|
|
367
|
-
|
|
368
367
|
/** The SDK product for a shell: the smallest variant covering the engines the compiled bundle
|
|
369
368
|
* actually uses, unless <configKey> (ios.variant / android.variant) forces one — then warn when
|
|
370
369
|
* the forced product can't run the project (missing engine = those scenes silently no-op on
|
|
@@ -542,7 +541,7 @@ const isDesktopPluginLibrary = (name: string): boolean => {
|
|
|
542
541
|
return process.platform === "win32" ? n.endsWith(".dll") : n.endsWith(".so")
|
|
543
542
|
}
|
|
544
543
|
|
|
545
|
-
/** Native plugin libraries for THIS project on THIS machine (
|
|
544
|
+
/** Native plugin libraries for THIS project on THIS machine (hosts/desktop/plugin-sdk/
|
|
546
545
|
* lecodes_plugin.h — runtime-loaded .dll/.so, no host rebuild):
|
|
547
546
|
* - every library in the project's own `plugins/` dir — the zero-manifest dev loop (build a
|
|
548
547
|
* DLL, drop it there, `lecodes desktop run`);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* Templates for `lecodes app init android` / `lecodes app sync` — the Android mirror of
|
|
2
|
-
*
|
|
2
|
+
* ios.ts, distilled from the LetaryCreatorViewer host (lecodes-android :app). The
|
|
3
3
|
* same three ownership tiers: app/ is USER-owned after generation (init writes once, sync
|
|
4
4
|
* never touches it — except app/src/main/assets/, the Resources/ analog, and values-only
|
|
5
5
|
* versionName/versionCode restamps in app/build.gradle.kts); settings/root gradle files,
|
|
@@ -144,20 +144,18 @@ dependencies {
|
|
|
144
144
|
|
|
145
145
|
/** lecodes-runtime/src/main/kotlin/io/letary/lecodes/runtime/LeCodesRuntime.kt — the generated
|
|
146
146
|
* registration + host-hook surface. Plugins are called by FQN (no import collisions); the
|
|
147
|
-
* activity hooks
|
|
148
|
-
*
|
|
149
|
-
* intent extra, survives until the host forwards it, so no buffering is needed). */
|
|
147
|
+
* activity hooks forward to the SDK's LeCodesAppHooks, which NAMES NO PLUGIN — a plugin that
|
|
148
|
+
* consumes activity moments (push: a notification tap is an intent) attaches its handler there. */
|
|
150
149
|
export const runtimeKotlinTemplate = (plugins: AndroidRuntimePlugin[], projectUuid?: string): string => {
|
|
151
150
|
const identity = projectUuid ? ` engine.bootProjectUuid = "${projectUuid}"\n` : ""
|
|
152
151
|
const calls = plugins.map(p => ` ${p.register}.register(engine, context)\n`).join("")
|
|
153
152
|
const body = identity + calls
|
|
154
|
-
const push = plugins.find(p => p.id === "push")
|
|
155
|
-
const pushNs = push ? push.register.split(".").slice(0, -1).join(".") : null
|
|
156
153
|
return `// Generated by \`lecodes app sync\` — regenerated on every sync, do not edit.
|
|
157
154
|
package io.letary.lecodes.runtime
|
|
158
155
|
|
|
159
156
|
import android.content.Context
|
|
160
157
|
import android.content.Intent
|
|
158
|
+
import io.letary.lecodes.LeCodesAppHooks
|
|
161
159
|
import io.letary.lecodes.LecodesEngine
|
|
162
160
|
|
|
163
161
|
object LeCodesRuntime {
|
|
@@ -170,16 +168,17 @@ ${body === "" ? " // no plugins installed\n" : body} }
|
|
|
170
168
|
/** Cold create only (savedInstanceState == null): forward the launch intent so a plugin
|
|
171
169
|
* can park launch-time state (a push notification tap) before the engine boots. */
|
|
172
170
|
fun onActivityLaunch(intent: Intent) {
|
|
173
|
-
|
|
171
|
+
LeCodesAppHooks.launchIntent(intent)
|
|
172
|
+
}
|
|
174
173
|
|
|
175
174
|
/** Warm intent (Activity.onNewIntent). @return true when a plugin consumed it — the
|
|
176
175
|
* caller must then SKIP its generic deep-link handling. */
|
|
177
|
-
fun onNewIntent(intent: Intent): Boolean
|
|
178
|
-
${push ? ` return ${pushNs}.PushPlugin.handleTapIntent(intent)\n` : " return false\n"} }
|
|
176
|
+
fun onNewIntent(intent: Intent): Boolean = LeCodesAppHooks.newIntent(intent)
|
|
179
177
|
|
|
180
178
|
/** Activity.onResume(true) / onPause(false). */
|
|
181
179
|
fun setForeground(foreground: Boolean) {
|
|
182
|
-
|
|
180
|
+
LeCodesAppHooks.foreground(foreground)
|
|
181
|
+
}
|
|
183
182
|
}
|
|
184
183
|
`
|
|
185
184
|
}
|
|
@@ -234,16 +233,17 @@ export const ANDROID_README = `# Android shell
|
|
|
234
233
|
Generated by \`lecodes app init android\`. Daily workflow:
|
|
235
234
|
|
|
236
235
|
\`\`\`
|
|
237
|
-
lecodes app sync
|
|
238
|
-
lecodes app open android
|
|
239
|
-
lecodes app
|
|
236
|
+
lecodes app sync # embed the compiled bundle + plugins into this shell
|
|
237
|
+
lecodes app open android # open in Android Studio
|
|
238
|
+
lecodes app keystore create # once: the release signing key (or \`keystore use <file>\`)
|
|
239
|
+
lecodes app build android # signed release .apk + .aab → ../dist/
|
|
240
240
|
\`\`\`
|
|
241
241
|
|
|
242
242
|
## Ownership
|
|
243
243
|
|
|
244
244
|
| Path | Owner |
|
|
245
245
|
| --- | --- |
|
|
246
|
-
| \`app/\` (sources, manifest, res, build.gradle.kts) | **Yours** after init — sync never rewrites them (it only restamps \`versionName\`/\`versionCode\` values, manages \`// lecodes:plugin\`-tagged lines in the plugins block, and — when \`app.json\` sets \`"icon"\` — the icon files below) |
|
|
246
|
+
| \`app/\` (sources, manifest, res, build.gradle.kts, proguard-rules.pro) | **Yours** after init — sync never rewrites them (it only restamps the \`versionName\`/\`versionCode\` and \`isMinifyEnabled\`/\`isShrinkResources\` values, manages \`// lecodes:plugin\`-tagged lines in the plugins block, and — when \`app.json\` sets \`"icon"\` — the icon files below) |
|
|
247
247
|
| \`app/src/main/assets/\` | CLI-owned — wiped and regenerated by every sync (the compiled \`app.js\` + its \`// preload:\` assets) |
|
|
248
248
|
| \`res/mipmap-*/ic_launcher*\`, \`res/values/lecodes_icon.xml\` | CLI-owned **only when** \`app.json\` sets \`"icon"\` — regenerated by every sync (plus a value-only \`android:icon\` restamp in the manifest); without the key sync never touches icons |
|
|
249
249
|
| \`settings.gradle.kts\`, \`build.gradle.kts\` (root), \`gradle.properties\`, \`lecodes-runtime/\`, \`plugins/\` | CLI-owned — regenerated by every sync |
|
|
@@ -280,23 +280,48 @@ dependencies they need to \`app/build.gradle.kts\` — that file is yours.
|
|
|
280
280
|
|
|
281
281
|
## Release signing
|
|
282
282
|
|
|
283
|
-
|
|
283
|
+
The key lives in the project's \`.env\` (next to \`app.json\`; the CLI loads it for every command
|
|
284
|
+
and never pushes it):
|
|
284
285
|
|
|
285
286
|
\`\`\`
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
287
|
+
LECODES_ANDROID_KEYSTORE_FILE=./android/release.keystore # ./ = the project root; ~/ and absolute paths work too
|
|
288
|
+
LECODES_ANDROID_KEYSTORE_PASSWORD=…
|
|
289
|
+
LECODES_ANDROID_KEY_ALIAS=upload
|
|
290
|
+
LECODES_ANDROID_KEY_PASSWORD=…
|
|
290
291
|
\`\`\`
|
|
291
292
|
|
|
292
|
-
|
|
293
|
-
|
|
293
|
+
\`lecodes app keystore create\` makes a new key and writes those lines; \`lecodes app keystore use
|
|
294
|
+
<file>\` attaches an existing one (a keystore outside the project is just a path); \`lecodes app
|
|
295
|
+
keystore info\` prints the SHA-1 / SHA-256 fingerprints the Play Console, Firebase and Google
|
|
296
|
+
Sign-In ask for. The same four names work as real environment variables (CI). Building from
|
|
297
|
+
Android Studio without the CLI? \`android/keystore.properties\` (gitignored, \`storeFile\` /
|
|
298
|
+
\`storePassword\` / \`keyAlias\` / \`keyPassword\`) is still honoured and wins over the env.
|
|
299
|
+
Never put passwords in app.json — \`android.signing\` there is metadata only.
|
|
300
|
+
|
|
301
|
+
With Play App Signing (the default for new Play apps) this key is the UPLOAD key — Google holds
|
|
302
|
+
the app signing key and a lost upload key can be reset through Play support. Without it, losing
|
|
303
|
+
the keystore means the app can never be updated. Back it up either way.
|
|
304
|
+
|
|
305
|
+
## Building
|
|
306
|
+
|
|
307
|
+
\`lecodes app build android\` syncs, runs \`gradlew assembleRelease bundleRelease\` and copies the
|
|
308
|
+
results to \`../dist/<name>-<version>-<versionCode>.apk|.aab\` (\`-o <dir>\` elsewhere). A release
|
|
309
|
+
without a signing key is refused (\`--allow-unsigned\` for a throwaway build); \`--configuration
|
|
310
|
+
Debug\` signs with the debug keystore. \`--apk\` / \`--aab\` pick one artifact.
|
|
311
|
+
|
|
312
|
+
Release builds run R8 (\`isMinifyEnabled\` + \`isShrinkResources\`): the SDK AAR ships the keep
|
|
313
|
+
rules its JNI bindings need, plugins ship theirs, \`app/proguard-rules.pro\` is for your own
|
|
314
|
+
classes. \`app.json\` \`"android": { "minify": false }\` turns it off (sync restamps the values).
|
|
315
|
+
R8 only touches the dex — the engine's native libraries and \`app.js\` are not shrunk.
|
|
316
|
+
|
|
317
|
+
\`abiFilters\` in \`app/build.gradle.kts\` keeps the package to the two ABIs the SDK ships
|
|
318
|
+
(arm64-v8a, armeabi-v7a).
|
|
294
319
|
|
|
295
320
|
## Icons
|
|
296
321
|
|
|
297
322
|
The easy path: set \`app.json\` \`"icon"\` to a square ≥1024px PNG/JPEG in the project (e.g.
|
|
298
|
-
\`"icon": "icon.png"\`, optional \`"iconBackground": "#RRGGBB"\` — the adaptive-icon
|
|
299
|
-
default white). Every \`lecodes app sync\` then renders the full launcher set: legacy density
|
|
323
|
+
\`"icon": "icon.png"\`, optional \`"iconBackground": "#RRGGBB"\` (any opaque CSS color) — the adaptive-icon
|
|
324
|
+
backdrop, default white). Every \`lecodes app sync\` then renders the full launcher set: legacy density
|
|
300
325
|
PNGs, the API 26+ adaptive icon (your image at 72dp on the 108dp canvas), and an Android 13
|
|
301
326
|
themed-icon (monochrome) layer that reuses the foreground silhouette.
|
|
302
327
|
|
|
@@ -314,11 +339,11 @@ the init-time placeholder.
|
|
|
314
339
|
|
|
315
340
|
/* ---------------------------- USER-owned (written once) ---------------------------- */
|
|
316
341
|
|
|
317
|
-
export const appBuildGradleTemplate = ({ applicationId, version, versionCode }: AndroidShellIdentity): string => `import java.util.Properties
|
|
342
|
+
export const appBuildGradleTemplate = ({ applicationId, version, versionCode }: AndroidShellIdentity, minify = true): string => `import java.util.Properties
|
|
318
343
|
|
|
319
344
|
// Generated by \`lecodes app init\` — yours to edit. \`lecodes app sync\` only restamps the
|
|
320
|
-
// versionName/versionCode VALUES and manages
|
|
321
|
-
// block below; everything else is left alone.
|
|
345
|
+
// versionName/versionCode and isMinifyEnabled/isShrinkResources VALUES and manages
|
|
346
|
+
// \`// lecodes:plugin\`-tagged lines in the plugins block below; everything else is left alone.
|
|
322
347
|
plugins {
|
|
323
348
|
id("com.android.application")
|
|
324
349
|
id("org.jetbrains.kotlin.android")
|
|
@@ -342,6 +367,9 @@ android {
|
|
|
342
367
|
targetSdk = ${TARGET_SDK}
|
|
343
368
|
versionCode = ${versionCode}
|
|
344
369
|
versionName = "${version}"
|
|
370
|
+
// The LeCodes SDK ships arm64-v8a + armeabi-v7a natives. A transitive dependency adds a
|
|
371
|
+
// lone x86_64 library, which would make the package claim an ABI the engine can't load on.
|
|
372
|
+
ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a") }
|
|
345
373
|
}
|
|
346
374
|
|
|
347
375
|
val storeFilePath = signing("storeFile", "LECODES_ANDROID_KEYSTORE_FILE")
|
|
@@ -356,7 +384,12 @@ android {
|
|
|
356
384
|
|
|
357
385
|
buildTypes {
|
|
358
386
|
release {
|
|
359
|
-
|
|
387
|
+
// R8 (shrink + optimize + obfuscate). The SDK AAR carries the keep rules its JNI
|
|
388
|
+
// bindings need; app-local rules go in proguard-rules.pro. app.json
|
|
389
|
+
// android.minify: false turns it off (sync restamps these two values).
|
|
390
|
+
isMinifyEnabled = ${minify}
|
|
391
|
+
isShrinkResources = ${minify}
|
|
392
|
+
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
|
360
393
|
signingConfig = signingConfigs.findByName("release")
|
|
361
394
|
}
|
|
362
395
|
}
|
|
@@ -378,6 +411,18 @@ dependencies {
|
|
|
378
411
|
}
|
|
379
412
|
`
|
|
380
413
|
|
|
414
|
+
/** app/proguard-rules.pro — user-owned R8 rules for app-local code. The SDK's own keep rules
|
|
415
|
+
* (its JNI-bound classes) ride inside the AAR as consumer rules, and plugin modules ship theirs
|
|
416
|
+
* through consumerProguardFiles, so this file starts empty. */
|
|
417
|
+
export const APP_PROGUARD_RULES = `# R8 rules for app-local code (yours — \`lecodes app sync\` never touches this file).
|
|
418
|
+
#
|
|
419
|
+
# The LeCodes SDK keeps its own JNI-bound classes through the consumer rules inside the AAR, and
|
|
420
|
+
# plugin modules bring their own. Add rules here only for classes YOUR Kotlin registers with the
|
|
421
|
+
# engine by name (engine.registerCallback / registerService / registerView) or reaches through
|
|
422
|
+
# reflection, e.g.
|
|
423
|
+
# -keep class com.example.myapp.MyBridge { *; }
|
|
424
|
+
`
|
|
425
|
+
|
|
381
426
|
export const appManifestTemplate = (): string => `<?xml version="1.0" encoding="utf-8"?>
|
|
382
427
|
<!-- Generated by \`lecodes app init\` — yours to edit. Sync leaves it alone, except that with
|
|
383
428
|
app.json "icon" set it restamps the android:icon VALUE to the generated @mipmap set.
|
|
@@ -404,18 +449,6 @@ export const appManifestTemplate = (): string => `<?xml version="1.0" encoding="
|
|
|
404
449
|
<action android:name="android.intent.action.MAIN" />
|
|
405
450
|
<category android:name="android.intent.category.LAUNCHER" />
|
|
406
451
|
</intent-filter>
|
|
407
|
-
|
|
408
|
-
<!-- le.codes/qr deep links (QR scans, notification taps). Editable — remove if the
|
|
409
|
-
app should not claim them. -->
|
|
410
|
-
<intent-filter android:autoVerify="true">
|
|
411
|
-
<action android:name="android.intent.action.VIEW" />
|
|
412
|
-
<category android:name="android.intent.category.DEFAULT" />
|
|
413
|
-
<category android:name="android.intent.category.BROWSABLE" />
|
|
414
|
-
<data android:scheme="https" />
|
|
415
|
-
<data android:scheme="http" />
|
|
416
|
-
<data android:host="le.codes" />
|
|
417
|
-
<data android:pathPrefix="/qr" />
|
|
418
|
-
</intent-filter>
|
|
419
452
|
</activity>
|
|
420
453
|
</application>
|
|
421
454
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* Gradle wrapper launcher scripts, verbatim from the gradle/gradle v8.13.0 tag (the same
|
|
2
|
-
* pinned source as the wrapper jar download in
|
|
2
|
+
* pinned source as the wrapper jar download in ../android.ts). JSON-escaped mechanically —
|
|
3
3
|
* regenerate from the tag rather than editing. gradlew must be written with LF and
|
|
4
4
|
* gradlew.bat with CRLF (initAndroid handles the conversion); both are version-agnostic
|
|
5
5
|
* launchers that read gradle/wrapper/gradle-wrapper.properties for the real distribution. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { swiftPackageName, type IosPackage } from "
|
|
1
|
+
import { swiftPackageName, type IosPackage } from "../shared"
|
|
2
2
|
|
|
3
3
|
/* Templates for `lecodes app init ios` / `lecodes app sync` — distilled 1:1 from the
|
|
4
4
|
* hand-built, device-verified reference shell (docs/xcode-app-plan.md). Three ownership
|
|
@@ -485,7 +485,7 @@ export const VIEW_CONTROLLER = `//
|
|
|
485
485
|
// Generated by lecodes app init — yours to edit.
|
|
486
486
|
//
|
|
487
487
|
// Runs the bundled app.js (produced by \`lecodes app sync\`) offline: everything the
|
|
488
|
-
// script needs is provided by the SDK core (
|
|
488
|
+
// script needs is provided by the SDK core (_creatorTree / the host bridges / console)
|
|
489
489
|
// plus the plugins registered below via LeCodesRuntime.
|
|
490
490
|
//
|
|
491
491
|
|
package/src/commands/assets.ts
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolveCmgenExe } from "../cmgenTool"
|
|
3
|
-
import { loadPeer } from "../peerInstall"
|
|
1
|
+
import { defineCommand } from "../cli"
|
|
2
|
+
import { resolveCmgenExe } from "../hosts/cmgenTool"
|
|
3
|
+
import { loadPeer } from "../hosts/peerInstall"
|
|
4
4
|
|
|
5
5
|
/*
|
|
6
|
-
* `lecodes assets
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* board and the renderer); its own argv parser handles the sub-command flags, so the raw argv is
|
|
10
|
-
* forwarded untouched (`assets` gets `_raw` from main). See docs/animation-plan.md §2.9.
|
|
6
|
+
* `lecodes assets …` — the asset pipeline (tools/assets): FBX → GLB, the GLB doctor,
|
|
7
|
+
* retargeting, Unity packs, skies. The package is an optional peer with its OWN argv parser and
|
|
8
|
+
* help, so the argv is forwarded untouched (`passthrough`) — `lecodes assets --help` is theirs.
|
|
11
9
|
*/
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
10
|
+
export default defineCommand({
|
|
11
|
+
name: "assets",
|
|
12
|
+
summary: "The asset pipeline: convert / probe / doctor / retarget / unpack / scene / sky / terrain-pack …",
|
|
13
|
+
usage: "<command> [args…]",
|
|
14
|
+
passthrough: true,
|
|
15
|
+
examples: ["lecodes assets convert hero.fbx --clips anims/*.fbx -o hero.glb", "lecodes assets doctor hero.glb --fix --ktx2", "lecodes assets --help"],
|
|
16
|
+
run: async ({ argv }) => {
|
|
17
|
+
const mod = await loadPeer<typeof import("lecodes-assets/cli")>("lecodes-assets", "cli", { for: "lecodes assets" })
|
|
18
|
+
// `sky` shells out to filament's cmgen, which the asset package deliberately never downloads
|
|
19
|
+
// itself (it stays network-free and usable standalone). The CLI owns release artifacts, so it
|
|
20
|
+
// resolves the binary — cache or first-use download — and hands it over through the env var the
|
|
21
|
+
// package already honours. An explicit LECODES_CMGEN wins, so a local filament build still rules.
|
|
22
|
+
if (argv[0] === "sky" && !process.env.LECODES_CMGEN) process.env.LECODES_CMGEN = await resolveCmgenExe()
|
|
23
|
+
const code = await mod.runAssetsCli(argv)
|
|
24
|
+
if (code !== 0) process.exitCode = code
|
|
25
|
+
},
|
|
26
|
+
})
|
package/src/commands/clone.ts
CHANGED
|
@@ -1,60 +1,57 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync } from "node:fs"
|
|
2
2
|
import { resolve } from "node:path"
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
/* `lecodes clone <uuid|url> [dir]` —
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
*
|
|
15
|
-
* (https://le.codes/app/projects/<uuid>) and viewer links (.../view/<uuid>), with any
|
|
16
|
-
* query/hash or trailing slash; falls back to the last path segment.
|
|
17
|
-
*/
|
|
3
|
+
import { CliError, UsageError, bool, c, defineCommand, endProgress, info, progress, success } from "../cli"
|
|
4
|
+
import { getCommits, getProject } from "../platform/api"
|
|
5
|
+
import { ensureGitignore, writeDefaultIgnore } from "../project/ignore"
|
|
6
|
+
import { writeManifest } from "../project/manifest"
|
|
7
|
+
import { materializeAssets } from "../project/materialize"
|
|
8
|
+
import { materializeTypes, writeTsconfig } from "../project/types"
|
|
9
|
+
import { session, slugOf } from "./shared"
|
|
10
|
+
|
|
11
|
+
/* `lecodes clone <uuid|url> [dir]` — a project's files + types into a new folder. */
|
|
12
|
+
|
|
13
|
+
/** A bare uuid or a project link (editor `/app/projects/<uuid>`, viewer `/view/<uuid>`, any query /
|
|
14
|
+
* hash / trailing slash) → the uuid; falls back to the last path segment. */
|
|
18
15
|
export const parseUuid = (input: string): string => {
|
|
19
|
-
if (!input) throw new
|
|
16
|
+
if (!input) throw new UsageError("a project uuid or url is required")
|
|
20
17
|
if (!input.includes("/")) return input
|
|
21
18
|
const clean = input.split(/[?#]/)[0].replace(/\/+$/, "")
|
|
22
19
|
const match = clean.match(/\/(?:projects|view)\/([^/]+)/)
|
|
23
20
|
return match ? match[1] : clean.slice(clean.lastIndexOf("/") + 1)
|
|
24
21
|
}
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
throw new CliError(`Directory "${dir}" already exists and is not empty.`)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
23
|
+
export default defineCommand({
|
|
24
|
+
name: "clone",
|
|
25
|
+
summary: "Download a project's files + types into a new folder",
|
|
26
|
+
usage: "<uuid|url> [dir]",
|
|
27
|
+
flags: { "no-types": bool("skip the .d.ts / tsconfig generation") },
|
|
28
|
+
examples: ["lecodes clone https://le.codes/app/projects/<uuid> myapp"],
|
|
29
|
+
run: async ({ args, flags }) => {
|
|
30
|
+
const { apiUrl, token } = session()
|
|
31
|
+
const uuid = parseUuid(args[0])
|
|
32
|
+
const project = await getProject(apiUrl, token, uuid)
|
|
33
|
+
|
|
34
|
+
const dir = args[1] ?? slugOf(project.name)
|
|
35
|
+
const root = resolve(dir)
|
|
36
|
+
if (existsSync(root) && readdirSync(root).length > 0) throw new CliError(`Directory "${dir}" already exists and is not empty.`)
|
|
37
|
+
mkdirSync(root, { recursive: true })
|
|
38
|
+
|
|
39
|
+
info(c.dim(`Fetching project ${c.bold(project.name)}…`))
|
|
40
|
+
const files = await materializeAssets(root, project, apiUrl, token, progress)
|
|
41
|
+
endProgress()
|
|
42
|
+
|
|
43
|
+
if (!flags["no-types"]) {
|
|
44
|
+
info(c.dim("Fetching type definitions…"))
|
|
45
|
+
await materializeTypes(root, apiUrl)
|
|
46
|
+
writeTsconfig(root)
|
|
47
|
+
}
|
|
48
|
+
writeDefaultIgnore(root)
|
|
49
|
+
ensureGitignore(root) // .lecodes/ is per-checkout state — keep it out of the project's git repo
|
|
50
|
+
|
|
51
|
+
const commits = await getCommits(apiUrl, token, uuid)
|
|
52
|
+
writeManifest(root, { uuid, apiUrl, name: project.name, baseHeadId: commits.headId, files })
|
|
53
|
+
|
|
54
|
+
success(`Cloned ${c.bold(project.name)} into ${c.bold(dir)}/ (${Object.keys(files).length} files)`)
|
|
55
|
+
info(c.dim(` cd ${dir} — edit, then \`lecodes push -m "your message"\``))
|
|
56
|
+
},
|
|
57
|
+
})
|