lecodes-cli 0.20.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +56 -57
  2. package/dist/index.js +5918 -5020
  3. package/package.json +7 -5
  4. package/runtime/materials/decal-relief.mat +12 -1
  5. package/runtime/materials/decal.mat +17 -3
  6. package/runtime/materials/lightmap-baked-lite.mat +6 -1
  7. package/runtime/materials/lightmap-baked.mat +6 -1
  8. package/runtime/materials/lightmap.mat +3 -0
  9. package/runtime/scene-harness.json +1 -1
  10. package/runtime/sdk-types.json +1 -1
  11. package/runtime/web/assets/createViewerLite-DHqTFhWB.js +866 -0
  12. package/runtime/web/assets/{index-BMt7AnC5.js → index-Q06c6oHx.js} +1 -1
  13. package/runtime/web/embed.js +1 -1
  14. package/src/api.ts +1 -302
  15. package/src/cli/args.ts +37 -0
  16. package/src/cli/command.ts +113 -0
  17. package/src/cli/errors.ts +13 -0
  18. package/src/cli/help.ts +133 -0
  19. package/src/cli/index.ts +7 -0
  20. package/src/cli/output.ts +102 -0
  21. package/src/cli/run.ts +106 -0
  22. package/src/commands/{appAndroid.ts → app/android.ts} +451 -42
  23. package/src/commands/{appDesktop.ts → app/desktop.ts} +63 -32
  24. package/src/commands/{appDesktopMac.ts → app/desktopMac.ts} +24 -18
  25. package/src/commands/{appIcon.ts → app/icon.ts} +2 -2
  26. package/src/commands/{app.ts → app/index.ts} +397 -289
  27. package/src/commands/{appShared.ts → app/shared.ts} +30 -39
  28. package/src/commands/{appTemplatesAndroid.ts → app/templates/android.ts} +62 -28
  29. package/src/commands/{appTemplatesGradlew.ts → app/templates/gradlew.ts} +1 -1
  30. package/src/commands/{appTemplates.ts → app/templates/ios.ts} +1 -1
  31. package/src/commands/assets.ts +23 -21
  32. package/src/commands/clone.ts +48 -51
  33. package/src/commands/compile.ts +62 -88
  34. package/src/commands/create.ts +46 -47
  35. package/src/commands/design/comments.ts +361 -0
  36. package/src/commands/design/context.ts +101 -0
  37. package/src/commands/design/index.ts +391 -0
  38. package/src/commands/design/snapshot.ts +134 -0
  39. package/src/commands/{designTemplates.ts → design/templates.ts} +4 -3
  40. package/src/commands/desktop.ts +90 -82
  41. package/src/commands/dev.ts +201 -176
  42. package/src/commands/diff.ts +39 -51
  43. package/src/commands/index.ts +60 -0
  44. package/src/commands/{init.ts → init/index.ts} +199 -191
  45. package/src/commands/install.ts +80 -67
  46. package/src/commands/lightmap.ts +203 -171
  47. package/src/commands/link.ts +87 -94
  48. package/src/commands/login.ts +25 -23
  49. package/src/commands/navmesh.ts +168 -138
  50. package/src/commands/pn.ts +201 -244
  51. package/src/commands/pull.ts +49 -56
  52. package/src/commands/push.ts +48 -54
  53. package/src/commands/render.ts +158 -99
  54. package/src/commands/scene.ts +58 -55
  55. package/src/commands/{shaders.ts → shaders/index.ts} +61 -42
  56. package/src/commands/{shadersNew.ts → shaders/new.ts} +2 -2
  57. package/src/commands/shared.ts +79 -0
  58. package/src/commands/status.ts +22 -26
  59. package/src/commands/test.ts +398 -371
  60. package/src/commands/thumbs.ts +185 -175
  61. package/src/commands/update/index.ts +226 -0
  62. package/src/commands/{types.ts → update/types.ts} +31 -22
  63. package/src/compile/collect.ts +5 -5
  64. package/src/compile/collectLocal.ts +3 -3
  65. package/src/compile/designCompile.ts +3 -3
  66. package/src/compile/headlessBundle.ts +133 -129
  67. package/src/compile/projectCompile.ts +2 -2
  68. package/src/compile/sceneCompile.ts +10 -8
  69. package/src/compile/screenEntry.ts +123 -127
  70. package/src/compile/shaders.ts +3 -3
  71. package/src/dev/androidDev.ts +1 -1
  72. package/src/dev/devServer.ts +3 -3
  73. package/src/dev/webRunner.ts +1 -1
  74. package/src/{cmgenTool.ts → hosts/cmgenTool.ts} +1 -1
  75. package/src/{desktopRenderer.ts → hosts/desktopRenderer.ts} +6 -5
  76. package/src/{desktopScript.ts → hosts/desktopScript.ts} +1 -1
  77. package/src/{distRoot.ts → hosts/distRoot.ts} +9 -5
  78. package/src/{matcTool.ts → hosts/matcTool.ts} +1 -1
  79. package/src/{peerInstall.ts → hosts/peerInstall.ts} +2 -2
  80. package/src/{releases.ts → hosts/releases.ts} +1 -1
  81. package/src/index.ts +32 -480
  82. package/src/platform/api.ts +302 -0
  83. package/src/{browserAuth.ts → platform/browserAuth.ts} +1 -1
  84. package/src/{config.ts → platform/config.ts} +1 -1
  85. package/src/{serverDiff.ts → platform/serverDiff.ts} +3 -3
  86. package/src/{projectEnv.ts → project/env.ts} +33 -2
  87. package/src/{localFiles.ts → project/localFiles.ts} +1 -1
  88. package/src/{manifest.ts → project/manifest.ts} +1 -1
  89. package/src/{project.ts → project/materialize.ts} +2 -2
  90. package/src/project/paths.ts +20 -0
  91. package/src/{textDiff.ts → project/textDiff.ts} +1 -1
  92. package/src/{types.ts → project/types.ts} +0 -0
  93. package/runtime/web/assets/createViewerLite-Ct_PZdof.js +0 -852
  94. package/src/commands/design.ts +0 -846
  95. package/src/commands/update.ts +0 -211
  96. package/src/util.ts +0 -146
  97. /package/src/commands/{projectTemplates.ts → init/templates.ts} +0 -0
  98. /package/src/{lecodes-3d-editor.d.ts → declarations/lecodes-3d-editor.d.ts} +0 -0
  99. /package/src/{lecodes-assets.d.ts → declarations/lecodes-assets.d.ts} +0 -0
  100. /package/src/{lecodes-design.d.ts → declarations/lecodes-design.d.ts} +0 -0
  101. /package/src/{lecodes-renderer.d.ts → declarations/lecodes-renderer.d.ts} +0 -0
  102. /package/src/{qrcode-terminal.d.ts → declarations/qrcode-terminal.d.ts} +0 -0
  103. /package/src/{peers.ts → hosts/peers.ts} +0 -0
  104. /package/src/{designMeta.ts → project/designMeta.ts} +0 -0
  105. /package/src/{ignore.ts → project/ignore.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 "../manifest"
6
- import { DEFAULT_IGNORE_FILE } from "../ignore"
7
- import { CliError, c, flagStr, log, note, warn, type Args } from "../util"
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 (app.ts = iOS + dispatch,
10
- * appAndroid.ts = Android). Everything here moved verbatim out of app.ts when the Android
11
- * arm landed — behavior for iOS is unchanged; the SDK helpers just grew a repo parameter
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,11 +53,14 @@ 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 — the store/key passwords live in android/
57
- * keystore.properties (gitignored) or LECODES_ANDROID_* env vars, never in app.json. */
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
65
  /** Desktop host (packages/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
@@ -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 2D engine and no native-view
71
- * plugins). Overridable per run with `--renderer` / LECODES_DESKTOP_RENDERER. */
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
@@ -88,7 +91,7 @@ export interface AppConfig {
88
91
  * a built folder's host reads it from the staged app.json. Headless render/test ignore it. */
89
92
  virtualKeyboard?: boolean | DesktopVirtualKeyboard,
90
93
  /** The macOS .app build (`lecodes app build desktop` on a Mac) — identity + signing +
91
- * notarization. See appDesktopMac.ts for the whole story. */
94
+ * notarization. See desktopMac.ts for the whole story. */
92
95
  macos?: MacDesktopConfig,
93
96
  }
94
97
  }
@@ -181,13 +184,14 @@ export const readAppConfigAt = (root: string): AppConfig | null =>
181
184
  export const DESKTOP_DEFAULT_WINDOW = { width: 1280, height: 720 }
182
185
 
183
186
  /**
184
- * The renderer for this run: `--renderer` > LECODES_DESKTOP_RENDERER > app.json `desktop.renderer`
185
- * > "gl". Resolve it ONCE per command and pass the result to both `resolveDesktopExe` and the
186
- * shader staging — two independent reads of the config is how a build ends up with a Vulkan exe
187
- * next to an OpenGL .filamat, which fails at the first material with a Filament abort.
187
+ * The renderer for this run: the `--renderer` flag > LECODES_DESKTOP_RENDERER > app.json
188
+ * `desktop.renderer` > "gl". Resolve it ONCE per command and pass the result to both
189
+ * `resolveDesktopExe` and the shader staging — two independent reads of the config is how a build
190
+ * ends up with a Vulkan exe next to an OpenGL .filamat, which fails at the first material with a
191
+ * Filament abort.
188
192
  */
189
- export const resolveDesktopRenderer = (root: string, args?: Args): "gl" | "vulkan" => {
190
- const raw = (args ? flagStr(args, "renderer") : undefined)
193
+ export const resolveDesktopRenderer = (root: string, renderer?: string): "gl" | "vulkan" => {
194
+ const raw = renderer
191
195
  ?? process.env.LECODES_DESKTOP_RENDERER
192
196
  ?? readAppConfigAt(root)?.desktop?.renderer
193
197
  if (raw === undefined) return "gl"
@@ -303,10 +307,16 @@ export const git = (cwd: string, ...args: string[]): string =>
303
307
 
304
308
  export const slugify = (s: string) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "app"
305
309
 
310
+ /** The environment for a child process. Spelled out on every spawn on purpose: Bun hands an
311
+ * inherited child the environment SNAPSHOT taken at startup, so anything the project .env
312
+ * contributed (projectEnv.ts — keystore passwords, CREATOR_* tracing) would never arrive. */
313
+ export const childEnv = (extra: Record<string, string> = {}): Record<string, string | undefined> =>
314
+ ({ ...process.env, ...extra })
315
+
306
316
  /** Run a tool, capturing output; on failure surface the tail (xcodebuild/gradle logs are huge). */
307
317
  export const runTool = (cwd: string, cmd: string, args: string[]): string => {
308
318
  try {
309
- return execFileSync(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 128 * 1024 * 1024 })
319
+ return execFileSync(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 128 * 1024 * 1024, env: childEnv() })
310
320
  } catch (e) {
311
321
  const err = e as { stdout?: string, stderr?: string }
312
322
  const tail = ((err.stdout ?? "") + "\n" + (err.stderr ?? "")).split("\n").filter(l => l.trim()).slice(-25).join("\n")
@@ -329,7 +339,7 @@ export const ensureShellIgnored = (root: string, dir: string) => {
329
339
  /* ----------------------------------- SDK variants ----------------------------------- */
330
340
 
331
341
  // Both native SDKs ship the same variant matrix (full / 3d / 2d / core), so the detection and
332
- // resolution logic is shared between the iOS (app.ts) and Android (appAndroid.ts) syncs.
342
+ // resolution logic is shared between the iOS (index.ts) and Android (android.ts) syncs.
333
343
  export const SDK_VARIANTS = ["core", "2d", "3d", "full"] as const
334
344
  type SdkVariantShared = (typeof SDK_VARIANTS)[number]
335
345
 
@@ -346,25 +356,6 @@ export const detectEngines = (bundleJs: string): { gl: boolean, g2: boolean } =>
346
356
  return { gl: !/^\/\/ gl: disable$/m.test(header), g2: /^\/\/ 2d: enable$/m.test(header) }
347
357
  }
348
358
 
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
359
  /** The SDK product for a shell: the smallest variant covering the engines the compiled bundle
369
360
  * actually uses, unless <configKey> (ios.variant / android.variant) forces one — then warn when
370
361
  * the forced product can't run the project (missing engine = those scenes silently no-op on
@@ -1,5 +1,5 @@
1
1
  /* Templates for `lecodes app init android` / `lecodes app sync` — the Android mirror of
2
- * appTemplates.ts, distilled from the LetaryCreatorViewer host (lecodes-android :app). The
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,
@@ -234,16 +234,17 @@ export const ANDROID_README = `# Android shell
234
234
  Generated by \`lecodes app init android\`. Daily workflow:
235
235
 
236
236
  \`\`\`
237
- lecodes app sync # embed the compiled bundle + plugins into this shell
238
- lecodes app open android # open in Android Studio
239
- lecodes app build android # signed release APK + AAB (gradlew assembleRelease bundleRelease)
237
+ lecodes app sync # embed the compiled bundle + plugins into this shell
238
+ lecodes app open android # open in Android Studio
239
+ lecodes app keystore create # once: the release signing key (or \`keystore use <file>\`)
240
+ lecodes app build android # signed release .apk + .aab → ../dist/
240
241
  \`\`\`
241
242
 
242
243
  ## Ownership
243
244
 
244
245
  | Path | Owner |
245
246
  | --- | --- |
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) |
247
+ | \`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
248
  | \`app/src/main/assets/\` | CLI-owned — wiped and regenerated by every sync (the compiled \`app.js\` + its \`// preload:\` assets) |
248
249
  | \`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
250
  | \`settings.gradle.kts\`, \`build.gradle.kts\` (root), \`gradle.properties\`, \`lecodes-runtime/\`, \`plugins/\` | CLI-owned — regenerated by every sync |
@@ -280,17 +281,42 @@ dependencies they need to \`app/build.gradle.kts\` — that file is yours.
280
281
 
281
282
  ## Release signing
282
283
 
283
- Create \`android/keystore.properties\` (gitignored):
284
+ The key lives in the project's \`.env\` (next to \`app.json\`; the CLI loads it for every command
285
+ and never pushes it):
284
286
 
285
287
  \`\`\`
286
- storeFile=release.keystore
287
- storePassword=…
288
- keyAlias=…
289
- keyPassword=…
288
+ LECODES_ANDROID_KEYSTORE_FILE=./android/release.keystore # ./ = the project root; ~/ and absolute paths work too
289
+ LECODES_ANDROID_KEYSTORE_PASSWORD=…
290
+ LECODES_ANDROID_KEY_ALIAS=upload
291
+ LECODES_ANDROID_KEY_PASSWORD=…
290
292
  \`\`\`
291
293
 
292
- or export \`LECODES_ANDROID_KEYSTORE_FILE\` / \`LECODES_ANDROID_KEYSTORE_PASSWORD\` /
293
- \`LECODES_ANDROID_KEY_ALIAS\` / \`LECODES_ANDROID_KEY_PASSWORD\`. Never put passwords in app.json.
294
+ \`lecodes app keystore create\` makes a new key and writes those lines; \`lecodes app keystore use
295
+ <file>\` attaches an existing one (a keystore outside the project is just a path); \`lecodes app
296
+ keystore info\` prints the SHA-1 / SHA-256 fingerprints the Play Console, Firebase and Google
297
+ Sign-In ask for. The same four names work as real environment variables (CI). Building from
298
+ Android Studio without the CLI? \`android/keystore.properties\` (gitignored, \`storeFile\` /
299
+ \`storePassword\` / \`keyAlias\` / \`keyPassword\`) is still honoured and wins over the env.
300
+ Never put passwords in app.json — \`android.signing\` there is metadata only.
301
+
302
+ With Play App Signing (the default for new Play apps) this key is the UPLOAD key — Google holds
303
+ the app signing key and a lost upload key can be reset through Play support. Without it, losing
304
+ the keystore means the app can never be updated. Back it up either way.
305
+
306
+ ## Building
307
+
308
+ \`lecodes app build android\` syncs, runs \`gradlew assembleRelease bundleRelease\` and copies the
309
+ results to \`../dist/<name>-<version>-<versionCode>.apk|.aab\` (\`-o <dir>\` elsewhere). A release
310
+ without a signing key is refused (\`--allow-unsigned\` for a throwaway build); \`--configuration
311
+ Debug\` signs with the debug keystore. \`--apk\` / \`--aab\` pick one artifact.
312
+
313
+ Release builds run R8 (\`isMinifyEnabled\` + \`isShrinkResources\`): the SDK AAR ships the keep
314
+ rules its JNI bindings need, plugins ship theirs, \`app/proguard-rules.pro\` is for your own
315
+ classes. \`app.json\` \`"android": { "minify": false }\` turns it off (sync restamps the values).
316
+ R8 only touches the dex — the engine's native libraries and \`app.js\` are not shrunk.
317
+
318
+ \`abiFilters\` in \`app/build.gradle.kts\` keeps the package to the two ABIs the SDK ships
319
+ (arm64-v8a, armeabi-v7a).
294
320
 
295
321
  ## Icons
296
322
 
@@ -314,11 +340,11 @@ the init-time placeholder.
314
340
 
315
341
  /* ---------------------------- USER-owned (written once) ---------------------------- */
316
342
 
317
- export const appBuildGradleTemplate = ({ applicationId, version, versionCode }: AndroidShellIdentity): string => `import java.util.Properties
343
+ export const appBuildGradleTemplate = ({ applicationId, version, versionCode }: AndroidShellIdentity, minify = true): string => `import java.util.Properties
318
344
 
319
345
  // Generated by \`lecodes app init\` — yours to edit. \`lecodes app sync\` only restamps the
320
- // versionName/versionCode VALUES and manages \`// lecodes:plugin\`-tagged lines in the plugins
321
- // block below; everything else is left alone.
346
+ // versionName/versionCode and isMinifyEnabled/isShrinkResources VALUES and manages
347
+ // \`// lecodes:plugin\`-tagged lines in the plugins block below; everything else is left alone.
322
348
  plugins {
323
349
  id("com.android.application")
324
350
  id("org.jetbrains.kotlin.android")
@@ -342,6 +368,9 @@ android {
342
368
  targetSdk = ${TARGET_SDK}
343
369
  versionCode = ${versionCode}
344
370
  versionName = "${version}"
371
+ // The LeCodes SDK ships arm64-v8a + armeabi-v7a natives. A transitive dependency adds a
372
+ // lone x86_64 library, which would make the package claim an ABI the engine can't load on.
373
+ ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a") }
345
374
  }
346
375
 
347
376
  val storeFilePath = signing("storeFile", "LECODES_ANDROID_KEYSTORE_FILE")
@@ -356,7 +385,12 @@ android {
356
385
 
357
386
  buildTypes {
358
387
  release {
359
- isMinifyEnabled = false
388
+ // R8 (shrink + optimize + obfuscate). The SDK AAR carries the keep rules its JNI
389
+ // bindings need; app-local rules go in proguard-rules.pro. app.json
390
+ // android.minify: false turns it off (sync restamps these two values).
391
+ isMinifyEnabled = ${minify}
392
+ isShrinkResources = ${minify}
393
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
360
394
  signingConfig = signingConfigs.findByName("release")
361
395
  }
362
396
  }
@@ -378,6 +412,18 @@ dependencies {
378
412
  }
379
413
  `
380
414
 
415
+ /** app/proguard-rules.pro — user-owned R8 rules for app-local code. The SDK's own keep rules
416
+ * (its JNI-bound classes) ride inside the AAR as consumer rules, and plugin modules ship theirs
417
+ * through consumerProguardFiles, so this file starts empty. */
418
+ export const APP_PROGUARD_RULES = `# R8 rules for app-local code (yours — \`lecodes app sync\` never touches this file).
419
+ #
420
+ # The LeCodes SDK keeps its own JNI-bound classes through the consumer rules inside the AAR, and
421
+ # plugin modules bring their own. Add rules here only for classes YOUR Kotlin registers with the
422
+ # engine by name (engine.registerCallback / registerService / registerView) or reaches through
423
+ # reflection, e.g.
424
+ # -keep class com.example.myapp.MyBridge { *; }
425
+ `
426
+
381
427
  export const appManifestTemplate = (): string => `<?xml version="1.0" encoding="utf-8"?>
382
428
  <!-- Generated by \`lecodes app init\` — yours to edit. Sync leaves it alone, except that with
383
429
  app.json "icon" set it restamps the android:icon VALUE to the generated @mipmap set.
@@ -404,18 +450,6 @@ export const appManifestTemplate = (): string => `<?xml version="1.0" encoding="
404
450
  <action android:name="android.intent.action.MAIN" />
405
451
  <category android:name="android.intent.category.LAUNCHER" />
406
452
  </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
453
  </activity>
420
454
  </application>
421
455
 
@@ -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 appAndroid.ts). JSON-escaped mechanically —
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 "./appShared"
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
@@ -1,24 +1,26 @@
1
- import { type Args } from "../util"
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 <convert|probe|doctor|sky> …` — the LeCodes asset pipeline (packages/lecodes-assets):
7
- * FBX → GLB (ufbx wasm + glTF-Transform), `--clips` merged by bone name, textures embedded, GLB
8
- * doctor (bone cap / texture normalize / KTX2). The package is an optional peer (like the design
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 (packages/lecodes-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 const assets = async (args: Args & { _raw?: string[] }) => {
13
- const mod = await loadPeer<typeof import("lecodes-assets/cli")>("lecodes-assets", "cli", { for: "lecodes assets" })
14
- const raw = args._raw ?? args._
15
- // `sky` shells out to filament's cmgen, which the asset package deliberately never downloads
16
- // itself (it stays network-free and usable standalone). The CLI owns release artifacts, so it
17
- // resolves the binary — cache or first-use download — and hands it over through the env var the
18
- // package already honours. An explicit LECODES_CMGEN wins, so a local filament build still rules.
19
- if (raw[0] === "sky" && !process.env.LECODES_CMGEN) {
20
- process.env.LECODES_CMGEN = await resolveCmgenExe()
21
- }
22
- const code = await mod.runAssetsCli(raw)
23
- if (code !== 0) process.exitCode = code
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
+ })
@@ -1,60 +1,57 @@
1
1
  import { existsSync, mkdirSync, readdirSync } from "node:fs"
2
2
  import { resolve } from "node:path"
3
- import { getCommits, getProject } from "../api"
4
- import { loadConfig, requireApiUrl, requireToken } from "../config"
5
- import { writeManifest } from "../manifest"
6
- import { materializeAssets } from "../project"
7
- import { materializeTypes, writeTsconfig } from "../types"
8
- import { ensureGitignore, writeDefaultIgnore } from "../ignore"
9
- import { CliError, c, info, success, progress, endProgress, flagBool, type Args } from "../util"
10
-
11
- /* `lecodes clone <uuid|url> [dir]` — download a project's files + types into a new folder. */
12
-
13
- /**
14
- * Accept a bare uuid or a project link and pull out the uuid. Handles editor links
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 CliError("Usage: lecodes clone <project-uuid|url> [directory]")
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
- const slug = (name: string) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project"
27
-
28
- export const clone = async (args: Args) => {
29
- const config = loadConfig()
30
- const apiUrl = requireApiUrl(config)
31
- const token = requireToken(config)
32
-
33
- const uuid = parseUuid(args._[0])
34
- const project = await getProject(apiUrl, token, uuid)
35
-
36
- const dir = args._[1] ?? slug(project.name)
37
- const root = resolve(dir)
38
- if (existsSync(root) && readdirSync(root).length > 0) {
39
- throw new CliError(`Directory "${dir}" already exists and is not empty.`)
40
- }
41
- mkdirSync(root, { recursive: true })
42
-
43
- info(c.dim(`Fetching project ${c.bold(project.name)}…`))
44
- const files = await materializeAssets(root, project, apiUrl, token, progress)
45
- endProgress()
46
-
47
- if (!flagBool(args, "no-types")) {
48
- info(c.dim("Fetching type definitions…"))
49
- await materializeTypes(root, apiUrl)
50
- writeTsconfig(root)
51
- }
52
- writeDefaultIgnore(root)
53
- ensureGitignore(root) // .lecodes/ is per-checkout state — keep it out of the project's git repo
54
-
55
- const commits = await getCommits(apiUrl, token, uuid)
56
- writeManifest(root, { uuid, apiUrl, name: project.name, baseHeadId: commits.headId, files })
57
-
58
- success(`Cloned ${c.bold(project.name)} into ${c.bold(dir)}/ (${Object.keys(files).length} files)`)
59
- info(c.dim(` cd ${dir} — edit, then \`lecodes push -m "your message"\``))
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
+ })