lecodes-cli 2.4.6 → 2.4.7

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 CHANGED
@@ -64,7 +64,7 @@ var package_default;
64
64
  var init_package = __esm(() => {
65
65
  package_default = {
66
66
  name: "lecodes-cli",
67
- version: "2.4.6",
67
+ version: "2.4.7",
68
68
  dependencies: {
69
69
  "@letary/chisel": "^0.15.0",
70
70
  jimp: "^1.6.1",
@@ -89,7 +89,7 @@ var init_package = __esm(() => {
89
89
  },
90
90
  peerDependencies: {
91
91
  "lecodes-design": "^0.11.0",
92
- "lecodes-headless": "^2.4.6",
92
+ "lecodes-headless": "^2.4.7",
93
93
  "lecodes-3d-editor": "^0.9.0",
94
94
  "lecodes-assets": "^0.2.1",
95
95
  "heic-decode": "^2.1.0",
@@ -159,7 +159,7 @@ var init_package = __esm(() => {
159
159
  });
160
160
 
161
161
  // ../../sdk/src/version.ts
162
- var SDK_VERSION = "2.4.6";
162
+ var SDK_VERSION = "2.4.7";
163
163
 
164
164
  // src/cli/args.ts
165
165
  var isNegativeNumber = (s) => /^-(\d|\.\d)/.test(s), isValue = (s) => !s.startsWith("-") || isNegativeNumber(s), parseArgv = (argv, booleans = new Set) => {
@@ -206398,7 +206398,7 @@ var init_esm29 = __esm(() => {
206398
206398
 
206399
206399
  // src/project/types.ts
206400
206400
  import { dirname as dirname18, join as join29, relative as relative5, sep as sep3 } from "node:path";
206401
- import { existsSync as existsSync27, mkdirSync as mkdirSync17, readdirSync as readdirSync12, readFileSync as readFileSync23, rmSync as rmSync9, statSync as statSync10, writeFileSync as writeFileSync20 } from "node:fs";
206401
+ import { existsSync as existsSync27, mkdirSync as mkdirSync17, readdirSync as readdirSync12, readFileSync as readFileSync24, rmSync as rmSync9, statSync as statSync10, writeFileSync as writeFileSync20 } from "node:fs";
206402
206402
  import { createRequire as createRequire5 } from "node:module";
206403
206403
  var TSCONFIG, listBundleFiles = (dir, base = dir) => {
206404
206404
  if (!existsSync27(dir))
@@ -206427,7 +206427,7 @@ var TSCONFIG, listBundleFiles = (dir, base = dir) => {
206427
206427
  const dest = join29(typesDir, path);
206428
206428
  if (!before.has(path))
206429
206429
  stats.added++;
206430
- else if (readFileSync23(dest, "utf8") !== text)
206430
+ else if (readFileSync24(dest, "utf8") !== text)
206431
206431
  stats.updated++;
206432
206432
  else
206433
206433
  continue;
@@ -206446,7 +206446,7 @@ var TSCONFIG, listBundleFiles = (dir, base = dir) => {
206446
206446
  const typesDir = join29(root, LECODES_DIR, "types");
206447
206447
  const bundle = {};
206448
206448
  for (const path of listBundleFiles(typesDir))
206449
- bundle[path] = readFileSync23(join29(typesDir, path), "utf8");
206449
+ bundle[path] = readFileSync24(join29(typesDir, path), "utf8");
206450
206450
  return bundle;
206451
206451
  }, materializeTypes = async (root, apiUrl) => writeTypeBundle(root, await getTypes(apiUrl)), readLocalTypeBundle = () => {
206452
206452
  const fromPackage = (() => {
@@ -206464,7 +206464,7 @@ var TSCONFIG, listBundleFiles = (dir, base = dir) => {
206464
206464
  for (const path of candidates) {
206465
206465
  if (existsSync27(path)) {
206466
206466
  try {
206467
- return JSON.parse(readFileSync23(path, "utf8"));
206467
+ return JSON.parse(readFileSync24(path, "utf8"));
206468
206468
  } catch {}
206469
206469
  }
206470
206470
  }
@@ -206502,7 +206502,7 @@ var init_types4 = __esm(() => {
206502
206502
  });
206503
206503
 
206504
206504
  // src/compile/designCompile.ts
206505
- import { readFileSync as readFileSync24 } from "node:fs";
206505
+ import { readFileSync as readFileSync25 } from "node:fs";
206506
206506
  var LOCAL_ASSET_SCHEME = "lecodesfile://", compileDesignScreen = async (opts) => {
206507
206507
  const { root, manifest, designDir, designKey, screenId, state } = opts;
206508
206508
  const entries = [];
@@ -206519,7 +206519,7 @@ var LOCAL_ASSET_SCHEME = "lecodesfile://", compileDesignScreen = async (opts) =>
206519
206519
  } else if (type === "shader") {
206520
206520
  entries.push({ path: file.path, type, shaders: [] });
206521
206521
  } else {
206522
- entries.push({ path: file.path, type, text: readFileSync24(file.absPath, "utf8") });
206522
+ entries.push({ path: file.path, type, text: readFileSync25(file.absPath, "utf8") });
206523
206523
  }
206524
206524
  }
206525
206525
  const invoke = state !== undefined ? `screen(${JSON.stringify(state)})` : "screen()";
@@ -206641,11 +206641,11 @@ var init_browserAuth = __esm(() => {
206641
206641
  });
206642
206642
 
206643
206643
  // src/project/designMeta.ts
206644
- import { readFileSync as readFileSync25 } from "node:fs";
206644
+ import { readFileSync as readFileSync26 } from "node:fs";
206645
206645
  import { join as join31 } from "node:path";
206646
206646
  var PHONE_SIZE, readMetaFrames = (designDir) => {
206647
206647
  try {
206648
- const meta = JSON.parse(readFileSync25(join31(designDir, "meta.json"), "utf8"));
206648
+ const meta = JSON.parse(readFileSync26(join31(designDir, "meta.json"), "utf8"));
206649
206649
  const d = meta?.defaultSize;
206650
206650
  const valid = Array.isArray(d) && d.length === 2 && d.every((n) => typeof n === "number" && Number.isFinite(n) && n >= 64 && n <= 8192);
206651
206651
  return {
@@ -206996,7 +206996,7 @@ var init_comments = __esm(() => {
206996
206996
  });
206997
206997
 
206998
206998
  // src/commands/design/snapshot.ts
206999
- import { existsSync as existsSync32, mkdirSync as mkdirSync20, readdirSync as readdirSync15, readFileSync as readFileSync26, writeFileSync as writeFileSync22 } from "node:fs";
206999
+ import { existsSync as existsSync32, mkdirSync as mkdirSync20, readdirSync as readdirSync15, readFileSync as readFileSync27, writeFileSync as writeFileSync22 } from "node:fs";
207000
207000
  import { join as join33 } from "node:path";
207001
207001
  var listScreens = (designDir) => {
207002
207002
  const dir = join33(designDir, "screens");
@@ -207068,7 +207068,7 @@ var init_snapshot = __esm(() => {
207068
207068
  const [width, height] = frames.screens[id]?.size ?? frames.defaultSize;
207069
207069
  let plan = [{ state: undefined, bases: [id] }];
207070
207070
  if (discover) {
207071
- const { states, canonical } = discover(readFileSync26(join33(ctx.designDir, "screens", `${id}.ts`), "utf8"));
207071
+ const { states, canonical } = discover(readFileSync27(join33(ctx.designDir, "screens", `${id}.ts`), "utf8"));
207072
207072
  if (stateFlag !== undefined) {
207073
207073
  if (!states.includes(stateFlag)) {
207074
207074
  if (args.length === 1)
@@ -207598,7 +207598,7 @@ var init_templates2 = __esm(() => {
207598
207598
  });
207599
207599
 
207600
207600
  // src/commands/design/index.ts
207601
- import { appendFileSync as appendFileSync2, existsSync as existsSync33, mkdirSync as mkdirSync21, readFileSync as readFileSync27, writeFileSync as writeFileSync23 } from "node:fs";
207601
+ import { appendFileSync as appendFileSync2, existsSync as existsSync33, mkdirSync as mkdirSync21, readFileSync as readFileSync28, writeFileSync as writeFileSync23 } from "node:fs";
207602
207602
  import { join as join34 } from "node:path";
207603
207603
  var firstLine = (body, max = 64) => {
207604
207604
  const line = body.split(`
@@ -207673,7 +207673,7 @@ var firstLine = (body, max = 64) => {
207673
207673
  let config = {};
207674
207674
  if (existsSync33(path)) {
207675
207675
  try {
207676
- config = JSON.parse(readFileSync27(path, "utf8"));
207676
+ config = JSON.parse(readFileSync28(path, "utf8"));
207677
207677
  } catch {
207678
207678
  warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-design server by hand.");
207679
207679
  return;
@@ -207753,7 +207753,7 @@ var init_design = __esm(() => {
207753
207753
  }
207754
207754
  const ignorePath = join34(ctx.root, ".lecodesignore");
207755
207755
  const ignoreLines = [`${ctx.dirName}/.snapshots/`, `${ctx.dirName}/comments/`];
207756
- const current = existsSync33(ignorePath) ? readFileSync27(ignorePath, "utf8") : "";
207756
+ const current = existsSync33(ignorePath) ? readFileSync28(ignorePath, "utf8") : "";
207757
207757
  const present = new Set(current.split(/\r?\n/));
207758
207758
  const missing = ignoreLines.filter((line) => !present.has(line));
207759
207759
  if (missing.length) {
@@ -227112,7 +227112,7 @@ import { delimiter as delimiter3, dirname as dirname31, join as join63 } from "n
227112
227112
  // src/commands/app/index.ts
227113
227113
  init_manifest();
227114
227114
  import { execFileSync as execFileSync6 } from "node:child_process";
227115
- import { cpSync as cpSync4, existsSync as existsSync25, mkdirSync as mkdirSync16, readFileSync as readFileSync22, readdirSync as readdirSync10, rmSync as rmSync8, statSync as statSync9, writeFileSync as writeFileSync19 } from "node:fs";
227115
+ import { cpSync as cpSync4, existsSync as existsSync25, mkdirSync as mkdirSync16, readFileSync as readFileSync23, readdirSync as readdirSync10, rmSync as rmSync8, statSync as statSync9, writeFileSync as writeFileSync19 } from "node:fs";
227116
227116
  import { homedir as homedir10 } from "node:os";
227117
227117
  import { basename as basename10, dirname as dirname17, join as join27, resolve as resolve14 } from "node:path";
227118
227118
 
@@ -229431,7 +229431,7 @@ init_compile();
229431
229431
  init_manifest();
229432
229432
  import { execFileSync as execFileSync2, spawn, spawnSync as spawnSync5 } from "node:child_process";
229433
229433
  import { createHash as createHash6, randomBytes as randomBytes2 } from "node:crypto";
229434
- import { chmodSync, cpSync, existsSync as existsSync21, mkdirSync as mkdirSync12, readFileSync as readFileSync20, readdirSync as readdirSync6, renameSync as renameSync2, rmSync as rmSync4, statSync as statSync5, writeFileSync as writeFileSync15 } from "node:fs";
229434
+ import { chmodSync, cpSync, existsSync as existsSync21, mkdirSync as mkdirSync12, readFileSync as readFileSync21, readdirSync as readdirSync6, renameSync as renameSync2, rmSync as rmSync4, statSync as statSync5, writeFileSync as writeFileSync15 } from "node:fs";
229435
229435
  import { homedir as homedir8 } from "node:os";
229436
229436
  import { basename as basename7, dirname as dirname15, join as join23, relative as relative4, resolve as resolve10 } from "node:path";
229437
229437
  init_cli();
@@ -231343,6 +231343,84 @@ if "%OS%"=="Windows_NT" endlocal
231343
231343
  :omega
231344
231344
  `;
231345
231345
 
231346
+ // src/commands/app/play.ts
231347
+ init_cli();
231348
+ import { sign } from "node:crypto";
231349
+ import { readFileSync as readFileSync20 } from "node:fs";
231350
+ var SCOPE = "https://www.googleapis.com/auth/androidpublisher";
231351
+ var apiBase = () => process.env.LECODES_PLAY_API_URL ?? "https://androidpublisher.googleapis.com";
231352
+ var readServiceAccount = (path) => {
231353
+ let key;
231354
+ try {
231355
+ key = JSON.parse(readFileSync20(path, "utf8"));
231356
+ } catch {
231357
+ throw new CliError(`${path} is not a JSON key file.`);
231358
+ }
231359
+ if (key.type !== "service_account" || !key.client_email || !key.private_key) {
231360
+ throw new CliError(`${path} is not a service account key (Google Cloud → IAM → Service accounts → Keys → Add key → JSON).`);
231361
+ }
231362
+ return key;
231363
+ };
231364
+ var serviceAccountToken = async (key, now = Math.floor(Date.now() / 1000)) => {
231365
+ const tokenUri = key.token_uri ?? "https://oauth2.googleapis.com/token";
231366
+ const part = (v) => Buffer.from(JSON.stringify(v)).toString("base64url");
231367
+ const unsigned = `${part({ alg: "RS256", typ: "JWT" })}.${part({ iss: key.client_email, scope: SCOPE, aud: tokenUri, iat: now, exp: now + 3600 })}`;
231368
+ const signature = sign("RSA-SHA256", Buffer.from(unsigned), key.private_key).toString("base64url");
231369
+ const res = await fetch(tokenUri, {
231370
+ method: "POST",
231371
+ headers: { "content-type": "application/x-www-form-urlencoded" },
231372
+ body: new URLSearchParams({ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion: `${unsigned}.${signature}` })
231373
+ });
231374
+ const body = await res.json().catch(() => ({}));
231375
+ if (!res.ok || !body.access_token) {
231376
+ throw new CliError(`Google refused the key of ${key.client_email}: ${body.error_description ?? body.error ?? `HTTP ${res.status}`} (a deleted or rotated key fails here — make a new one).`);
231377
+ }
231378
+ return body.access_token;
231379
+ };
231380
+ var HINTS = [
231381
+ [/caller does not have permission|insufficient permissions/i, () => 'invite the service account in Play Console → Users and permissions with "Release to testing tracks" for this app (a new grant can take hours to reach the API)'],
231382
+ [/package not found/i, (r) => `Play Console has no app ${r.packageName} — create it there first (the API cannot); app.json android.applicationId must match`],
231383
+ [/version code .*already been used/i, () => "bump app.json android.versionCode and build again"],
231384
+ [/only releases with status draft/i, () => "the app was never published — upload with --draft, then roll the release out in Play Console"],
231385
+ [/has not been used in project|is disabled/i, () => "enable the Google Play Android Developer API in the key's Cloud project"]
231386
+ ];
231387
+ var playError = (step, status, text, release) => {
231388
+ let message = text;
231389
+ try {
231390
+ message = JSON.parse(text).error?.message ?? text;
231391
+ } catch {}
231392
+ const hint = HINTS.find(([re]) => re.test(message))?.[1](release);
231393
+ return new CliError(`Google Play refused the ${step} (HTTP ${status}): ${message.trim()}${hint ? `
231394
+ → ${hint}` : ""}`);
231395
+ };
231396
+ var uploadToPlay = async (token, release) => {
231397
+ const app = `applications/${encodeURIComponent(release.packageName)}`;
231398
+ const call = async (step, method, path, body) => {
231399
+ const bytes = body instanceof Uint8Array;
231400
+ const res = await fetch(`${apiBase()}/${path}`, {
231401
+ method,
231402
+ headers: {
231403
+ authorization: `Bearer ${token}`,
231404
+ ...body ? { "content-type": bytes ? "application/octet-stream" : "application/json" } : {}
231405
+ },
231406
+ body: bytes ? body : body ? JSON.stringify(body) : undefined
231407
+ });
231408
+ const text = await res.text();
231409
+ if (!res.ok)
231410
+ throw playError(step, res.status, text, release);
231411
+ return text ? JSON.parse(text) : {};
231412
+ };
231413
+ const edit = await call("edit", "POST", `androidpublisher/v3/${app}/edits`, {});
231414
+ const editPath = `androidpublisher/v3/${app}/edits/${encodeURIComponent(edit.id)}`;
231415
+ const bundle = await call("bundle", "POST", `upload/${editPath}/bundles?uploadType=media`, release.aab);
231416
+ await call("track", "PUT", `${editPath}/tracks/${encodeURIComponent(release.track)}`, {
231417
+ track: release.track,
231418
+ releases: [{ versionCodes: [String(bundle.versionCode)], status: release.status }]
231419
+ });
231420
+ await call("commit", "POST", `${editPath}:commit`);
231421
+ return { versionCode: bundle.versionCode };
231422
+ };
231423
+
231346
231424
  // src/commands/app/android.ts
231347
231425
  var ANDROID_SDK_REPO = "letary/lecodes-android-sdk";
231348
231426
  var ANDROID_SDK_MIN_VERSION = "2.0.13";
@@ -231354,7 +231432,7 @@ var shellSdkVersionAndroid = (androidDir) => {
231354
231432
  const path = join23(androidDir, "lecodes-runtime", "build.gradle.kts");
231355
231433
  if (!existsSync21(path))
231356
231434
  return null;
231357
- const m = /io\.letary:lecodes-sdk(?:-(?:core|2d|3d))?:(\d+\.\d+\.\d+)@aar/.exec(readFileSync20(path, "utf8"));
231435
+ const m = /io\.letary:lecodes-sdk(?:-(?:core|2d|3d))?:(\d+\.\d+\.\d+)@aar/.exec(readFileSync21(path, "utf8"));
231358
231436
  return m ? m[1] : null;
231359
231437
  };
231360
231438
  var GRADLE_WRAPPER_JAR_URL = "https://raw.githubusercontent.com/gradle/gradle/v8.13.0/gradle/wrapper/gradle-wrapper.jar";
@@ -231362,7 +231440,7 @@ var GRADLE_WRAPPER_JAR_SHA256 = "81a82aaea5abcc8ff68b3dfcb58b3c3c429378efd98e743
231362
231440
  var ensureGradleWrapperJar = async () => {
231363
231441
  const cached = join23(homedir8(), ".lecodes", "gradle-wrapper", GRADLE_VERSION, "gradle-wrapper.jar");
231364
231442
  if (existsSync21(cached))
231365
- return new Uint8Array(readFileSync20(cached));
231443
+ return new Uint8Array(readFileSync21(cached));
231366
231444
  try {
231367
231445
  const res = await fetch(GRADLE_WRAPPER_JAR_URL);
231368
231446
  if (!res.ok)
@@ -231493,7 +231571,7 @@ var syncAndroid = async (root, config, prior) => {
231493
231571
  resources = prior.resources;
231494
231572
  } else {
231495
231573
  resources = await compileShell(join23(assetsDir, "app.js"), { remotePatterns });
231496
- bundleJs = readFileSync20(join23(assetsDir, "app.js"), "utf8");
231574
+ bundleJs = readFileSync21(join23(assetsDir, "app.js"), "utf8");
231497
231575
  }
231498
231576
  const requestedVariant = config.android?.variant ?? "auto";
231499
231577
  if (requestedVariant !== "auto" && !ANDROID_SDK_VARIANTS.includes(requestedVariant)) {
@@ -231638,7 +231716,7 @@ var restampManifestBlock = (androidDir, tag, block, what, missing, after = []) =
231638
231716
  const path = join23(androidDir, "app", "src", "main", "AndroidManifest.xml");
231639
231717
  if (!existsSync21(path))
231640
231718
  return;
231641
- const content = readFileSync20(path, "utf8");
231719
+ const content = readFileSync21(path, "utf8");
231642
231720
  const tagged = new RegExp(`\\n[ \\t]*<!-- lecodes:${tag}[\\s\\S]*?<!-- \\/lecodes:${tag} -->`);
231643
231721
  let next;
231644
231722
  if (tagged.test(content))
@@ -231667,7 +231745,7 @@ var restampAppBuildGradle = (androidDir, config, gradlePlugins) => {
231667
231745
  warn("android/app/build.gradle.kts is missing — run `lecodes app init android` on a clean tree to regenerate the shell.");
231668
231746
  return;
231669
231747
  }
231670
- let content = readFileSync20(path, "utf8");
231748
+ let content = readFileSync21(path, "utf8");
231671
231749
  const before = content;
231672
231750
  const versionName = config.version ?? "1.0.0";
231673
231751
  const versionCode = config.android?.versionCode ?? 1;
@@ -231818,10 +231896,9 @@ var buildAndroid = async (flags) => {
231818
231896
  }
231819
231897
  if (produced.length === 0)
231820
231898
  throw new CliError("Gradle finished but produced no .apk/.aab — check android/app/build/outputs.");
231821
- const slug = flags.slug ?? slugify(config.name ?? basename7(root));
231822
231899
  const outDir = resolve10(root, flags.out ?? join23(root, "dist"));
231823
231900
  mkdirSync12(outDir, { recursive: true });
231824
- const stem = `${slug}-${config.version ?? "1.0.0"}-${config.android.versionCode ?? 1}${configuration === "debug" ? "-debug" : ""}`;
231901
+ const stem = `${artifactStem(root, config, flags.slug)}${configuration === "debug" ? "-debug" : ""}`;
231825
231902
  for (const src of produced) {
231826
231903
  const unsigned = configuration === "release" && !signing;
231827
231904
  const dest = join23(outDir, `${stem}${unsigned ? "-unsigned" : ""}${src.endsWith(".aab") ? ".aab" : ".apk"}`);
@@ -231829,7 +231906,7 @@ var buildAndroid = async (flags) => {
231829
231906
  const size = (statSync5(dest).size / (1024 * 1024)).toFixed(1);
231830
231907
  log(`${c.green("output")} ${relative4(root, dest) || dest}${c.dim(` (${size} MB)`)}`);
231831
231908
  }
231832
- success(configuration === "debug" ? "Debug build done — `adb install` the .apk to test (or `lecodes dev --android --apk <file>`)." : signing ? "Release build done — upload the .aab in the Play Console (the .apk side-loads for testing)." : "Unsigned release build done — sign it before it goes anywhere.");
231909
+ success(configuration === "debug" ? "Debug build done — `adb install` the .apk to test (or `lecodes dev --android --apk <file>`)." : signing ? "Release build done — `lecodes app upload android` sends the .aab to Play (the .apk side-loads for testing)." : "Unsigned release build done — sign it before it goes anywhere.");
231833
231910
  };
231834
231911
  var androidBuild = defineCommand({
231835
231912
  name: "android",
@@ -231839,6 +231916,69 @@ var androidBuild = defineCommand({
231839
231916
  examples: ["lecodes app build android", "lecodes app build android --configuration Debug --apk", "lecodes app build android -o out/"],
231840
231917
  run: ({ flags }) => buildAndroid(flags)
231841
231918
  });
231919
+ var artifactStem = (root, config, slug) => `${slug ?? slugify(config.name ?? basename7(root))}-${config.version ?? "1.0.0"}-${config.android?.versionCode ?? 1}`;
231920
+ var PLAY_KEY_ENV = "LECODES_PLAY_SERVICE_ACCOUNT";
231921
+ var androidUploadFlags = {
231922
+ aab: str("the bundle (default: the dist/<name>-<version>-<versionCode>.aab `app build android` wrote)", { value: "<file>" }),
231923
+ track: str("internal | alpha | beta | production | a closed track's name", { value: "<t>", default: "internal" }),
231924
+ draft: bool("a draft release, rolled out in Play Console (the only kind a never-published app takes)"),
231925
+ key: str("the service account's JSON key (remembered in the project .env)", { value: "<json>" }),
231926
+ slug: str("file name stem of the default bundle, as given to `app build android --slug`", { value: "<name>" })
231927
+ };
231928
+ var uploadAndroid = async (flags) => {
231929
+ const root = projectRoot();
231930
+ const config = readAppConfigAt(root);
231931
+ if (!config?.android?.applicationId || !existsSync21(join23(root, "android"))) {
231932
+ throw new CliError("No Android shell here — run `lecodes app init android` first.");
231933
+ }
231934
+ const packageName = config.android.applicationId;
231935
+ const aabPath = flags.aab ? resolve10(flags.aab) : join23(root, "dist", `${artifactStem(root, config, flags.slug)}.aab`);
231936
+ if (!existsSync21(aabPath)) {
231937
+ throw new CliError(`No bundle at ${relative4(root, aabPath) || aabPath} — run \`lecodes app build android\` first (it names the file by app.json's version + versionCode).`);
231938
+ }
231939
+ const given = flags.key;
231940
+ const keyPath = given ? resolve10(given.startsWith("~/") ? join23(homedir8(), given.slice(2)) : given) : process.env[PLAY_KEY_ENV];
231941
+ if (!keyPath) {
231942
+ throw new CliError([
231943
+ `A Google Play service account key is needed: --key <file.json> (remembered in the project .env as ${PLAY_KEY_ENV}).`,
231944
+ " Google Cloud: enable the Google Play Android Developer API, then IAM → Service accounts → Keys → Add key → JSON;",
231945
+ ` Play Console → Users and permissions: invite the account's email with "Release to testing tracks" for the app.`
231946
+ ].join(`
231947
+ `));
231948
+ }
231949
+ if (!existsSync21(keyPath))
231950
+ throw new CliError(`Service account key not found: ${keyPath}`);
231951
+ const key = readServiceAccount(keyPath);
231952
+ if (given) {
231953
+ upsertDotEnv(root, { [PLAY_KEY_ENV]: envValue(envPathFor(root, keyPath)) });
231954
+ note(`Stored the key reference in .env (${PLAY_KEY_ENV})`);
231955
+ }
231956
+ const size = (statSync5(aabPath).size / (1024 * 1024)).toFixed(1);
231957
+ note(`Uploading ${relative4(root, aabPath) || aabPath} (${size} MB) to ${packageName}, track ${flags.track}${flags.draft ? " as a draft" : ""} — as ${key.client_email}`);
231958
+ const token = await serviceAccountToken(key);
231959
+ const { versionCode } = await uploadToPlay(token, {
231960
+ packageName,
231961
+ aab: readFileSync21(aabPath),
231962
+ track: flags.track,
231963
+ status: flags.draft ? "draft" : "completed"
231964
+ });
231965
+ const fresh = readAppConfigAt(root);
231966
+ const next = Math.max(fresh.android.versionCode ?? 1, versionCode + 1);
231967
+ if (next !== fresh.android.versionCode) {
231968
+ fresh.android.versionCode = next;
231969
+ writeJson(join23(root, "app.json"), fresh);
231970
+ }
231971
+ success(`Uploaded versionCode ${versionCode} to the ${flags.track} track${flags.draft ? " as a draft — roll it out in Play Console" : ""}.`);
231972
+ note(`android.versionCode is ${next} for the next build.`);
231973
+ };
231974
+ var androidUpload = defineCommand({
231975
+ name: "android",
231976
+ summary: "Upload the .aab to a Google Play track (then bumps android.versionCode)",
231977
+ description: `One edit of the Google Play Developer API — the bundle onto the track as one release, then the commit; Play changes only when every step went through. Authenticated with a Cloud service account's JSON key: --key on first use, then the project .env remembers it (${PLAY_KEY_ENV}; in CI set the variable to the key's path, e.g. a GitLab File variable). The app must already exist in Play Console under app.json android.applicationId (the API cannot create it), the account be invited there with "Release to testing tracks". A successful upload spends the versionCode, so app.json android.versionCode moves past it for the next build.`,
231978
+ flags: androidUploadFlags,
231979
+ examples: ["lecodes app upload android --key ~/keys/play.json", "lecodes app upload android --track internal", "lecodes app upload android --draft"],
231980
+ run: ({ flags }) => uploadAndroid(flags)
231981
+ });
231842
231982
  var ANDROID_SIGNING_ENV = {
231843
231983
  storeFile: "LECODES_ANDROID_KEYSTORE_FILE",
231844
231984
  storePassword: "LECODES_ANDROID_KEYSTORE_PASSWORD",
@@ -231847,7 +231987,7 @@ var ANDROID_SIGNING_ENV = {
231847
231987
  };
231848
231988
  var readProperties = (path) => {
231849
231989
  const out = {};
231850
- for (const raw of readFileSync20(path, "utf8").split(/\r?\n/)) {
231990
+ for (const raw of readFileSync21(path, "utf8").split(/\r?\n/)) {
231851
231991
  const line = raw.trim();
231852
231992
  if (!line || line.startsWith("#") || line.startsWith("!"))
231853
231993
  continue;
@@ -231947,7 +232087,7 @@ var writeSigningConfig = (root, config, storeFile, alias, storePassword, keyPass
231947
232087
  const rel = relative4(root, storeFile);
231948
232088
  if (rel && !rel.startsWith("..") && !rel.includes(":") && !rel.replace(/\\/g, "/").startsWith("android/")) {
231949
232089
  const ignorePath = join23(root, ".gitignore");
231950
- const ignore = existsSync21(ignorePath) ? readFileSync20(ignorePath, "utf8") : "";
232090
+ const ignore = existsSync21(ignorePath) ? readFileSync21(ignorePath, "utf8") : "";
231951
232091
  if (!ignore.split(`
231952
232092
  `).some((l) => l.trim() === "*.keystore")) {
231953
232093
  writeFileSync15(ignorePath, ignore + (!ignore || ignore.endsWith(`
@@ -232185,10 +232325,10 @@ var doctorAndroidChecks = async (root, config, check) => {
232185
232325
  }
232186
232326
  if (config.update) {
232187
232327
  const manifest = join23(androidDir, "lecodes-runtime", "src", "main", "AndroidManifest.xml");
232188
- check(existsSync21(manifest) && readFileSync20(manifest, "utf8").includes("io.letary.lecodes.UPDATE_URL"), "OTA update URL in the runtime manifest", "run `lecodes app sync`", false);
232328
+ check(existsSync21(manifest) && readFileSync21(manifest, "utf8").includes("io.letary.lecodes.UPDATE_URL"), "OTA update URL in the runtime manifest", "run `lecodes app sync`", false);
232189
232329
  const appSrc = join23(androidDir, "app", "src", "main", "java");
232190
232330
  const kt = existsSync21(appSrc) ? readdirSync6(appSrc, { recursive: true }).map(String).filter((f) => f.endsWith(".kt")) : [];
232191
- const sources = kt.map((f) => readFileSync20(join23(appSrc, f), "utf8"));
232331
+ const sources = kt.map((f) => readFileSync21(join23(appSrc, f), "utf8"));
232192
232332
  const boots = sources.some((src) => src.includes("LeCodesShell.boot"));
232193
232333
  check(boots, "App code launches via the updater (LeCodesShell.boot)", 'no app/src/main/java/**/*.kt calls LeCodesShell.boot(engine, activity) — downloaded updates would never run; see "Over-the-air updates" in android/README.md for the AppViewModel snippet', false);
232194
232334
  const waits = sources.some((src) => /LeCodesShell\.boot\(\s*engine\s*,\s*activity\b/.test(src));
@@ -232208,7 +232348,7 @@ var doctorAndroidChecks = async (root, config, check) => {
232208
232348
  }
232209
232349
  check(!!findKeytool(), "keytool", "install a JDK or set JAVA_HOME (`lecodes app keystore …` needs it)", false);
232210
232350
  const appGradle = join23(androidDir, "app", "build.gradle.kts");
232211
- const gradleText = existsSync21(appGradle) ? readFileSync20(appGradle, "utf8") : "";
232351
+ const gradleText = existsSync21(appGradle) ? readFileSync21(appGradle, "utf8") : "";
232212
232352
  check(gradleText.includes("abiFilters"), "abiFilters in app/build.gradle.kts", 'add `ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a") }` to defaultConfig — x86_64 devices crash at launch without it', false);
232213
232353
  check(["isMinifyEnabled", "isShrinkResources"].every((k) => new RegExp(`${k} = (true|false)`).test(gradleText)), "R8 block in app/build.gradle.kts", 'shell predates R8-by-default — see android/README.md "Building"', false);
232214
232354
  if (Object.keys(config.plugins ?? {}).includes("push")) {
@@ -232223,7 +232363,7 @@ import { basename as basename9, dirname as dirname16, join as join26, resolve as
232223
232363
 
232224
232364
  // src/hosts/desktopRenderer.ts
232225
232365
  import { execFileSync as execFileSync3, spawn as spawn2, spawnSync as spawnSync6 } from "node:child_process";
232226
- import { copyFileSync, existsSync as existsSync22, mkdirSync as mkdirSync13, mkdtempSync as mkdtempSync2, readdirSync as readdirSync7, readFileSync as readFileSync21, rmSync as rmSync5, statSync as statSync6, writeFileSync as writeFileSync16, linkSync } from "node:fs";
232366
+ import { copyFileSync, existsSync as existsSync22, mkdirSync as mkdirSync13, mkdtempSync as mkdtempSync2, readdirSync as readdirSync7, readFileSync as readFileSync22, rmSync as rmSync5, statSync as statSync6, writeFileSync as writeFileSync16, linkSync } from "node:fs";
232227
232367
  import { homedir as homedir9, tmpdir as tmpdir2 } from "node:os";
232228
232368
  import { delimiter, join as join24, resolve as resolve11 } from "node:path";
232229
232369
  init_cli();
@@ -232368,7 +232508,7 @@ var installedDesktopVersions = () => {
232368
232508
  };
232369
232509
  var selectedDesktopVersion = () => {
232370
232510
  try {
232371
- const saved = JSON.parse(readFileSync21(currentFile(), "utf8")).version;
232511
+ const saved = JSON.parse(readFileSync22(currentFile(), "utf8")).version;
232372
232512
  if (saved && /^\d+\.\d+\.\d+/.test(saved) && semverNewer(DESKTOP_RENDERER_VERSION, saved) > 0)
232373
232513
  return saved;
232374
232514
  } catch {}
@@ -232493,7 +232633,84 @@ var resolveServerExe = async (renderer = "gl") => {
232493
232633
  }
232494
232634
  return exe;
232495
232635
  };
232496
- var resolveHostExe = (host, renderer) => host === "server" ? resolveServerExe(renderer) : resolveDesktopExe(renderer);
232636
+ var bakeExeName = () => process.platform === "win32" ? "lecodes-bake.exe" : "lecodes-bake";
232637
+ var bakePlatformKey = () => {
232638
+ if (process.platform === "win32")
232639
+ return "windows-x64";
232640
+ if (process.platform === "linux")
232641
+ return "linux-x64";
232642
+ throw new CliError(`The lightmap bake host (lecodes-bake) isn't available on ${process.platform}: the bake traces with Vulkan ray query or OptiX — run it on Windows or Linux.`);
232643
+ };
232644
+ var bakeCacheRoot = () => join24(homedir9(), ".lecodes", "bake");
232645
+ var findBakeExe = (dir) => {
232646
+ for (const entry of readdirSync7(dir, { withFileTypes: true })) {
232647
+ const p = join24(dir, entry.name);
232648
+ if (entry.isFile() && entry.name === bakeExeName())
232649
+ return p;
232650
+ if (entry.isDirectory()) {
232651
+ const found = findBakeExe(p);
232652
+ if (found)
232653
+ return found;
232654
+ }
232655
+ }
232656
+ return null;
232657
+ };
232658
+ var installedBakeVersions = () => {
232659
+ try {
232660
+ return readdirSync7(bakeCacheRoot(), { withFileTypes: true }).filter((e) => e.isDirectory() && e.name.startsWith("bake-")).map((e) => e.name.slice("bake-".length)).filter((v) => findBakeExe(join24(bakeCacheRoot(), `bake-${v}`)) !== null).sort(semverNewer);
232661
+ } catch {
232662
+ return [];
232663
+ }
232664
+ };
232665
+ var resolveBakeExe = async () => {
232666
+ const override = process.env.LECODES_BAKE_EXE;
232667
+ if (override) {
232668
+ if (!existsSync22(override))
232669
+ throw new CliError(`LECODES_BAKE_EXE points at "${override}" which doesn't exist.`);
232670
+ return resolve11(override);
232671
+ }
232672
+ const key = bakePlatformKey();
232673
+ const selected = selectedDesktopVersion();
232674
+ const cachedDir = (version) => join24(bakeCacheRoot(), `bake-${version}`);
232675
+ const cachedSelected = existsSync22(cachedDir(selected)) ? findBakeExe(cachedDir(selected)) : null;
232676
+ if (cachedSelected)
232677
+ return cachedSelected;
232678
+ let pick;
232679
+ try {
232680
+ const { versions } = await fetchReleaseProduct("bake", "lightmap bake host (lecodes-bake)");
232681
+ const exact = versions.find((v) => v.version === selected)?.files[key];
232682
+ pick = exact ? { version: selected, file: exact } : newestForPlatform(versions, key);
232683
+ } catch (e) {
232684
+ const newest = installedBakeVersions()[0];
232685
+ if (newest) {
232686
+ warnErr(`${e instanceof Error ? e.message : String(e)}
232687
+ Using the cached lightmap bake host ${newest}.`);
232688
+ return findBakeExe(cachedDir(newest));
232689
+ }
232690
+ throw new CliError(`${e instanceof Error ? e.message : String(e)}
232691
+ ` + " The lightmap bake runs in lecodes-bake, downloaded from the release manifest on first use. Point LECODES_BAKE_EXE at a local build " + "(hosts/desktop: build.ps1 -Bake → build-bake/lecodes-bake.exe; build.sh with CREATOR_DESKTOP_BAKE=ON → build-bake-linux/lecodes-bake).");
232692
+ }
232693
+ if (!pick) {
232694
+ throw new CliError(`The lightmap bake host (lecodes-bake) isn't published for ${key} yet. Point LECODES_BAKE_EXE at a local build ` + "(hosts/desktop: build.ps1 -Bake → build-bake/lecodes-bake.exe; build.sh with CREATOR_DESKTOP_BAKE=ON → build-bake-linux/lecodes-bake).");
232695
+ }
232696
+ const dir = cachedDir(pick.version);
232697
+ let exe = existsSync22(dir) ? findBakeExe(dir) : null;
232698
+ if (!exe) {
232699
+ if (pick.version !== selected)
232700
+ note(`No lightmap bake host is published for LeCodes Desktop ${selected} — using lecodes-bake ${pick.version}.`);
232701
+ note(`Downloading the lightmap bake host (lecodes-bake ${pick.version} [${key}], ${(pick.file.size / 1024 / 1024).toFixed(1)} MB)…`);
232702
+ await downloadReleaseArchive(pick.file, {
232703
+ cacheRoot: bakeCacheRoot(),
232704
+ dest: dir,
232705
+ validate: (d) => findBakeExe(d) ? null : "The downloaded archive doesn't contain the lecodes-bake binary."
232706
+ });
232707
+ exe = findBakeExe(dir);
232708
+ if (!exe)
232709
+ throw new CliError(`Bake host cache at ${dir} is broken — delete the folder and retry.`);
232710
+ }
232711
+ return exe;
232712
+ };
232713
+ var resolveHostExe = (host, renderer) => host === "bake" ? resolveBakeExe() : host === "server" ? resolveServerExe(renderer) : resolveDesktopExe(renderer);
232497
232714
  var updateDesktop = async () => {
232498
232715
  const manifest = await fetchDesktopManifest();
232499
232716
  const latest = isMac() ? pickMacVersion(manifest.versions, manifest.latest, platformKey2()) : manifest.latest;
@@ -232680,7 +232897,8 @@ var runDesktopDev = async (url, opts = {}) => {
232680
232897
  });
232681
232898
  };
232682
232899
  var runDesktopRender = async (js, resources, opts) => {
232683
- const server2 = opts.host === "server";
232900
+ const server2 = opts.host === "server" || opts.host === "bake";
232901
+ const what = opts.host === "bake" ? "bake" : server2 ? "server" : "desktop";
232684
232902
  const exe = await resolveHostExe(opts.host, opts.renderer);
232685
232903
  const runDir = stageBundle(js, resources);
232686
232904
  try {
@@ -232729,7 +232947,7 @@ var runDesktopRender = async (js, resources, opts) => {
232729
232947
  }, opts.timeoutMs);
232730
232948
  child.on("error", (e) => {
232731
232949
  clearTimeout(killer);
232732
- fail(new CliError(`Can't run the ${server2 ? "server" : "desktop"} host: ${e.message}`));
232950
+ fail(new CliError(`Can't run the ${what} host: ${e.message}`));
232733
232951
  });
232734
232952
  child.on("exit", (code) => {
232735
232953
  clearTimeout(killer);
@@ -232741,7 +232959,7 @@ var runDesktopRender = async (js, resources, opts) => {
232741
232959
  const tail = demangle(chunks.slice(-80).join("")).trimEnd();
232742
232960
  const detail = tail.length > 0 ? `
232743
232961
  ${tail}` : "";
232744
- fail(new CliError(code === 0 ? `The renderer exited cleanly but produced no PNG — did the app open a screen?${detail}` : `The ${server2 ? "server host" : "renderer"} exited with code ${code}.${detail}`));
232962
+ fail(new CliError(code === 0 ? `The renderer exited cleanly but produced no PNG — did the app open a screen?${detail}` : `The ${server2 ? `${what} host` : "renderer"} exited with code ${code}.${detail}`));
232745
232963
  });
232746
232964
  });
232747
232965
  } finally {
@@ -233412,7 +233630,7 @@ var shellSdkVersion = (iosDir) => {
233412
233630
  const path = join27(iosDir, "LeCodesRuntime", "Package.swift");
233413
233631
  if (!existsSync25(path))
233414
233632
  return null;
233415
- const m = /lecodes-ios-sdk\.git",\s*(?:exact|from):\s*"(\d+\.\d+\.\d+)(?:-(?:core|2d|3d))?"/.exec(readFileSync22(path, "utf8"));
233633
+ const m = /lecodes-ios-sdk\.git",\s*(?:exact|from):\s*"(\d+\.\d+\.\d+)(?:-(?:core|2d|3d))?"/.exec(readFileSync23(path, "utf8"));
233416
233634
  return m && VERSION_RE.test(m[1]) ? m[1] : null;
233417
233635
  };
233418
233636
  var IOS_SDK_OPTS = { repo: SDK_REPO, minVersion: SDK_MIN_VERSION, configKey: "ios.sdk" };
@@ -233618,7 +233836,7 @@ var syncIos = async (root, config) => {
233618
233836
  rmSync8(resourcesDir, { recursive: true, force: true });
233619
233837
  mkdirSync16(resourcesDir, { recursive: true });
233620
233838
  const resources = await compileShell(join27(resourcesDir, "app.js"), { remotePatterns });
233621
- const bundleJs = readFileSync22(join27(resourcesDir, "app.js"), "utf8");
233839
+ const bundleJs = readFileSync23(join27(resourcesDir, "app.js"), "utf8");
233622
233840
  const { variant, why } = resolveVariant(requestedVariant, bundleJs);
233623
233841
  log(`${c.green("variant")} ${variant}${requestedVariant === "auto" ? ` (auto: ${why})` : " (forced by ios.variant)"}`);
233624
233842
  const sdkWhy = sdkPin === "latest" ? "latest" : config.ios.sdk ? "pinned by ios.sdk" : "from the shell — set ios.sdk or run `lecodes app update`";
@@ -233684,7 +233902,7 @@ var syncIos = async (root, config) => {
233684
233902
  if (pluginSpecs.length > 0 || existsSync25(lockPath))
233685
233903
  writeJson(lockPath, { plugins: newLock });
233686
233904
  const pbxPath = join27(iosDir, "App.xcodeproj", "project.pbxproj");
233687
- let pbx = readFileSync22(pbxPath, "utf8").replace(/MARKETING_VERSION = [^;]+;/g, `MARKETING_VERSION = ${config.version ?? "1.0.0"};`).replace(/CURRENT_PROJECT_VERSION = [^;]+;/g, `CURRENT_PROJECT_VERSION = ${config.ios.buildNumber ?? 1};`);
233905
+ let pbx = readFileSync23(pbxPath, "utf8").replace(/MARKETING_VERSION = [^;]+;/g, `MARKETING_VERSION = ${config.version ?? "1.0.0"};`).replace(/CURRENT_PROJECT_VERSION = [^;]+;/g, `CURRENT_PROJECT_VERSION = ${config.ios.buildNumber ?? 1};`);
233688
233906
  const mergedEntitlements = {};
233689
233907
  const appLinks = appLinksOf(config);
233690
233908
  if (appLinks.length) {
@@ -233976,8 +234194,8 @@ var doctorIosChecks = async (root, config, check) => {
233976
234194
  const validVariant = requestedVariant === "auto" || SDK_VARIANTS.includes(requestedVariant);
233977
234195
  check(validVariant, `ios.variant "${requestedVariant}"`, `use "auto" or one of ${SDK_VARIANTS.join(", ")}`);
233978
234196
  if (existsSync25(bundle) && validVariant) {
233979
- const { variant } = resolveVariant(requestedVariant, readFileSync22(bundle, "utf8"));
233980
- const pkg = readFileSync22(join27(root, "ios", "LeCodesRuntime", "Package.swift"), "utf8");
234197
+ const { variant } = resolveVariant(requestedVariant, readFileSync23(bundle, "utf8"));
234198
+ const pkg = readFileSync23(join27(root, "ios", "LeCodesRuntime", "Package.swift"), "utf8");
233981
234199
  const tag = /lecodes-ios-sdk\.git",\s*exact:\s*"([^"]+)"/.exec(pkg)?.[1] ?? "";
233982
234200
  const tagVariant = /-(core|2d|3d)$/.exec(tag)?.[1] ?? "full";
233983
234201
  check(tagVariant === variant, `SDK variant ${variant}`, "run `lecodes app sync`");
@@ -233999,25 +234217,25 @@ var doctorIosChecks = async (root, config, check) => {
233999
234217
  const inSync = wanted.every((id) => lock[id]) && Object.keys(lock).every((id) => wanted.includes(id));
234000
234218
  check(wanted.length === 0 || inSync, "Plugins synced", "run `lecodes app sync`");
234001
234219
  const pbxFile = join27(root, "ios", "App.xcodeproj", "project.pbxproj");
234002
- if (existsSync25(pbxFile) && readFileSync22(pbxFile, "utf8").includes("CODE_SIGN_ENTITLEMENTS")) {
234220
+ if (existsSync25(pbxFile) && readFileSync23(pbxFile, "utf8").includes("CODE_SIGN_ENTITLEMENTS")) {
234003
234221
  check(existsSync25(join27(root, "ios", "App", "App.entitlements")), "App/App.entitlements present", "run `lecodes app sync`");
234004
234222
  }
234005
234223
  if (wanted.includes("push")) {
234006
234224
  const runtimeSwift = join27(root, "ios", "LeCodesRuntime", "Sources", "LeCodesRuntime", "LeCodesRuntime.swift");
234007
- const baked = existsSync25(runtimeSwift) && readFileSync22(runtimeSwift, "utf8").includes("bootProjectUuid");
234225
+ const baked = existsSync25(runtimeSwift) && readFileSync23(runtimeSwift, "utf8").includes("bootProjectUuid");
234008
234226
  check(baked, "Push world identity baked", `without it Push.register() rejects "unavailable" — needs a pushed project (uuid), lecodes-ios-sdk ${SDK_IDENTITY_VERSION}+ and \`lecodes app sync\``, false);
234009
234227
  }
234010
234228
  if (config.update) {
234011
- const plist = readFileSync22(join27(root, "ios", "App", "Info.plist"), "utf8");
234229
+ const plist = readFileSync23(join27(root, "ios", "App", "Info.plist"), "utf8");
234012
234230
  check(plist.includes("<key>LeCodesUpdateURL</key>"), "OTA update URL in Info.plist", "run `lecodes app sync`", false);
234013
234231
  const appDir = join27(root, "ios", "App");
234014
- const swift = readdirSync10(appDir).filter((f) => f.endsWith(".swift")).map((f) => readFileSync22(join27(appDir, f), "utf8"));
234232
+ const swift = readdirSync10(appDir).filter((f) => f.endsWith(".swift")).map((f) => readFileSync23(join27(appDir, f), "utf8"));
234015
234233
  const launchesViaUpdater = swift.some((t) => t.includes("LeCodesAppViewController") || t.includes("LeCodesUpdater.activeBundle"));
234016
234234
  check(launchesViaUpdater, "App code launches via the updater", `no App/*.swift subclasses LeCodesAppViewController or calls LeCodesUpdater.activeBundle() — downloaded updates would never run; see "Over-the-air updates" in ios/README.md`, false);
234017
234235
  }
234018
234236
  {
234019
234237
  const vc = join27(root, "ios", "App", "ViewController.swift");
234020
- const is2x = !existsSync25(vc) || readFileSync22(vc, "utf8").includes("LeCodesAppViewController");
234238
+ const is2x = !existsSync25(vc) || readFileSync23(vc, "utf8").includes("LeCodesAppViewController");
234021
234239
  check(is2x, "App/ViewController.swift is the 2.x shell (LeCodesAppViewController)", "a 1.x shell's App/*.swift: replace AppDelegate.swift, SceneDelegate.swift and ViewController.swift with the ones `lecodes app init ios` writes today (your own code goes into ViewController.configure(_:))", false);
234022
234240
  }
234023
234241
  const asc = readJson2(join27(homedir10(), ".lecodes", "asc.json"));
@@ -234202,18 +234420,8 @@ var iosUpload = defineCommand({
234202
234420
  });
234203
234421
  var upload = defineCommand({
234204
234422
  name: "upload",
234205
- summary: "Upload a build to the store: ios (App Store Connect)",
234206
- commands: [
234207
- iosUpload,
234208
- defineCommand({
234209
- name: "android",
234210
- summary: "Play upload (not wired yet)",
234211
- hidden: true,
234212
- run: () => {
234213
- throw new CliError("Play upload isn't wired yet — upload android/app/build/outputs/bundle/release/*.aab in the Play Console (`lecodes app build android` produces it).");
234214
- }
234215
- })
234216
- ]
234423
+ summary: "Upload a build to the store: ios (App Store Connect) | android (Google Play)",
234424
+ commands: [iosUpload, androidUpload]
234217
234425
  });
234218
234426
  var open = defineCommand({
234219
234427
  name: "open",
@@ -234337,7 +234545,7 @@ var resolveCmgenExe = async () => {
234337
234545
  init_peerInstall();
234338
234546
  var assets_default = defineCommand({
234339
234547
  name: "assets",
234340
- summary: "The asset pipeline: convert / probe / doctor / retarget / unpack / scene / sky / terrain-pack …",
234548
+ summary: "The asset pipeline: convert / probe / doctor / unwrap / retarget / unpack / scene / sky / terrain-pack …",
234341
234549
  usage: "<command> [args…]",
234342
234550
  passthrough: true,
234343
234551
  examples: ["lecodes assets convert hero.fbx --clips anims/*.fbx -o hero.glb", "lecodes assets doctor hero.glb --fix --ktx2", "lecodes assets --help"],
@@ -235367,7 +235575,7 @@ var desktop_default = defineCommand({
235367
235575
 
235368
235576
  // src/commands/dev.ts
235369
235577
  init_manifest();
235370
- import { existsSync as existsSync38, mkdirSync as mkdirSync22, readFileSync as readFileSync31, writeFileSync as writeFileSync25 } from "node:fs";
235578
+ import { existsSync as existsSync38, mkdirSync as mkdirSync22, readFileSync as readFileSync32, writeFileSync as writeFileSync25 } from "node:fs";
235371
235579
  import { randomBytes as randomBytes5 } from "node:crypto";
235372
235580
  import { networkInterfaces } from "node:os";
235373
235581
  import { basename as basename15, join as join43 } from "node:path";
@@ -235375,7 +235583,7 @@ import { basename as basename15, join as join43 } from "node:path";
235375
235583
  // src/dev/devServer.ts
235376
235584
  init_localFiles();
235377
235585
  import { createHash as createHash8 } from "node:crypto";
235378
- import { createReadStream as createReadStream2, readFileSync as readFileSync29, statSync as statSync11 } from "node:fs";
235586
+ import { createReadStream as createReadStream2, readFileSync as readFileSync30, statSync as statSync11 } from "node:fs";
235379
235587
  import { join as join39 } from "node:path";
235380
235588
  import { Readable as Readable3 } from "node:stream";
235381
235589
  init_projectCompile();
@@ -235510,7 +235718,7 @@ init_paths();
235510
235718
  // src/web/halves.ts
235511
235719
  init_compile();
235512
235720
  init_cli();
235513
- import { cpSync as cpSync5, existsSync as existsSync34, readFileSync as readFileSync28 } from "node:fs";
235721
+ import { cpSync as cpSync5, existsSync as existsSync34, readFileSync as readFileSync29 } from "node:fs";
235514
235722
  import { join as join38, sep as sep4 } from "node:path";
235515
235723
  var halfModule = (half) => `plugins/${half.folder}/index.js`;
235516
235724
  var asCliError = (e) => new CliError(e instanceof Error ? e.message : String(e));
@@ -235521,7 +235729,7 @@ var projectHalves = (root) => {
235521
235729
  const folder = dir.split(sep4).pop();
235522
235730
  let web;
235523
235731
  try {
235524
- web = webHalfManifestOf(readFileSync28(join38(dir, MANIFEST_FILE), "utf8"), folder, id);
235732
+ web = webHalfManifestOf(readFileSync29(join38(dir, MANIFEST_FILE), "utf8"), folder, id);
235525
235733
  } catch (e) {
235526
235734
  throw asCliError(e);
235527
235735
  }
@@ -235662,7 +235870,7 @@ var startDevServer = async (options) => {
235662
235870
  const liteTable = () => {
235663
235871
  let plugins = {};
235664
235872
  try {
235665
- plugins = JSON.parse(readFileSync29(join39(lite, "plugins.json"), "utf8")).plugins ?? {};
235873
+ plugins = JSON.parse(readFileSync30(join39(lite, "plugins.json"), "utf8")).plugins ?? {};
235666
235874
  } catch {}
235667
235875
  const table = { ...plugins };
235668
235876
  for (const { half, hash } of halves) {
@@ -236153,7 +236361,7 @@ var androidCrashLines = (session) => {
236153
236361
 
236154
236362
  // src/dev/iosDev.ts
236155
236363
  init_cli();
236156
- import { existsSync as existsSync37, readdirSync as readdirSync17, readFileSync as readFileSync30, statSync as statSync13, unlinkSync } from "node:fs";
236364
+ import { existsSync as existsSync37, readdirSync as readdirSync17, readFileSync as readFileSync31, statSync as statSync13, unlinkSync } from "node:fs";
236157
236365
  import { homedir as homedir12 } from "node:os";
236158
236366
  import { join as join42 } from "node:path";
236159
236367
  import { spawnSync as spawnSync10 } from "node:child_process";
@@ -236186,7 +236394,7 @@ var listDevices2 = () => {
236186
236394
  if (!ok || !existsSync37(tmp))
236187
236395
  return [];
236188
236396
  try {
236189
- const json = JSON.parse(readFileSync30(tmp, "utf8"));
236397
+ const json = JSON.parse(readFileSync31(tmp, "utf8"));
236190
236398
  return (json.result?.devices ?? []).map((d) => ({
236191
236399
  udid: d.identifier,
236192
236400
  name: d.deviceProperties?.name ?? d.identifier,
@@ -236233,7 +236441,7 @@ var deviceHasApp = (udid, bundleId) => {
236233
236441
  if (!ok || !existsSync37(tmp))
236234
236442
  return false;
236235
236443
  try {
236236
- const json = JSON.parse(readFileSync30(tmp, "utf8"));
236444
+ const json = JSON.parse(readFileSync31(tmp, "utf8"));
236237
236445
  return (json.result?.apps ?? []).some((a) => a.bundleIdentifier === bundleId);
236238
236446
  } catch {
236239
236447
  return false;
@@ -236273,7 +236481,7 @@ var launchOnIos = (opts) => {
236273
236481
  if (!start.ok)
236274
236482
  throw new CliError(`Could not launch ${bundleId} on ${target.name}: ${start.out}`);
236275
236483
  try {
236276
- const json = JSON.parse(readFileSync30(tmp, "utf8"));
236484
+ const json = JSON.parse(readFileSync31(tmp, "utf8"));
236277
236485
  pid = json.result?.process?.processIdentifier?.toString() ?? null;
236278
236486
  } catch {
236279
236487
  pid = null;
@@ -236345,7 +236553,7 @@ var lanCandidates = () => {
236345
236553
  var loadDevToken = (root) => {
236346
236554
  const path = join43(root, LECODES_DIR, "dev.json");
236347
236555
  try {
236348
- const token = JSON.parse(readFileSync31(path, "utf8")).token;
236556
+ const token = JSON.parse(readFileSync32(path, "utf8")).token;
236349
236557
  if (token && /^[a-z0-9]{8,}$/.test(token))
236350
236558
  return token;
236351
236559
  } catch {}
@@ -236580,7 +236788,7 @@ init_api();
236580
236788
 
236581
236789
  // src/platform/serverDiff.ts
236582
236790
  init_localFiles();
236583
- import { readFileSync as readFileSync32 } from "node:fs";
236791
+ import { readFileSync as readFileSync33 } from "node:fs";
236584
236792
  var isTextAsset = (t) => t === "text" || t === "shader" || t === "scene";
236585
236793
  var computeServerDiff = (project, scan) => {
236586
236794
  const pathById = buildPaths(project.assets);
@@ -236598,7 +236806,7 @@ var computeServerDiff = (project, scan) => {
236598
236806
  const local = localByPath.get(path);
236599
236807
  if (local && !server2) {
236600
236808
  const text = isTextKind(local.kind);
236601
- entries.push({ path, status: "added", binary: !text, serverText: "", localText: text ? readFileSync32(local.absPath, "utf8") : undefined });
236809
+ entries.push({ path, status: "added", binary: !text, serverText: "", localText: text ? readFileSync33(local.absPath, "utf8") : undefined });
236602
236810
  continue;
236603
236811
  }
236604
236812
  if (server2 && !local) {
@@ -236609,12 +236817,12 @@ var computeServerDiff = (project, scan) => {
236609
236817
  if (server2 && local) {
236610
236818
  if (isTextAsset(server2.type)) {
236611
236819
  const serverText = server2.text ?? "";
236612
- const localText = readFileSync32(local.absPath, "utf8");
236820
+ const localText = readFileSync33(local.absPath, "utf8");
236613
236821
  if (serverText !== localText)
236614
236822
  entries.push({ path, status: "modified", binary: false, serverText, localText });
236615
236823
  } else {
236616
236824
  const serverSize = server2.file?.size;
236617
- const localSize = Buffer.byteLength(readFileSync32(local.absPath));
236825
+ const localSize = Buffer.byteLength(readFileSync33(local.absPath));
236618
236826
  if (serverSize !== undefined && serverSize !== localSize) {
236619
236827
  entries.push({ path, status: "modified", binary: true });
236620
236828
  }
@@ -236946,7 +237154,7 @@ var init3 = (dir, forDesktop) => {
236946
237154
  // src/commands/install.ts
236947
237155
  init_cli();
236948
237156
  import { execFileSync as execFileSync7 } from "node:child_process";
236949
- import { appendFileSync as appendFileSync3, chmodSync as chmodSync2, cpSync as cpSync6, existsSync as existsSync40, mkdirSync as mkdirSync24, readFileSync as readFileSync33, realpathSync as realpathSync2, rmSync as rmSync11, symlinkSync as symlinkSync2 } from "node:fs";
237157
+ import { appendFileSync as appendFileSync3, chmodSync as chmodSync2, cpSync as cpSync6, existsSync as existsSync40, mkdirSync as mkdirSync24, readFileSync as readFileSync34, realpathSync as realpathSync2, rmSync as rmSync11, symlinkSync as symlinkSync2 } from "node:fs";
236950
237158
  import { homedir as homedir13 } from "node:os";
236951
237159
  import { basename as basename17, delimiter as delimiter2, dirname as dirname22, join as join45, resolve as resolve21 } from "node:path";
236952
237160
  var isWin = process.platform === "win32";
@@ -237000,7 +237208,7 @@ var appendPathToShellRc = (binDir) => {
237000
237208
  const { file, name } = shellRc();
237001
237209
  const line = `export PATH="${binDir}:$PATH"`;
237002
237210
  try {
237003
- const existing = existsSync40(file) ? readFileSync33(file, "utf8") : "";
237211
+ const existing = existsSync40(file) ? readFileSync34(file, "utf8") : "";
237004
237212
  if (existing.includes(line))
237005
237213
  return name;
237006
237214
  appendFileSync3(file, `${existing && !existing.endsWith(`
@@ -237122,10 +237330,11 @@ var install_default = defineCommand({
237122
237330
  });
237123
237331
 
237124
237332
  // src/commands/lightmap.ts
237125
- import { existsSync as existsSync41, mkdirSync as mkdirSync25, mkdtempSync as mkdtempSync4, readFileSync as readFileSync34, rmSync as rmSync12, statSync as statSync14, writeFileSync as writeFileSync27 } from "node:fs";
237333
+ import { closeSync, existsSync as existsSync41, mkdirSync as mkdirSync25, mkdtempSync as mkdtempSync4, openSync, readFileSync as readFileSync35, readSync, rmSync as rmSync12, statSync as statSync14, writeFileSync as writeFileSync27 } from "node:fs";
237126
237334
  import { tmpdir as tmpdir4 } from "node:os";
237127
237335
  import { basename as basename18, join as join46, relative as relative7, resolve as resolve22 } from "node:path";
237128
237336
  import { deflateSync } from "node:zlib";
237337
+ init_peerInstall();
237129
237338
  init_cli();
237130
237339
  init_shared();
237131
237340
  var range = (name, value, min, max) => {
@@ -237136,7 +237345,7 @@ var range = (name, value, min, max) => {
237136
237345
  var readBake = (file) => {
237137
237346
  if (!existsSync41(file))
237138
237347
  throw new CliError(`No bake at ${file} — run \`lecodes lightmap bake\` first.`);
237139
- const data = JSON.parse(readFileSync34(file, "utf8"));
237348
+ const data = JSON.parse(readFileSync35(file, "utf8"));
237140
237349
  if (data.version !== 4)
237141
237350
  throw new CliError(`${file} is not a creator-bake file (version ${String(data.version)}) — rebake.`);
237142
237351
  return data;
@@ -237152,6 +237361,26 @@ var probeBoxOf = (s) => {
237152
237361
  throw new CliError(`--probe-box wants six numbers x0,y0,z0,x1,y1,z1, got "${s}".`);
237153
237362
  return [Math.min(v[0], v[3]), Math.min(v[1], v[4]), Math.min(v[2], v[5]), Math.max(v[0], v[3]), Math.max(v[1], v[4]), Math.max(v[2], v[5])];
237154
237363
  };
237364
+ var cubePatternsOf = (argv) => {
237365
+ const out = [];
237366
+ for (let i = 0;i < argv.length; i++) {
237367
+ const a = argv[i];
237368
+ if (a === "--")
237369
+ break;
237370
+ if (a.startsWith("--cube="))
237371
+ out.push(a.slice("--cube=".length));
237372
+ else if (a === "--cube" && i + 1 < argv.length && (!argv[i + 1].startsWith("-") || /^-(\d|\.\d)/.test(argv[i + 1])))
237373
+ out.push(argv[++i]);
237374
+ }
237375
+ for (const p of out) {
237376
+ try {
237377
+ new RegExp(p);
237378
+ } catch (e) {
237379
+ throw new CliError(`bad --cube pattern: ${p} (${e instanceof Error ? e.message : String(e)})`);
237380
+ }
237381
+ }
237382
+ return out.filter((p) => p !== "");
237383
+ };
237155
237384
  var placeholderBake = (stem) => JSON.stringify({ version: 4, placeholder: true, size: 4, pages: 0, texel: 0, lightScale: 0, light: [`${stem}-light.ktx2`], aux: [`${stem}-aux.ktx2`], instances: [] }) + `
237156
237385
  `;
237157
237386
  var ktx2Placeholder = (vkFormat) => {
@@ -237197,13 +237426,74 @@ var ensurePlaceholders = (outDir, stem) => {
237197
237426
  const grid = join46(outDir, `${stem}.lgrid`);
237198
237427
  if (missing(grid))
237199
237428
  writeFileSync27(grid, "NONE");
237429
+ const uv1 = join46(outDir, `${stem}-uv1.bin`);
237430
+ if (missing(uv1))
237431
+ writeFileSync27(uv1, "NONE");
237432
+ };
237433
+ var readGlbJson = (file) => {
237434
+ let fd;
237435
+ try {
237436
+ fd = openSync(file, "r");
237437
+ const head = Buffer.alloc(20);
237438
+ if (readSync(fd, head, 0, 20, 0) !== 20)
237439
+ return null;
237440
+ if (head.readUInt32LE(0) !== 1179937895 || head.readUInt32LE(16) !== 1313821514)
237441
+ return null;
237442
+ const json = Buffer.alloc(head.readUInt32LE(12));
237443
+ if (readSync(fd, json, 0, json.length, 20) !== json.length)
237444
+ return null;
237445
+ return JSON.parse(json.toString("utf8"));
237446
+ } catch {
237447
+ return null;
237448
+ } finally {
237449
+ if (fd !== undefined)
237450
+ closeSync(fd);
237451
+ }
237452
+ };
237453
+ var lacksLightmapUv = (g) => {
237454
+ const skinned = new Set((g.nodes ?? []).filter((n) => n.skin !== undefined && n.mesh !== undefined).map((n) => n.mesh));
237455
+ const count = (accessor) => accessor === undefined ? 0 : g.accessors?.[accessor]?.count ?? 0;
237456
+ return (g.meshes ?? []).some((mesh, i) => !skinned.has(i) && (mesh.primitives ?? []).some((p) => {
237457
+ const pos = count(p.attributes?.POSITION);
237458
+ return (p.mode ?? 4) === 4 && pos > 0 && (p.indices === undefined ? pos : count(p.indices)) >= 3 && p.attributes?.TEXCOORD_1 === undefined;
237459
+ }));
237460
+ };
237461
+ var referencedGlbs = (js, resources) => {
237462
+ const wanted = new Set([...js.matchAll(/(?:fetchLocal|_creatorFetch\.local)\("([^"]+)"\)/g)].map((m) => m[1]));
237463
+ return [...new Set(resources.filter((r) => !r.remote && !r.shader && r.absPath && /\.glb$/i.test(r.name) && wanted.has(r.name)).map((r) => r.absPath))];
237464
+ };
237465
+ var unwrapReferenced = async (js, resources, root, texel) => {
237466
+ const glbs = referencedGlbs(js, resources);
237467
+ const raw = glbs.filter((f) => {
237468
+ const g = readGlbJson(f);
237469
+ return g !== null && lacksLightmapUv(g);
237470
+ });
237471
+ if (raw.length === 0) {
237472
+ note(`Lightmap uvs: the ${glbs.length} GLB(s) the app references carry them`);
237473
+ return;
237474
+ }
237475
+ note(`Unwrapping ${raw.length} of the ${glbs.length} GLB(s) the app references — no lightmap uvs; written in place (--no-unwrap skips)…`);
237476
+ const assets = await loadPeer("lecodes-assets", "cli", { for: "lecodes lightmap bake (lightmap uvs)" });
237477
+ const t0 = performance.now();
237478
+ const rel = (f) => relative7(root, f).replace(/\\/g, "/");
237479
+ const reports = await assets.unwrapGlbFiles(raw, {
237480
+ texel,
237481
+ onFile: (r) => {
237482
+ if (r.status === "failed")
237483
+ warnErr(`${rel(r.input)}: ${r.summary} — it bakes without lightmap uvs`);
237484
+ else
237485
+ logErr(c.dim(` ${r.status === "unwrapped" ? "unwrapped " : ""}${rel(r.input)}: ${r.summary}`));
237486
+ }
237487
+ });
237488
+ const count = (s) => reports.filter((r) => r.status === s).length;
237489
+ note(`Lightmap uvs: ${count("unwrapped")} file(s) unwrapped${count("skipped") ? `, ${count("skipped")} had nothing to unwrap` : ""}${count("failed") ? `, ${count("failed")} failed` : ""} in ${((performance.now() - t0) / 1000).toFixed(1)} s`);
237200
237490
  };
237201
237491
  var bake = defineCommand({
237202
237492
  name: "bake",
237203
237493
  summary: "Bake the sun, the sky and the lamps into a lux atlas for the level's statics",
237204
- description: `Runs the app in the desktop host in bake mode: the statics are what the app registers with Lightmap.add (a scene file does it for every node no Physics aspect moves), the bake fires at its Lightmap.load call. Writes <out>/<name>.bake + <name>-light[_n].ktx2 (irradiance, BC6H) + <name>-aux[_n].ktx2 (sky visibility, the directional share and direction of the light) + <name>-probes.ktx2 (the reflection probes: a cubemap of the level's radiance per probe, prefiltered per roughness; --probe-spacing 0 = none). Needs a desktop host built with the CUDA toolkit and an NVIDIA card (LECODES_DESKTOP_EXE for a local build).
237494
+ description: `Runs the app in bake mode in lecodes-bake — the windowless host with the bake built in (no physics, 2D, audio, net or navmesh), downloaded on first use like the desktop host; LECODES_BAKE_EXE points at a local build (hosts/desktop build.ps1 -Bake). The statics are what the app registers with Lightmap.add (a scene file does it for every node no Physics aspect moves), the bake fires at its Lightmap.load call. Writes <out>/<name>.bake + <name>-light[_n].ktx2 (irradiance, BC6H) + <name>-aux[_n].ktx2 (sky visibility, the directional share and direction of the light) + <name>-probes.ktx2 (the reflection probes: a cubemap of the level's radiance per probe, prefiltered per roughness; --probe-spacing 0 = none). Needs a GPU with hardware ray tracing (--tracer).
237205
237495
 
237206
- Props need lightmap UVs first: lecodes assets doctor assets/props/*.glb --lightmap-uv`,
237496
+ Lightmap UVs: the GLBs the app references that have none (TEXCOORD_1) are unwrapped first, IN PLACE — the same as lecodes assets unwrap, run on just those files; a file that has them is left alone, --no-unwrap skips the step. A whole level in one GLB that wants --groups / --zone is unwrapped by hand first (lecodes assets unwrap --help).`,
237207
237497
  flags: {
237208
237498
  out: str("where the atlas lands", { value: "<dir>", default: "assets/lightmap" }),
237209
237499
  name: str("file stem — one per baked level", { value: "<stem>", default: "lightmap" }),
@@ -237219,7 +237509,9 @@ Props need lightmap UVs first: lecodes assets doctor assets/props/*.glb --lightm
237219
237509
  bounces: num("path vertices at most (Russian roulette from the third); 0 = direct light only", { default: 3 }),
237220
237510
  "patch-samples": num("next-event samples per texel over the BRIGHT PATCHES of a first direct pass (sunlit floors, the ceiling around a lamp) — what keeps the bounce in dark rooms from arriving as impulses (every path vertex takes one more); 0 = the cosine paths alone", { default: 64 }),
237221
237511
  split: bool('also write <name>-direct[_n].ktx2 / <name>-indirect[_n].ktx2 — the pages the "direct" / "indirect" views bind (list them as env.lightmap.direct / .indirect)'),
237222
- denoise: str("OptiX's AI denoiser over: indirect = the bounce alone (the range 2026-09-16: bounce noise 57 % -> 23 % of the mean, per-instance means unchanged), no-sun = + the sky and the lamps (the sky through a small opening: 0..3 hits of 256 rays, a texel is one of four values), all = + the sun (its shadow edges too: a sunlit pillar's mean moved 6 %), none. School, two seeds: no-sun took the sky 1.8 % -> 0.5 %, the lamps 43 % -> 21 %", { value: "<none|indirect|no-sun|all>", default: "no-sun" }),
237512
+ denoise: str("the AI denoiser (--denoiser) over: indirect = the bounce alone (the range 2026-09-16: bounce noise 57 % -> 23 % of the mean, per-instance means unchanged), no-sun = + the sky and the lamps (the sky through a small opening: 0..3 hits of 256 rays, a texel is one of four values), all = + the sun (its shadow edges too: a sunlit pillar's mean moved 6 %), none. School, two seeds: no-sun took the sky 1.8 % -> 0.5 %, the lamps 43 % -> 21 %", { value: "<none|indirect|no-sun|all>", default: "no-sun" }),
237513
+ tracer: str("which GPU API traces: auto = OptiX where an NVIDIA driver with OptiX opens, else Vulkan ray query — any card with hardware ray tracing (RTX, Radeon RX 6000+, Arc) with its driver alone; optix / vulkan by name (the bake fails when it cannot open)", { value: "<auto|optix|vulkan>", default: "auto" }),
237514
+ denoiser: str("which AI denoiser --denoise runs: auto = OptiX's on an NVIDIA card, else Intel Open Image Denoise (its CUDA / HIP / CPU device, the DLLs next to the host); optix / oidn by name. Independent of --tracer; one that cannot open is a warning and the pages stay as traced", { value: "<auto|optix|oidn>", default: "auto" }),
237223
237515
  "light-radius": num("lamps: the emitter's radius (m) — soft shadows, no 1/d² singularity", { default: 0.05 }),
237224
237516
  "sun-angle": num("sun angular diameter (deg)", { default: 0.53 }),
237225
237517
  bias: num("ray start offset from the surface (m)", { default: 0.01 }),
@@ -237231,6 +237523,9 @@ Props need lightmap UVs first: lecodes assets doctor assets/props/*.glb --lightm
237231
237523
  "probe-indoor": bool("keep only the probes under a roof — those whose open share of the sphere (rays from the point that meet nothing) is at most 15 %, holes in the geometry or not: outdoor statics reflect the sky through their baked sky visibility instead — an environment map without the ground, but no outdoor probe can reach indoors and the atlas shrinks"),
237232
237524
  "probe-layout": str("rooms = every indoor point of the grid slides to the middle of its room and the points of one room merge: one probe per room, far from the walls, its box the room (stations every ~8 m along a room over 12 m), outdoor points stay on the grid; grid = everything stays on the grid. Either way the pages carry which probes each block of texels sees (rays at bake time)", { value: "<rooms|grid>", default: "rooms" }),
237233
237525
  "min-texels": num("instances smaller than this many ATLAS TEXELS along every axis (x --texel: 12 at 5 cm = 0.6 m) get no place in the atlas: they still cast their baked shadow and bounce, their own light is baked where they stand (six sides, no atlas texels) — cartridges, boxes, pipe elbows, a fire extinguisher: a baked shadow does not read on them and their tight charts are where the seams are. 0 = everything bakes; --dry-run lists what falls under it", { default: 12 }),
237526
+ "thin-texels": num("ROUND AND NARROW objects leave the atlas like --min-texels: an instance none of whose lightmap charts is wider than this many texels across its short side (at --texel) — a railing's tubes, a cable, a pipe run, blinds, a gun barrel. A strip of 1-3 texels around a tube cannot hold the light's change around it and its uv seam shows as a step (the range's railing: two texels 5 mm apart on the seam, one lit, one shadowed); the ambient cube baked where it stands is smooth. A flat object has a wide chart somewhere and stays. 0 = off; --dry-run lists what falls under it", { default: 4 }),
237527
+ cube: str(`BAKE AS CUBES BY NAME: a regular expression (ECMAScript, case-sensitive) searched in every record's key — <node path>@<ordinal>, one record per mesh of a model, so SM_pipe takes every pipe node and @1$ the second mesh of each. A record that matches leaves the atlas like a --min-texels / --thin-texels one, whatever its size or charts: an occluder whose own light is baked where it stands as ambient cubes (in slabs along a long one, --object-slab) — a pipe run, a fence, a prop whose lightmap seams show. Repeat the flag (--cube SM_pipe --cube SM_fence) or join with | in one (--cube "SM_pipe|SM_fence", quoted for the shell); a pattern that starts with - goes as --cube=<regex>. The scene's env.lightmap.cube adds its own patterns, a node's lightmap: 'cube' marks one node by hand; --dry-run lists what matches`, { value: "<regex>" }),
237528
+ "object-slab": num("A LONG object the atlas left out (--min-texels, --thin-texels, --cube) takes its light in SLABS of this many metres along the longest axis of its bounds — round(length / this) ambient cubes, 16 at most — a 6 m pipe run, a railing along a wall: one end under a lamp, the other in the dark, where one cube gave all of it the mean. The shader blends the two slabs around each pixel along that axis. 0 = one cube an object", { value: "<m>", default: 1 }),
237234
237529
  "emissive-nits": num("EMISSIVE SURFACES LIGHT THE LEVEL: cd/m² of emission 1.0 (emissive factor x strength x map) — every lit region of an emissive surface (a panel drawn in a ceiling's texture, a screen, a sign) bakes as a rectangle lamp. Default: what the level's camera makes of 1.0, 1.2 x 2^EV100, so a panel lights as brightly as it looks. 0 = emissive surfaces light nothing"),
237235
237530
  "detail-texels": num("A SMALL SURFACE GETS A FINER TEXEL: an instance whose surface is under this many atlas texels (area / texel²) is laid out denser by sqrt(this / its texels), 3x at most — a pipe of 1 m is ~300 texels at 5 cm, its charts a few texels wide and every chart edge a step; at 2000 it bakes at ~2 cm. 0 = off", { default: 2000 }),
237236
237531
  volume: num("THE LIGHT GRID for movers: the cell size in metres (0.75 of it tall) — per cell in free space the baked ambient light (sky, lamps, every bounce; not the direct sun, which stays real-time on movers), links between cells a ray connects so nothing blends through a wall; written as <name>.lgrid, list it as env.lightmap.volume. 0 = none", { default: 1 }),
@@ -237239,16 +237534,18 @@ Props need lightmap UVs first: lecodes assets doctor assets/props/*.glb --lightm
237239
237534
  "dry-run": bool("only the atlas LAYOUT: pages, texel size, the largest rects — no tracing, nothing written (seconds, not minutes; the host still loads the level)"),
237240
237535
  frames: num("host frames to run before giving up — load headroom", { default: 900 }),
237241
237536
  timeout: num("host time cap (ms; default: none)"),
237242
- width: num("host window width (default: app.json desktop.window, else 1280)"),
237243
- height: num("host window height (default: app.json desktop.window, else 720)"),
237244
- renderer: str("desktop host + shader variant (default: app.json desktop.renderer, else gl)", { value: "<gl|vulkan>" }),
237245
- logs: bool("forward the host's output")
237537
+ width: num("the bake host's virtual viewport width — it has no window; nothing in the bake depends on it (default: app.json desktop.window, else 1280)"),
237538
+ height: num("the bake host's virtual viewport height (default: app.json desktop.window, else 720)"),
237539
+ renderer: str("ignored: the bake host is one binary (lecodes-bake, GL shaders on Filament's NOOP backend); kept so older scripts still parse", { value: "<gl|vulkan>" }),
237540
+ logs: bool("forward the host's output"),
237541
+ "no-unwrap": bool("leave the referenced GLBs as they are: no lightmap unwrap of those without TEXCOORD_1 (they bake without lightmap uvs)")
237246
237542
  },
237247
237543
  examples: ["lecodes lightmap bake", "lecodes lightmap bake --entry school.ts --name school --texel 0.04 --pages 4 --debug-dir .lightmap/school --logs"],
237248
- run: async ({ flags }) => {
237544
+ run: async ({ flags, argv }) => {
237249
237545
  const { root } = localProject();
237250
237546
  const outDir = resolve22(root, flags.out);
237251
237547
  const stem = flags.name;
237548
+ const cube = cubePatternsOf(argv);
237252
237549
  if (!/^[A-Za-z0-9_-]+$/.test(stem))
237253
237550
  throw new CliError(`--name wants a plain file stem (letters, digits, - and _), got "${stem}".`);
237254
237551
  const entryFile = flags.entry;
@@ -237256,6 +237553,12 @@ Props need lightmap UVs first: lecodes assets doctor assets/props/*.glb --lightm
237256
237553
  throw new CliError(`No ${entryFile} in ${root} — pass --entry <file> (the file that builds the scene and calls Lightmap.load).`);
237257
237554
  }
237258
237555
  const debugDir = flags["debug-dir"] ? resolve22(root, flags["debug-dir"]) : "";
237556
+ if (!["none", "indirect", "no-sun", "all"].includes(flags.denoise))
237557
+ throw new CliError(`--denoise wants none, indirect, no-sun or all, got "${flags.denoise}".`);
237558
+ if (!["auto", "optix", "vulkan"].includes(flags.tracer))
237559
+ throw new CliError(`--tracer wants auto, optix or vulkan, got "${flags.tracer}".`);
237560
+ if (!["auto", "optix", "oidn"].includes(flags.denoiser))
237561
+ throw new CliError(`--denoiser wants auto, optix or oidn, got "${flags.denoiser}".`);
237259
237562
  const cfg = {
237260
237563
  outDir: outDir.replace(/\\/g, "/"),
237261
237564
  stem,
@@ -237274,6 +237577,8 @@ Props need lightmap UVs first: lecodes assets doctor assets/props/*.glb --lightm
237274
237577
  patchSamples: range("patch-samples", flags["patch-samples"], 0, 1024),
237275
237578
  split: !!flags.split,
237276
237579
  denoise: flags.denoise,
237580
+ tracer: flags.tracer,
237581
+ denoiser: flags.denoiser,
237277
237582
  filter: range("filter", flags.filter, 0, 4),
237278
237583
  probeSpacing: range("probe-spacing", flags["probe-spacing"], 0, 100),
237279
237584
  probeSize: range("probe-size", flags["probe-size"], 16, 128),
@@ -237281,6 +237586,9 @@ Props need lightmap UVs first: lecodes assets doctor assets/props/*.glb --lightm
237281
237586
  probeIndoor: !!flags["probe-indoor"],
237282
237587
  probeLayout: probeLayoutOf(flags["probe-layout"]),
237283
237588
  minTexels: range("min-texels", flags["min-texels"], 0, 1e4),
237589
+ thinTexels: range("thin-texels", flags["thin-texels"], 0, 64),
237590
+ objectSlab: range("object-slab", flags["object-slab"], 0, 100),
237591
+ ...cube.length ? { cube } : {},
237284
237592
  volumeSpacing: range("volume", flags.volume, 0, 100),
237285
237593
  detailTexels: range("detail-texels", flags["detail-texels"], 0, 1e7),
237286
237594
  ...flags["emissive-nits"] !== undefined ? { emissiveNits: range("emissive-nits", flags["emissive-nits"], 0, 1e7) } : {},
@@ -237300,13 +237608,15 @@ import ${JSON.stringify(importPath)}
237300
237608
  ensurePlaceholders(outDir, stem);
237301
237609
  if (debugDir)
237302
237610
  mkdirSync25(debugDir, { recursive: true });
237303
- const desktopRenderer = resolveDesktopRenderer(root, flags.renderer);
237611
+ const desktopRenderer = "gl";
237304
237612
  const { js, resources } = await buildBundle(bundleOptions(flags), {
237305
237613
  localAssets: true,
237306
237614
  desktopRenderer,
237307
237615
  extraEntries: [{ path: entryPath, type: "text", text: entryText }],
237308
237616
  entryOverride: entryPath
237309
237617
  });
237618
+ if (!flags["no-unwrap"])
237619
+ await unwrapReferenced(js, resources, root, cfg.texel);
237310
237620
  const scratch = mkdtempSync4(join46(tmpdir4(), "lecodes-lightmap-"));
237311
237621
  const outPng = join46(scratch, "frame.png");
237312
237622
  const bounce = cfg.bounceRays > 0 && cfg.bounces > 0 ? `, ${cfg.bounceRays} bounce paths of ${cfg.bounces}${cfg.patchSamples > 0 ? ` + ${cfg.patchSamples} patch samples` : ""}${cfg.denoise !== "none" ? `, denoised (${cfg.denoise})` : ", raw"}` : ", no bounce";
@@ -237317,6 +237627,7 @@ import ${JSON.stringify(importPath)}
237317
237627
  try {
237318
237628
  const timeoutMs = flags.timeout || 24 * 3600 * 1000;
237319
237629
  const res = await runDesktopRender(js, resources, {
237630
+ host: "bake",
237320
237631
  renderer: desktopRenderer,
237321
237632
  width,
237322
237633
  height,
@@ -237346,7 +237657,7 @@ import ${JSON.stringify(importPath)}
237346
237657
  }
237347
237658
  if (flags["dry-run"] && /\[lightmap\] done/.test(output)) {
237348
237659
  for (const l of lines)
237349
- if (/atlas \d+ x|larger rect|largest rect|do not fit|charts touch/.test(l))
237660
+ if (/atlas \d+ x|packed per file|largest rect|do not fit|fill:|--cube|lightmap: 'cube'/.test(l))
237350
237661
  logErr(l.replace(/^\[lightmap\] /, " "));
237351
237662
  logErr(c.green("✓ layout only — nothing written"));
237352
237663
  return;
@@ -237359,15 +237670,16 @@ import ${JSON.stringify(importPath)}
237359
237670
  const splitDef = data.direct && data.indirect ? `, direct: ${assets(data.direct)}, indirect: ${assets(data.indirect)}` : "";
237360
237671
  const probeFiles = data.probes ? ` + ${data.probes.file} (${data.probes.count} probe(s) of ${data.probes.size}² at ${data.probes.spacing} m)` : "";
237361
237672
  const gridFile = data.volume ? ` + ${data.volume.file} (light grid ${data.volume.n.join(" x ")}, ${data.volume.valid} cell(s) in free space)` : "";
237362
- const probeDef = (data.probes ? `, probes: asset('<rel>/${rel(data.probes.file)}')` : "") + (data.volume ? `, volume: asset('<rel>/${rel(data.volume.file)}')` : "");
237363
- logErr(c.green(`✓ baked → ${relative7(root, outDir) || "."}/${stem}.bake + ${data.light.join(", ")} + ${data.aux.join(", ")}${splitFiles}${probeFiles}${gridFile}`));
237673
+ const probeDef = (data.probes ? `, probes: asset('<rel>/${rel(data.probes.file)}')` : "") + (data.volume ? `, volume: asset('<rel>/${rel(data.volume.file)}')` : "") + (data.uv1 ? `, uv1: asset('<rel>/${rel(data.uv1)}')` : "");
237674
+ const uv1File = data.uv1 ? ` + ${data.uv1} (the packed lightmap uv1 — list it as env.lightmap.uv1, the rects map it)` : "";
237675
+ logErr(c.green(`✓ baked → ${relative7(root, outDir) || "."}/${stem}.bake + ${data.light.join(", ")} + ${data.aux.join(", ")}${splitFiles}${probeFiles}${gridFile}${uv1File}`));
237364
237676
  logErr(c.dim(` scene file: env.lightmap: { data: asset('<rel>/${rel(`${stem}.bake`)}'), light: ${assets(data.light)}, aux: ${assets(data.aux)}${splitDef}${probeDef} } — asset() paths are relative to the scene file`));
237365
237677
  if (debugDir)
237366
237678
  logErr(c.dim(` debug layers: ${relative7(root, debugDir)} (lecodes lightmap inspect --bake ${relative7(root, join46(outDir, `${stem}.bake`)).replace(/\\/g, "/")} --debug-dir ${relative7(root, debugDir).replace(/\\/g, "/")})`));
237367
237679
  return;
237368
237680
  }
237369
- if (lines.some((l) => /no bake module/.test(l))) {
237370
- throw new CliError("This desktop host has no bake module. Build hosts/desktop with the CUDA toolkit installed and point LECODES_DESKTOP_EXE at build/lecodes-desktop.exe.");
237681
+ if (/\[lightmap\] error: this host has no bake module/.test(output)) {
237682
+ throw new CliError("This host has no bake module — the bake runs in lecodes-bake. Point LECODES_BAKE_EXE at it (hosts/desktop: build.ps1 -Bake → build-bake/lecodes-bake.exe), or unset it to use the published one.");
237371
237683
  }
237372
237684
  if (lines.some((l) => /error/.test(l)))
237373
237685
  throw new CliError("The bake failed — see the lines above.");
@@ -237519,7 +237831,7 @@ var diff = defineCommand({
237519
237831
  }
237520
237832
  });
237521
237833
  var readPfm = (file) => {
237522
- const buf = readFileSync34(file);
237834
+ const buf = readFileSync35(file);
237523
237835
  let o = 0;
237524
237836
  const token = () => {
237525
237837
  while (o < buf.length && /\s/.test(String.fromCharCode(buf[o])))
@@ -237785,7 +238097,7 @@ init_types4();
237785
238097
  // src/commands/push.ts
237786
238098
  init_cli();
237787
238099
  init_api();
237788
- import { readFileSync as readFileSync36 } from "node:fs";
238100
+ import { readFileSync as readFileSync37 } from "node:fs";
237789
238101
  import { basename as basename19 } from "node:path";
237790
238102
  init_localFiles();
237791
238103
  init_manifest();
@@ -237794,7 +238106,7 @@ init_manifest();
237794
238106
  init_cli();
237795
238107
  init_api();
237796
238108
  init_localFiles();
237797
- import { existsSync as existsSync42, readFileSync as readFileSync35 } from "node:fs";
238109
+ import { existsSync as existsSync42, readFileSync as readFileSync36 } from "node:fs";
237798
238110
  import { resolve as resolve23 } from "node:path";
237799
238111
  init_shared();
237800
238112
  var compareVersions = (a, b) => {
@@ -237881,11 +238193,11 @@ var upload2 = defineCommand({
237881
238193
  const path = resolve23(file);
237882
238194
  if (!existsSync42(path))
237883
238195
  throw new CliError(`No such file: ${file}`);
237884
- const code = readFileSync35(path, "utf8");
238196
+ const code = readFileSync36(path, "utf8");
237885
238197
  const sdk = /^\/\/ sdk: (\S+)/m.exec(code)?.[1];
237886
238198
  if (!sdk)
237887
238199
  throw new CliError(`${file} is not a LeCodes bundle: no \`// sdk:\` header line. \`lecodes compile --remote-assets -o app.js\` writes one.`);
237888
- const map = existsSync42(`${path}.map`) ? readFileSync35(`${path}.map`, "utf8") : null;
238200
+ const map = existsSync42(`${path}.map`) ? readFileSync36(`${path}.map`, "utf8") : null;
237889
238201
  const { root, manifest, apiUrl, token } = linkedProject();
237890
238202
  if (hasChanges(diffLocal(manifest, scanProject(root, manifest))))
237891
238203
  await runPush({ message: flags.message || "Release" });
@@ -237990,9 +238302,9 @@ var runPush = async (opts) => {
237990
238302
  if (source)
237991
238303
  entry.assetId = source.id;
237992
238304
  if (isTextKind(kind))
237993
- entry.text = readFileSync36(file.absPath, "utf8");
238305
+ entry.text = readFileSync37(file.absPath, "utf8");
237994
238306
  else if (!source || source.sha !== file.sha) {
237995
- const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename19(file.path), readFileSync36(file.absPath));
238307
+ const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename19(file.path), readFileSync37(file.absPath));
237996
238308
  entry.blobFileId = blob.id;
237997
238309
  uploaded++;
237998
238310
  }
@@ -238192,7 +238504,7 @@ var login_default = defineCommand({
238192
238504
  });
238193
238505
 
238194
238506
  // src/commands/navmesh.ts
238195
- import { existsSync as existsSync44, mkdirSync as mkdirSync26, mkdtempSync as mkdtempSync5, readdirSync as readdirSync18, readFileSync as readFileSync37, rmSync as rmSync13, statSync as statSync15, unlinkSync as unlinkSync2, writeFileSync as writeFileSync28 } from "node:fs";
238507
+ import { existsSync as existsSync44, mkdirSync as mkdirSync26, mkdtempSync as mkdtempSync5, readdirSync as readdirSync18, readFileSync as readFileSync38, rmSync as rmSync13, statSync as statSync15, unlinkSync as unlinkSync2, writeFileSync as writeFileSync28 } from "node:fs";
238196
238508
  import { tmpdir as tmpdir5 } from "node:os";
238197
238509
  import { basename as basename21, join as join48, relative as relative8, resolve as resolve25 } from "node:path";
238198
238510
  init_cli();
@@ -239003,7 +239315,7 @@ var guessName = (root) => {
239003
239315
  if (!f.endsWith(".scene.ts"))
239004
239316
  continue;
239005
239317
  try {
239006
- if (/\bnavmesh\s*:/.test(readFileSync37(join48(dir, f), "utf8")))
239318
+ if (/\bnavmesh\s*:/.test(readFileSync38(join48(dir, f), "utf8")))
239007
239319
  return f.slice(0, -".scene.ts".length);
239008
239320
  } catch {}
239009
239321
  }
@@ -239153,7 +239465,7 @@ import ${JSON.stringify(importPath)}
239153
239465
  const geoPath = join48(outDir, `${name}.navgeo`);
239154
239466
  if (!existsSync44(geoPath))
239155
239467
  throw new CliError(`The host reported a dump but ${geoPath} is missing.`);
239156
- const geo = readNgeo(new Uint8Array(readFileSync37(geoPath)));
239468
+ const geo = readNgeo(new Uint8Array(readFileSync38(geoPath)));
239157
239469
  const params = {
239158
239470
  agentRadius: range2("radius", flags.radius, config.agent.radius, 0.05, 10),
239159
239471
  agentHeight: range2("height", flags.height, config.agent.height, 0.1, 20),
@@ -239227,7 +239539,7 @@ init_cli();
239227
239539
 
239228
239540
  // src/plugin/generate.ts
239229
239541
  init_cli();
239230
- import { existsSync as existsSync45, mkdirSync as mkdirSync27, readFileSync as readFileSync38, readdirSync as readdirSync19, statSync as statSync16, writeFileSync as writeFileSync29 } from "node:fs";
239542
+ import { existsSync as existsSync45, mkdirSync as mkdirSync27, readFileSync as readFileSync39, readdirSync as readdirSync19, statSync as statSync16, writeFileSync as writeFileSync29 } from "node:fs";
239231
239543
  import { createRequire as createRequire7 } from "node:module";
239232
239544
  import { basename as basename22, join as join49, relative as relative9, resolve as resolve26, sep as sep5 } from "node:path";
239233
239545
 
@@ -242101,7 +242413,7 @@ const wire = {
242101
242413
  var shown = (from, to) => relative9(from, to).split(sep5).join("/");
242102
242414
  var implementsOf = (dir) => {
242103
242415
  try {
242104
- const raw = JSON.parse(readFileSync38(join49(dir, MANIFEST_FILE), "utf8").replace(/^/, ""));
242416
+ const raw = JSON.parse(readFileSync39(join49(dir, MANIFEST_FILE), "utf8").replace(/^/, ""));
242105
242417
  return typeof raw.implements === "string" ? raw.implements : null;
242106
242418
  } catch {
242107
242419
  return null;
@@ -242132,7 +242444,7 @@ var generatePlugin = (ts, plugin) => {
242132
242444
  const manifest = readManifestIfAny(plugin.dir, plugin.id);
242133
242445
  const implemented = manifest?.implements;
242134
242446
  const file = implemented ? firstPartyContractFile(implemented) : join49(plugin.dir, CONTRACT_FILE);
242135
- const contract = readContract(ts, file, implemented ? firstPartyContractText(file) : readFileSync38(file, "utf8"));
242447
+ const contract = readContract(ts, file, implemented ? firstPartyContractText(file) : readFileSync39(file, "utf8"));
242136
242448
  checkPermissions(plugin, contract, manifest);
242137
242449
  const id = implemented ?? plugin.id;
242138
242450
  const handWritten = existsSync45(join49(plugin.dir, "sdk", `${id}.ts`));
@@ -242172,7 +242484,7 @@ var checkPermissions = (plugin, contract, manifest) => {
242172
242484
  if (kinds.length === 0)
242173
242485
  return;
242174
242486
  const androidManifest = join49(plugin.dir, "android", "src", "main", "AndroidManifest.xml");
242175
- const androidXml = manifest.android ? existsSync45(androidManifest) ? readFileSync38(androidManifest, "utf8") : "" : null;
242487
+ const androidXml = manifest.android ? existsSync45(androidManifest) ? readFileSync39(androidManifest, "utf8") : "" : null;
242176
242488
  const missing = missingDeclarations(kinds, manifest.ios ? manifest.ios.infoPlist ?? {} : null, androidXml === null ? null : usesPermissions(androidXml), androidXml === null ? null : usesFeatures(androidXml));
242177
242489
  const problems = [];
242178
242490
  if (missing.infoPlist.length)
@@ -242196,7 +242508,7 @@ var hostImports = (web) => {
242196
242508
  }
242197
242509
  if (!/\.(ts|tsx|js|mjs)$/.test(entry))
242198
242510
  continue;
242199
- readFileSync38(path, "utf8").split(`
242511
+ readFileSync39(path, "utf8").split(`
242200
242512
  `).forEach((line, i) => {
242201
242513
  const m = /(?:from\s+|import\s*\(\s*|import\s+)["'](lecodes-[^"']*)["']/.exec(line);
242202
242514
  if (m && !/^\s*\/\//.test(line))
@@ -242233,7 +242545,7 @@ var repoSdkPath = (root) => {
242233
242545
  if (!existsSync45(file))
242234
242546
  return DEFAULT_SDK_PATH;
242235
242547
  try {
242236
- const pkg = JSON.parse(readFileSync38(file, "utf8"));
242548
+ const pkg = JSON.parse(readFileSync39(file, "utf8"));
242237
242549
  const path = pkg.lecodes?.iosSdkPath;
242238
242550
  if (path !== undefined && typeof path !== "string")
242239
242551
  throw new PackageError(`package.json lecodes.iosSdkPath: a path, as a string`);
@@ -242253,7 +242565,7 @@ var generate = (path, opts = {}) => {
242253
242565
  const report = { plugins: [], repo: { written: [], stale: [], fresh: [] }, failures: [] };
242254
242566
  const place = (f, entry) => {
242255
242567
  const name = shown(root, f.file) || basename22(f.file);
242256
- const current = existsSync45(f.file) ? readFileSync38(f.file, "utf8") : null;
242568
+ const current = existsSync45(f.file) ? readFileSync39(f.file, "utf8") : null;
242257
242569
  if (current === f.text) {
242258
242570
  entry.fresh.push(name);
242259
242571
  return;
@@ -242690,7 +243002,7 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
242690
243002
  };
242691
243003
 
242692
243004
  // src/commands/render.ts
242693
- import { mkdirSync as mkdirSync30, readFileSync as readFileSync40, writeFileSync as writeFileSync32 } from "node:fs";
243005
+ import { mkdirSync as mkdirSync30, readFileSync as readFileSync41, writeFileSync as writeFileSync32 } from "node:fs";
242694
243006
  import { dirname as dirname25, resolve as resolve30 } from "node:path";
242695
243007
 
242696
243008
  // src/compile/headlessBundle.ts
@@ -242778,7 +243090,7 @@ var compileHeadlessBundle = async (opts = {}) => {
242778
243090
  init_designMeta();
242779
243091
  init_cli();
242780
243092
  init_paths();
242781
- import { existsSync as existsSync47, readFileSync as readFileSync39, statSync as statSync17 } from "node:fs";
243093
+ import { existsSync as existsSync47, readFileSync as readFileSync40, statSync as statSync17 } from "node:fs";
242782
243094
  import { isAbsolute as isAbsolute3, join as join51, relative as relative10, resolve as resolve27 } from "node:path";
242783
243095
  var SCREEN_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs"];
242784
243096
  var SCREEN_ENTRY_PATH = "/.screen-entry.ts";
@@ -242812,7 +243124,7 @@ var resolveScreenTarget = (root, file, opts = {}) => {
242812
243124
  throw new CliError(`"${file}" is outside the project (${root}) — a screen renders as part of its project, so its ` + `imports and asset() files resolve from the same tree.`);
242813
243125
  }
242814
243126
  const key = toKey(rel);
242815
- if (!/^\s*export\s+default\b/m.test(readFileSync39(abs, "utf8"))) {
243127
+ if (!/^\s*export\s+default\b/m.test(readFileSync40(abs, "utf8"))) {
242816
243128
  throw new CliError(`${key.slice(1)} has no default export. A screen file ends with ` + `\`export default UIScreen([...])\` — or \`export default (state) => UIScreen([...])\` for screen states.`);
242817
243129
  }
242818
243130
  const invoke = opts.state !== undefined ? `screen(${JSON.stringify(opts.state)})` : "screen()";
@@ -243609,7 +243921,7 @@ var resolveFixtures = (value, baseDir) => {
243609
243921
  if (typeof value === "string") {
243610
243922
  const abs = resolve30(baseDir, value);
243611
243923
  try {
243612
- map = JSON.parse(readFileSync40(abs, "utf8"));
243924
+ map = JSON.parse(readFileSync41(abs, "utf8"));
243613
243925
  } catch (e) {
243614
243926
  throw new CliError(`Can't read fixtures "${value}": ${e instanceof Error ? e.message : String(e)}`);
243615
243927
  }
@@ -243622,7 +243934,7 @@ var resolveFixtures = (value, baseDir) => {
243622
243934
  var readScenarioFile = (path) => {
243623
243935
  let raw;
243624
243936
  try {
243625
- raw = JSON.parse(readFileSync40(resolve30(path), "utf8"));
243937
+ raw = JSON.parse(readFileSync41(resolve30(path), "utf8"));
243626
243938
  } catch (e) {
243627
243939
  throw new CliError(`Can't read scenario "${path}": ${e instanceof Error ? e.message : String(e)}`);
243628
243940
  }
@@ -243967,7 +244279,7 @@ var renderHeadless = async (flags, file) => {
243967
244279
 
243968
244280
  // src/commands/scene.ts
243969
244281
  init_cli();
243970
- import { existsSync as existsSync50, readFileSync as readFileSync42, readdirSync as readdirSync20, statSync as statSync18, writeFileSync as writeFileSync34 } from "node:fs";
244282
+ import { existsSync as existsSync50, readFileSync as readFileSync43, readdirSync as readdirSync20, statSync as statSync18, writeFileSync as writeFileSync34 } from "node:fs";
243971
244283
  import { basename as basename23, dirname as dirname26, join as join54, relative as relative11, resolve as resolve31 } from "node:path";
243972
244284
 
243973
244285
  // src/compile/sceneCompile.ts
@@ -243976,7 +244288,7 @@ init_ignore();
243976
244288
  init_cli();
243977
244289
  init_projectCompile();
243978
244290
  import { createHash as createHash9 } from "node:crypto";
243979
- import { existsSync as existsSync49, mkdirSync as mkdirSync31, readFileSync as readFileSync41, writeFileSync as writeFileSync33 } from "node:fs";
244291
+ import { existsSync as existsSync49, mkdirSync as mkdirSync31, readFileSync as readFileSync42, writeFileSync as writeFileSync33 } from "node:fs";
243980
244292
  import { join as join53 } from "node:path";
243981
244293
  var TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i;
243982
244294
  var rel = (key) => key.replace(/^\//, "");
@@ -243988,9 +244300,9 @@ var compileSceneEditor = async (opts) => {
243988
244300
  for (const file of scan.files) {
243989
244301
  const path = "./" + rel(file.path);
243990
244302
  if (TEXT_EXT.test(path)) {
243991
- entries.push({ path, type: "text", text: readFileSync41(file.absPath, "utf8") });
244303
+ entries.push({ path, type: "text", text: readFileSync42(file.absPath, "utf8") });
243992
244304
  } else if (/\.mat$/i.test(path)) {
243993
- entries.push({ path, type: "shader", text: readFileSync41(file.absPath, "utf8"), absPath: file.absPath });
244305
+ entries.push({ path, type: "shader", text: readFileSync42(file.absPath, "utf8"), absPath: file.absPath });
243994
244306
  } else {
243995
244307
  entries.push({ path, type: "resource", fileSrc: "/project/" + rel(file.path) });
243996
244308
  }
@@ -244097,7 +244409,7 @@ var writeMcpConfig2 = (root, port) => {
244097
244409
  let config = {};
244098
244410
  if (existsSync50(path)) {
244099
244411
  try {
244100
- config = JSON.parse(readFileSync42(path, "utf8"));
244412
+ config = JSON.parse(readFileSync43(path, "utf8"));
244101
244413
  } catch {
244102
244414
  warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-scene server by hand.");
244103
244415
  return;
@@ -244212,7 +244524,7 @@ var serve_default = defineCommand({
244212
244524
  });
244213
244525
 
244214
244526
  // src/commands/shaders/index.ts
244215
- import { readFileSync as readFileSync44 } from "node:fs";
244527
+ import { readFileSync as readFileSync45 } from "node:fs";
244216
244528
  import { relative as relative13 } from "node:path";
244217
244529
  init_cli();
244218
244530
  init_shared();
@@ -244221,7 +244533,7 @@ init_shared();
244221
244533
  init_compile();
244222
244534
  init_distRoot();
244223
244535
  init_cli();
244224
- import { existsSync as existsSync51, mkdirSync as mkdirSync32, readdirSync as readdirSync21, readFileSync as readFileSync43, writeFileSync as writeFileSync35 } from "node:fs";
244536
+ import { existsSync as existsSync51, mkdirSync as mkdirSync32, readdirSync as readdirSync21, readFileSync as readFileSync44, writeFileSync as writeFileSync35 } from "node:fs";
244225
244537
  import { basename as basename25, join as join55, relative as relative12, resolve as resolve32 } from "node:path";
244226
244538
  var engineMaterialsDir = () => {
244227
244539
  const candidates = [
@@ -244328,7 +244640,7 @@ var writeNewShader = (o) => {
244328
244640
  const known = engineMaterialNames();
244329
244641
  if (!known.includes(ref))
244330
244642
  throw new CliError(`no engine material "${ref}". Known: ${known.join(", ")}`);
244331
- mat = refMat(name, ref, readFileSync43(join55(srcDir, `${ref}.mat`), "utf8"));
244643
+ mat = refMat(name, ref, readFileSync44(join55(srcDir, `${ref}.mat`), "utf8"));
244332
244644
  } else
244333
244645
  mat = starterMat(name);
244334
244646
  mkdirSync32(dir, { recursive: true });
@@ -244360,7 +244672,7 @@ var status2 = async () => {
244360
244672
  if (matcId === null)
244361
244673
  state = c.dim("unknown (matc not downloaded)");
244362
244674
  else {
244363
- const text = readFileSync44(shader.absPath, "utf8");
244675
+ const text = readFileSync45(shader.absPath, "utf8");
244364
244676
  if (shaderIsCompiled(root, text, matcId))
244365
244677
  state = "compiled";
244366
244678
  else {
@@ -244369,7 +244681,7 @@ var status2 = async () => {
244369
244681
  }
244370
244682
  }
244371
244683
  log(` ${shader.path.padEnd(width)} ${state}`);
244372
- const schema = parseShaderSchema(readFileSync44(shader.absPath, "utf8"));
244684
+ const schema = parseShaderSchema(readFileSync45(shader.absPath, "utf8"));
244373
244685
  for (const p of schema.params) {
244374
244686
  const range = p.min !== undefined ? ` [${p.min}..${p.max}]` : "";
244375
244687
  const def = p.default !== null && p.default !== undefined ? ` = ${JSON.stringify(p.default)}` : "";
@@ -244410,7 +244722,7 @@ var compileShaders = async (platforms) => {
244410
244722
  }
244411
244723
  const matcExe = await resolveMatcExe();
244412
244724
  const matcId = matcIdentity(matcExe);
244413
- const wasCompiled = new Map(shaders.map((s) => [s.path, shaderIsCompiled(root, readFileSync44(s.absPath, "utf8"), matcId)]));
244725
+ const wasCompiled = new Map(shaders.map((s) => [s.path, shaderIsCompiled(root, readFileSync45(s.absPath, "utf8"), matcId)]));
244414
244726
  const errors = [];
244415
244727
  for (const shader of shaders) {
244416
244728
  try {
@@ -244512,7 +244824,7 @@ var status_default = defineCommand({
244512
244824
  });
244513
244825
 
244514
244826
  // src/commands/test.ts
244515
- import { existsSync as existsSync53, mkdirSync as mkdirSync34, readFileSync as readFileSync45, readdirSync as readdirSync22 } from "node:fs";
244827
+ import { existsSync as existsSync53, mkdirSync as mkdirSync34, readFileSync as readFileSync46, readdirSync as readdirSync22 } from "node:fs";
244516
244828
  import { dirname as dirname28, resolve as resolve33 } from "node:path";
244517
244829
  init_manifest();
244518
244830
 
@@ -245029,7 +245341,7 @@ var slug = (s) => s.replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "").sli
245029
245341
  var readFlowFile = (abs, displayName) => {
245030
245342
  let raw;
245031
245343
  try {
245032
- raw = JSON.parse(readFileSync45(abs, "utf8"));
245344
+ raw = JSON.parse(readFileSync46(abs, "utf8"));
245033
245345
  } catch (e) {
245034
245346
  throw new CliError(`Can't read "${displayName}": ${e instanceof Error ? e.message : String(e)}`);
245035
245347
  }
@@ -245155,7 +245467,7 @@ var runTests = async (args, flags) => {
245155
245467
  if (!desktop && !lite && (wantFlows || flowsOnly) && existsSync53(designMetaPath)) {
245156
245468
  let meta;
245157
245469
  try {
245158
- meta = JSON.parse(readFileSync45(designMetaPath, "utf8"));
245470
+ meta = JSON.parse(readFileSync46(designMetaPath, "utf8"));
245159
245471
  } catch (e) {
245160
245472
  throw new CliError(`Can't read ${designMetaPath}: ${e instanceof Error ? e.message : String(e)}`);
245161
245473
  }
@@ -245228,7 +245540,7 @@ var runTests = async (args, flags) => {
245228
245540
  script: plan.text,
245229
245541
  fixedDtMs: nativeFixedDt
245230
245542
  });
245231
- const r = evaluateDesktopRun(plan, output, (path) => existsSync53(path) ? readFileSync45(path, "utf8") : null);
245543
+ const r = evaluateDesktopRun(plan, output, (path) => existsSync53(path) ? readFileSync46(path, "utf8") : null);
245232
245544
  if (!r.loaded)
245233
245545
  throw new CliError("This desktop host doesn't support scripted input (CREATOR_SCRIPT). Update it: lecodes desktop update (or point LECODES_DESKTOP_EXE at a newer build).");
245234
245546
  return {
@@ -245267,7 +245579,7 @@ var runTests = async (args, flags) => {
245267
245579
  height: flags.height || kase.height || projectViewport?.height || 844,
245268
245580
  onConsole
245269
245581
  });
245270
- const r = evaluateDesktopRun(plan, output, (path) => existsSync53(path) ? readFileSync45(path, "utf8") : null);
245582
+ const r = evaluateDesktopRun(plan, output, (path) => existsSync53(path) ? readFileSync46(path, "utf8") : null);
245271
245583
  return {
245272
245584
  name: kase.name,
245273
245585
  kind: kase.kind,
@@ -245793,7 +246105,7 @@ init_cli();
245793
246105
  // src/web/build.ts
245794
246106
  init_compile();
245795
246107
  import { createHash as createHash10 } from "node:crypto";
245796
- import { copyFileSync as copyFileSync2, cpSync as cpSync7, existsSync as existsSync56, mkdirSync as mkdirSync35, readFileSync as readFileSync46, readdirSync as readdirSync25, rmSync as rmSync18, statSync as statSync20, writeFileSync as writeFileSync37 } from "node:fs";
246108
+ import { copyFileSync as copyFileSync2, cpSync as cpSync7, existsSync as existsSync56, mkdirSync as mkdirSync35, readFileSync as readFileSync47, readdirSync as readdirSync25, rmSync as rmSync18, statSync as statSync20, writeFileSync as writeFileSync37 } from "node:fs";
245797
246109
  import { dirname as dirname30, extname as extname2, join as join59, resolve as resolve37 } from "node:path";
245798
246110
  init_fonts();
245799
246111
  init_assetIcons();
@@ -245837,7 +246149,7 @@ var buildWeb = async (options) => {
245837
246149
  for (const entry of entries) {
245838
246150
  if (entry.type !== "shader" || !entry.absPath)
245839
246151
  continue;
245840
- const text = readFileSync46(entry.absPath, "utf8");
246152
+ const text = readFileSync47(entry.absPath, "utf8");
245841
246153
  const hash = createHash10("sha256").update(text).digest("hex").slice(0, 16);
245842
246154
  shaders.set(hash, JSON.stringify(buildLiteMaterialArtifact(text)));
245843
246155
  entry.shaders = [{ platform: "opengl", src: `shaders/${hash}_opengl.filamat` }];
@@ -245884,7 +246196,7 @@ var buildWeb = async (options) => {
245884
246196
  cpSync7(lite, out, { recursive: true });
245885
246197
  const table = join59(out, "features.json");
245886
246198
  if (existsSync56(table)) {
245887
- const { features, fonts } = JSON.parse(readFileSync46(table, "utf8"));
246199
+ const { features, fonts } = JSON.parse(readFileSync47(table, "utf8"));
245888
246200
  if (config.font === "system")
245889
246201
  for (const file of fonts ?? [])
245890
246202
  rmSync18(join59(out, file), { force: true });
@@ -245898,7 +246210,7 @@ var buildWeb = async (options) => {
245898
246210
  }
245899
246211
  const pluginsTable = join59(out, "plugins.json");
245900
246212
  if (existsSync56(pluginsTable) || halves.length > 0) {
245901
- const { plugins } = existsSync56(pluginsTable) ? JSON.parse(readFileSync46(pluginsTable, "utf8")) : { plugins: {} };
246213
+ const { plugins } = existsSync56(pluginsTable) ? JSON.parse(readFileSync47(pluginsTable, "utf8")) : { plugins: {} };
245902
246214
  const kept = {};
245903
246215
  for (const id of Object.keys(plugins)) {
245904
246216
  if (listed.has(id))
@@ -245948,7 +246260,7 @@ var buildWeb = async (options) => {
245948
246260
  writeFileSync37(join59(out, "shaders", `${hash}_webgl-lite.filamat`), artifact);
245949
246261
  }
245950
246262
  const app = readAppConfigAt(root);
245951
- let html = readFileSync46(join59(lite, "index.html"), "utf8");
246263
+ let html = readFileSync47(join59(lite, "index.html"), "utf8");
245952
246264
  const head = [];
245953
246265
  head.push(`<meta name="theme-color" content="${escapeHtml(config.themeColor ?? "#000000")}" />`);
245954
246266
  if (app?.icon) {
@@ -246043,7 +246355,7 @@ var startPreview = async (dir, port) => {
246043
246355
  init_shared();
246044
246356
 
246045
246357
  // src/dev/https.ts
246046
- import { existsSync as existsSync58, mkdirSync as mkdirSync36, readFileSync as readFileSync47, writeFileSync as writeFileSync38 } from "node:fs";
246358
+ import { existsSync as existsSync58, mkdirSync as mkdirSync36, readFileSync as readFileSync48, writeFileSync as writeFileSync38 } from "node:fs";
246047
246359
  import { homedir as homedir15 } from "node:os";
246048
246360
  import { join as join61 } from "node:path";
246049
246361
  var DIR = () => join61(homedir15(), ".lecodes", "dev-https");
@@ -246053,12 +246365,12 @@ var devCertificate = async (names, dir = DIR()) => {
246053
246365
  const files = { cert: join61(dir, "cert.pem"), key: join61(dir, "key.pem"), meta: join61(dir, "names.json") };
246054
246366
  let kept = null;
246055
246367
  try {
246056
- kept = JSON.parse(readFileSync47(files.meta, "utf8"));
246368
+ kept = JSON.parse(readFileSync48(files.meta, "utf8"));
246057
246369
  } catch {}
246058
246370
  const wanted = [...new Set(["localhost", "127.0.0.1", ...names])];
246059
246371
  const fresh = kept && Date.now() - kept.made < RENEW_DAYS * 86400000 && wanted.every((n) => kept.names.includes(n));
246060
246372
  if (fresh && existsSync58(files.cert) && existsSync58(files.key)) {
246061
- return { cert: readFileSync47(files.cert, "utf8"), key: readFileSync47(files.key, "utf8") };
246373
+ return { cert: readFileSync48(files.cert, "utf8"), key: readFileSync48(files.key, "utf8") };
246062
246374
  }
246063
246375
  const { generate } = await Promise.resolve().then(() => __toESM(require_selfsigned(), 1));
246064
246376
  const all = [...new Set([...kept?.names ?? [], ...wanted])];