lecodes-cli 0.17.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/index.js +1340 -648
  2. package/package.json +3 -3
  3. package/runtime/scene-harness.json +1 -1
  4. package/runtime/sdk/compile/bundler.ts +1 -1
  5. package/runtime/sdk/compile/compileProject.ts +16 -1
  6. package/runtime/sdk/compile/header.ts +6 -1
  7. package/runtime/sdk/compile/index.ts +16 -0
  8. package/runtime/sdk/compile/liteMaterial.ts +247 -0
  9. package/runtime/sdk/compile/shaderTargets.ts +42 -0
  10. package/runtime/sdk/core/Aspect.ts +255 -244
  11. package/runtime/sdk/g2/CharacterController2D.ts +2 -2
  12. package/runtime/sdk/gl/Camera.ts +41 -0
  13. package/runtime/sdk/gl/CameraPlace.ts +51 -0
  14. package/runtime/sdk/gl/CharacterController.ts +6 -2
  15. package/runtime/sdk/gl/IK.ts +193 -174
  16. package/runtime/sdk/gl/Material.ts +23 -0
  17. package/runtime/sdk/gl/Model.ts +7 -6
  18. package/runtime/sdk/gl/Node.ts +270 -285
  19. package/runtime/sdk/gl/Particles.ts +676 -625
  20. package/runtime/sdk/gl/Physics.ts +53 -1
  21. package/runtime/sdk/gl/Scene.ts +323 -291
  22. package/runtime/sdk/gl/Shape.ts +248 -8
  23. package/runtime/sdk/gl/Trigger.ts +50 -46
  24. package/runtime/sdk/gl/Vehicle.ts +519 -0
  25. package/runtime/sdk/gl/{AnimationClip.ts → animation/AnimationClip.ts} +37 -7
  26. package/runtime/sdk/gl/animation/Animator.ts +87 -0
  27. package/runtime/sdk/gl/animation/Layer.ts +29 -0
  28. package/runtime/sdk/gl/animation/Loop.ts +25 -0
  29. package/runtime/sdk/gl/animation/Playback.ts +43 -0
  30. package/runtime/sdk/gl/animation/core.ts +294 -0
  31. package/runtime/sdk/gl/scenarios.ts +26 -58
  32. package/runtime/sdk/inject.ts +18 -9
  33. package/runtime/sdk/runtime/app.ts +13 -0
  34. package/runtime/sdk/runtime/input.ts +169 -6
  35. package/runtime/sdk/scene/defineScene.ts +182 -26
  36. package/runtime/sdk/scene/gizmos.ts +128 -0
  37. package/runtime/sdk-types.json +1 -1
  38. package/runtime/sdk/gl/Animator.ts +0 -642
  39. package/runtime/sdk/gl/ModelAnimation.ts +0 -95
package/dist/index.js CHANGED
@@ -171459,6 +171459,264 @@ var init_detectEntry = __esm(() => {
171459
171459
  EXPORT_RE = /(?:^|[\n;])\s*export\b/;
171460
171460
  });
171461
171461
 
171462
+ // ../sdk/src/compile/shaderTargets.ts
171463
+ var externalSamplerReg, PLATFORM_BUILD, matcPlatformsFor = (text) => {
171464
+ externalSamplerReg.lastIndex = 0;
171465
+ return externalSamplerReg.test(text) ? ["opengl", "metal", "webgl", "desktop"] : ["opengl", "metal", "desktop"];
171466
+ }, matcArgs = (platform, outPath, inPath) => {
171467
+ const build = PLATFORM_BUILD[platform];
171468
+ return ["-p", build.profile, "--no-essl1", "-a", build.api, "-o", outPath, inPath];
171469
+ };
171470
+ var init_shaderTargets = __esm(() => {
171471
+ externalSamplerReg = /type\s*:\s*samplerExternal/g;
171472
+ PLATFORM_BUILD = {
171473
+ opengl: { profile: "mobile", api: "opengl", replaceExternal: false },
171474
+ metal: { profile: "mobile", api: "metal", replaceExternal: false },
171475
+ webgl: { profile: "mobile", api: "opengl", replaceExternal: true },
171476
+ desktop: { profile: "desktop", api: "opengl", replaceExternal: true }
171477
+ };
171478
+ });
171479
+
171480
+ // ../sdk/src/compile/liteMaterial.ts
171481
+ var extractMatBlocks = (text) => {
171482
+ const blocks = {};
171483
+ let i = 0;
171484
+ const skipNoise = () => {
171485
+ for (;; ) {
171486
+ while (i < text.length && /\s/.test(text[i]))
171487
+ i++;
171488
+ if (text[i] === "/" && text[i + 1] === "/") {
171489
+ while (i < text.length && text[i] !== `
171490
+ `)
171491
+ i++;
171492
+ } else if (text[i] === "/" && text[i + 1] === "*") {
171493
+ i += 2;
171494
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/"))
171495
+ i++;
171496
+ i += 2;
171497
+ } else
171498
+ return;
171499
+ }
171500
+ };
171501
+ while (i < text.length) {
171502
+ skipNoise();
171503
+ const m = /^[A-Za-z_][A-Za-z0-9_]*/.exec(text.slice(i));
171504
+ if (!m) {
171505
+ i++;
171506
+ continue;
171507
+ }
171508
+ i += m[0].length;
171509
+ skipNoise();
171510
+ if (text[i] !== "{")
171511
+ continue;
171512
+ let depth = 0;
171513
+ const start = i;
171514
+ while (i < text.length) {
171515
+ const c2 = text[i];
171516
+ if (c2 === "/" && text[i + 1] === "/") {
171517
+ while (i < text.length && text[i] !== `
171518
+ `)
171519
+ i++;
171520
+ } else if (c2 === "/" && text[i + 1] === "*") {
171521
+ i += 2;
171522
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/"))
171523
+ i++;
171524
+ i++;
171525
+ } else if (c2 === "{")
171526
+ depth++;
171527
+ else if (c2 === "}") {
171528
+ depth--;
171529
+ if (depth === 0)
171530
+ break;
171531
+ }
171532
+ i++;
171533
+ }
171534
+ blocks[m[0]] = text.slice(start + 1, i);
171535
+ i++;
171536
+ }
171537
+ return blocks;
171538
+ }, parseJsonish = (src) => {
171539
+ let i = 0;
171540
+ const fail = (msg) => {
171541
+ throw new Error(`material block: ${msg} at offset ${i}`);
171542
+ };
171543
+ const skip = () => {
171544
+ for (;; ) {
171545
+ while (i < src.length && /[\s,]/.test(src[i]))
171546
+ i++;
171547
+ if (src[i] === "/" && src[i + 1] === "/") {
171548
+ while (i < src.length && src[i] !== `
171549
+ `)
171550
+ i++;
171551
+ } else if (src[i] === "/" && src[i + 1] === "*") {
171552
+ i += 2;
171553
+ while (i < src.length && !(src[i] === "*" && src[i + 1] === "/"))
171554
+ i++;
171555
+ i += 2;
171556
+ } else
171557
+ return;
171558
+ }
171559
+ };
171560
+ const quoted = (q) => {
171561
+ i++;
171562
+ const start = i;
171563
+ while (i < src.length && src[i] !== q)
171564
+ i++;
171565
+ return src.slice(start, i++);
171566
+ };
171567
+ const word = () => {
171568
+ const m = /^[^\s{}[\]:,="']+/.exec(src.slice(i));
171569
+ if (!m)
171570
+ fail("expected a value");
171571
+ i += m[0].length;
171572
+ return m[0];
171573
+ };
171574
+ const value = () => {
171575
+ skip();
171576
+ const c2 = src[i];
171577
+ if (c2 === "{")
171578
+ return object();
171579
+ if (c2 === "[")
171580
+ return array();
171581
+ if (c2 === '"' || c2 === "'")
171582
+ return quoted(c2);
171583
+ const w = word();
171584
+ if (w === "true")
171585
+ return true;
171586
+ if (w === "false")
171587
+ return false;
171588
+ const n = Number(w);
171589
+ return Number.isNaN(n) ? w : n;
171590
+ };
171591
+ const array = () => {
171592
+ i++;
171593
+ const out = [];
171594
+ for (;; ) {
171595
+ skip();
171596
+ if (i >= src.length)
171597
+ fail("unterminated array");
171598
+ if (src[i] === "]") {
171599
+ i++;
171600
+ return out;
171601
+ }
171602
+ out.push(value());
171603
+ }
171604
+ };
171605
+ const object = () => {
171606
+ i++;
171607
+ const out = {};
171608
+ for (;; ) {
171609
+ skip();
171610
+ if (i >= src.length)
171611
+ fail("unterminated object");
171612
+ if (src[i] === "}") {
171613
+ i++;
171614
+ return out;
171615
+ }
171616
+ const key = src[i] === '"' || src[i] === "'" ? quoted(src[i]) : word();
171617
+ skip();
171618
+ if (src[i] !== ":" && src[i] !== "=")
171619
+ fail(`expected ':' after key "${key}"`);
171620
+ i++;
171621
+ out[key] = value();
171622
+ }
171623
+ };
171624
+ skip();
171625
+ if (src[i] !== "{")
171626
+ fail("expected '{'");
171627
+ return object();
171628
+ }, PARAM_TYPES, SAMPLER_TYPES, unsupported = (reason) => ({
171629
+ lecodesLiteMaterial: 1,
171630
+ unsupported: reason,
171631
+ shadingModel: "unlit",
171632
+ blending: "opaque",
171633
+ maskThreshold: 0.4,
171634
+ doubleSided: false,
171635
+ culling: "back",
171636
+ depthWrite: true,
171637
+ shadowMultiplier: false,
171638
+ params: [],
171639
+ samplers: [],
171640
+ fragmentCode: "",
171641
+ writesNormal: false,
171642
+ hasVertexBlock: false
171643
+ }), buildLiteMaterialArtifact = (text) => {
171644
+ try {
171645
+ const blocks = extractMatBlocks(text);
171646
+ if (blocks.material === undefined)
171647
+ return unsupported("no material block");
171648
+ if (blocks.fragment === undefined)
171649
+ return unsupported("no fragment block");
171650
+ const def = parseJsonish("{" + blocks.material + "}");
171651
+ const params = [];
171652
+ const samplers = [];
171653
+ const rawParams = def.parameters === undefined ? [] : Array.isArray(def.parameters) ? def.parameters : [def.parameters];
171654
+ for (const raw of rawParams) {
171655
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw))
171656
+ return unsupported("malformed parameter entry");
171657
+ const name = String(raw.name ?? "");
171658
+ const typeRaw = String(raw.type ?? "");
171659
+ const typeMatch = /^(\w+)(?:\[(\d+)\])?$/.exec(typeRaw);
171660
+ if (!name || !typeMatch)
171661
+ return unsupported(`malformed parameter "${name || typeRaw}"`);
171662
+ const [, type, size] = typeMatch;
171663
+ if (SAMPLER_TYPES.has(type)) {
171664
+ if (size)
171665
+ return unsupported(`sampler arrays are not supported ("${name}")`);
171666
+ samplers.push({ name });
171667
+ } else if (PARAM_TYPES.has(type)) {
171668
+ params.push(size ? { name, type, size: Number(size) } : { name, type });
171669
+ } else {
171670
+ return unsupported(`parameter type "${typeRaw}" ("${name}")`);
171671
+ }
171672
+ }
171673
+ const shadingModel = def.shadingModel === "unlit" ? "unlit" : "lit";
171674
+ const blendingRaw = String(def.blending ?? "opaque");
171675
+ const blending = blendingRaw === "transparent" || blendingRaw === "fade" || blendingRaw === "add" || blendingRaw === "masked" ? blendingRaw : blendingRaw === "multiply" || blendingRaw === "screen" ? "transparent" : "opaque";
171676
+ const culling = def.culling === "none" ? "none" : def.culling === "front" ? "front" : "back";
171677
+ const depthWrite = typeof def.depthWrite === "boolean" ? def.depthWrite : blending !== "transparent" && blending !== "fade" && blending !== "add";
171678
+ const fragmentCode = blocks.fragment.replace(/\bmaterialParams\./g, "materialParams_");
171679
+ if (!/\bvoid\s+material\s*\(/.test(fragmentCode))
171680
+ return unsupported("fragment block has no material() function");
171681
+ return {
171682
+ lecodesLiteMaterial: 1,
171683
+ shadingModel,
171684
+ blending,
171685
+ maskThreshold: typeof def.maskThreshold === "number" ? def.maskThreshold : 0.4,
171686
+ doubleSided: def.doubleSided === true,
171687
+ culling,
171688
+ depthWrite,
171689
+ shadowMultiplier: def.shadowMultiplier === true,
171690
+ params,
171691
+ samplers,
171692
+ fragmentCode,
171693
+ writesNormal: /\.normal\s*=/.test(fragmentCode),
171694
+ hasVertexBlock: blocks.vertex !== undefined
171695
+ };
171696
+ } catch (e) {
171697
+ return unsupported(e instanceof Error ? e.message : String(e));
171698
+ }
171699
+ };
171700
+ var init_liteMaterial = __esm(() => {
171701
+ PARAM_TYPES = new Set([
171702
+ "float",
171703
+ "float2",
171704
+ "float3",
171705
+ "float4",
171706
+ "int",
171707
+ "int2",
171708
+ "int3",
171709
+ "int4",
171710
+ "bool",
171711
+ "bool2",
171712
+ "bool3",
171713
+ "bool4",
171714
+ "float3x3",
171715
+ "float4x4"
171716
+ ]);
171717
+ SAMPLER_TYPES = new Set(["sampler2d", "samplerExternal"]);
171718
+ });
171719
+
171462
171720
  // ../sdk/src/compile/sourcemap.ts
171463
171721
  var offsetSourceMap = (mapJson, lines) => {
171464
171722
  if (lines <= 0)
@@ -171494,7 +171752,10 @@ var buildHeader = (name, code, opts = {}) => {
171494
171752
  `;
171495
171753
  const localAssets = new Set;
171496
171754
  for (const m of code.matchAll(/_creatorUtils\.fetchLocal\((.+?)\)/g)) {
171497
- localAssets.add(m[1].replaceAll(/['"]/g, "").trim());
171755
+ const arg = m[1].trim();
171756
+ if (!/^(['"])[^'"]*\1$/.test(arg))
171757
+ continue;
171758
+ localAssets.add(arg.slice(1, -1).trim());
171498
171759
  }
171499
171760
  if (/_creator\.createGlb/.test(code))
171500
171761
  localAssets.add("uberarchive");
@@ -171540,7 +171801,11 @@ var compileProject = async (opts) => {
171540
171801
  }
171541
171802
  } else if (e.type === "shader") {
171542
171803
  const shaders = e.shaders ?? [];
171543
- if (shaders.length > 2) {
171804
+ if (e.shadersLocal && shaders.length > 0) {
171805
+ const name = shaders[0].src.split(/[\\/]/).pop() ?? "";
171806
+ const [prefix, suffix = ".filamat"] = name.split("_opengl");
171807
+ files[e.path] = `export default ("id:" + _creatorUtils.fetchLocal(${JSON.stringify(prefix + "_")} + _creator.backend + ${JSON.stringify(suffix)}))`;
171808
+ } else if (shaders.length > 2) {
171544
171809
  files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.subBackend ?? _creator.backend}")}\``;
171545
171810
  } else if (shaders.length > 0) {
171546
171811
  files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.backend}")}\``;
@@ -172479,7 +172744,10 @@ __export(exports_compile, {
172479
172744
  sanitizeAssetName: () => sanitizeAssetName,
172480
172745
  rewriteAssetIcons: () => rewriteAssetIcons,
172481
172746
  parseSfnt: () => parseSfnt,
172747
+ parseJsonish: () => parseJsonish,
172482
172748
  offsetSourceMap: () => offsetSourceMap,
172749
+ matcPlatformsFor: () => matcPlatformsFor,
172750
+ matcArgs: () => matcArgs,
172483
172751
  loadSceneHarness: () => loadSceneHarness,
172484
172752
  isServerPath: () => isServerPath,
172485
172753
  isCanonicalAssetName: () => isCanonicalAssetName,
@@ -172487,6 +172755,8 @@ __export(exports_compile, {
172487
172755
  iconNotFoundError: () => iconNotFoundError,
172488
172756
  fontRegistryBase: () => fontRegistryBase,
172489
172757
  faceFileName: () => faceFileName,
172758
+ extractMatBlocks: () => extractMatBlocks,
172759
+ externalSamplerReg: () => externalSamplerReg,
172490
172760
  detectProjectKind: () => detectProjectKind,
172491
172761
  detectEntry: () => detectEntry,
172492
172762
  deriveEndpointParams: () => deriveEndpointParams,
@@ -172495,6 +172765,7 @@ __export(exports_compile, {
172495
172765
  collectVendoredIcons: () => collectVendoredIcons,
172496
172766
  bundleProjectWithMap: () => bundleProjectWithMap,
172497
172767
  bundleProject: () => bundleProject,
172768
+ buildLiteMaterialArtifact: () => buildLiteMaterialArtifact,
172498
172769
  buildIconsHeaderValue: () => buildIconsHeaderValue,
172499
172770
  buildHeader: () => buildHeader,
172500
172771
  buildFontsHeaderValue: () => buildFontsHeaderValue,
@@ -172504,6 +172775,7 @@ __export(exports_compile, {
172504
172775
  SERVER_ENTRY_PATH: () => SERVER_ENTRY_PATH,
172505
172776
  SCENE_HARNESS_DIR: () => SCENE_HARNESS_DIR,
172506
172777
  SCENE_ENTRY_PATH: () => SCENE_ENTRY_PATH,
172778
+ PLATFORM_BUILD: () => PLATFORM_BUILD,
172507
172779
  ICON_ID: () => ICON_ID,
172508
172780
  FONT_REGISTRY: () => FONT_REGISTRY,
172509
172781
  EndpointTypeError: () => EndpointTypeError,
@@ -172517,6 +172789,8 @@ var init_compile = __esm(() => {
172517
172789
  init_serverTypes();
172518
172790
  init_detectEntry();
172519
172791
  init_assetName();
172792
+ init_shaderTargets();
172793
+ init_liteMaterial();
172520
172794
  init_compileProject();
172521
172795
  init_sceneEditor();
172522
172796
  init_fontMacro();
@@ -172544,7 +172818,7 @@ var vendoredInject, resolveChiselBin2 = () => {
172544
172818
  return null;
172545
172819
  return `Ambiguous entrypoint: nothing imports ${roots.join(", ")} — picked ${entry}. Rename your entry to main.ts (or pass --entry).`;
172546
172820
  }, compileProject2 = (opts) => compileProject({
172547
- entries: opts.entries.map((e) => ({ path: e.path, type: e.type, text: e.text, fileSrc: e.fileSrc, remote: e.remote, shaders: e.shaders })),
172821
+ entries: opts.entries.map((e) => ({ path: e.path, type: e.type, text: e.text, fileSrc: e.fileSrc, remote: e.remote, shaders: e.shaders, shadersLocal: e.shadersLocal })),
172548
172822
  name: opts.name,
172549
172823
  publicUrl: opts.publicUrl,
172550
172824
  entryOverride: opts.entryOverride,
@@ -172682,11 +172956,11 @@ var init_fonts = __esm(() => {
172682
172956
  });
172683
172957
 
172684
172958
  // src/compile/assetIcons.ts
172685
- import { createHash as createHash3 } from "node:crypto";
172686
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "node:fs";
172687
- import { homedir as homedir2 } from "node:os";
172688
- import { dirname as dirname7, join as join11 } from "node:path";
172689
- var DEFAULT_REGISTRY, isHttp2 = (s) => /^https?:/i.test(s), sha122 = (s) => createHash3("sha256").update(s).digest("hex").slice(0, 12), readText = async (registry, rel) => {
172959
+ import { createHash as createHash5 } from "node:crypto";
172960
+ import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "node:fs";
172961
+ import { homedir as homedir3 } from "node:os";
172962
+ import { dirname as dirname7, join as join14 } from "node:path";
172963
+ var DEFAULT_REGISTRY, isHttp2 = (s) => /^https?:/i.test(s), sha122 = (s) => createHash5("sha256").update(s).digest("hex").slice(0, 12), readText = async (registry, rel) => {
172690
172964
  if (isHttp2(registry)) {
172691
172965
  try {
172692
172966
  const res = await fetch(`${registry}/${rel}`);
@@ -172696,21 +172970,21 @@ var DEFAULT_REGISTRY, isHttp2 = (s) => /^https?:/i.test(s), sha122 = (s) => crea
172696
172970
  }
172697
172971
  }
172698
172972
  try {
172699
- return readFileSync10(join11(registry, rel), "utf8");
172973
+ return readFileSync11(join14(registry, rel), "utf8");
172700
172974
  } catch {
172701
172975
  return null;
172702
172976
  }
172703
172977
  }, safeRead = (path) => {
172704
172978
  try {
172705
- return readFileSync10(path, "utf8");
172979
+ return readFileSync11(path, "utf8");
172706
172980
  } catch {
172707
172981
  return null;
172708
172982
  }
172709
172983
  }, writeIfChanged2 = (path, text) => {
172710
- const current = existsSync9(path) ? safeRead(path) : null;
172984
+ const current = existsSync12(path) ? safeRead(path) : null;
172711
172985
  if (current === text)
172712
172986
  return false;
172713
- writeFileSync6(path, text);
172987
+ writeFileSync8(path, text);
172714
172988
  return true;
172715
172989
  }, fetchIcon = async (registry, pack, name) => {
172716
172990
  const svg = await readText(registry, `${pack}/${name}.svg`);
@@ -172724,7 +172998,7 @@ var DEFAULT_REGISTRY, isHttp2 = (s) => /^https?:/i.test(s), sha122 = (s) => crea
172724
172998
  return JSON.parse(raw);
172725
172999
  }, readLock2 = (path) => {
172726
173000
  try {
172727
- const lock = JSON.parse(readFileSync10(path, "utf8"));
173001
+ const lock = JSON.parse(readFileSync11(path, "utf8"));
172728
173002
  if (lock && typeof lock === "object" && lock.icons && lock.packs)
172729
173003
  return lock;
172730
173004
  } catch {}
@@ -172759,13 +173033,13 @@ var DEFAULT_REGISTRY, isHttp2 = (s) => /^https?:/i.test(s), sha122 = (s) => crea
172759
173033
  `);
172760
173034
  }, vendorIcons = async (designDir, ids, opts = {}) => {
172761
173035
  const registry = (opts.registry ?? DEFAULT_REGISTRY).replace(/\/+$/, "");
172762
- const lockPath = join11(designDir, "icons.lock.json");
173036
+ const lockPath = join14(designDir, "icons.lock.json");
172763
173037
  const lock = readLock2(lockPath) ?? { registry, icons: {}, packs: {} };
172764
173038
  const svgById = new Map;
172765
173039
  const missing = [];
172766
173040
  for (const id of ids) {
172767
173041
  const li = lock.icons[id];
172768
- const cached = li && safeRead(join11(designDir, li.path));
173042
+ const cached = li && safeRead(join14(designDir, li.path));
172769
173043
  if (cached)
172770
173044
  svgById.set(id, cached);
172771
173045
  else
@@ -172781,9 +173055,9 @@ var DEFAULT_REGISTRY, isHttp2 = (s) => /^https?:/i.test(s), sha122 = (s) => crea
172781
173055
  throw new Error(`assetIcon: unknown icon pack "${pack}" (in "${id}"). Available packs: ${Object.keys(licenses).sort().join(", ")}.`);
172782
173056
  const svg = await fetchIcon(registry, pack, name);
172783
173057
  const rel = `assets/icons/${pack}/${name}.svg`;
172784
- const abs = join11(designDir, rel);
172785
- mkdirSync5(dirname7(abs), { recursive: true });
172786
- writeFileSync6(abs, svg);
173058
+ const abs = join14(designDir, rel);
173059
+ mkdirSync7(dirname7(abs), { recursive: true });
173060
+ writeFileSync8(abs, svg);
172787
173061
  lock.icons[id] = { pack, name, hash: sha122(svg), spdx: lic.spdx, path: rel };
172788
173062
  lock.packs[pack] = { spdx: lic.spdx, title: lic.title, url: lic.url, attribution: lic.attribution, modifiedNotice: lic.modifiedNotice };
172789
173063
  svgById.set(id, svg);
@@ -172792,25 +173066,25 @@ var DEFAULT_REGISTRY, isHttp2 = (s) => /^https?:/i.test(s), sha122 = (s) => crea
172792
173066
  writeIfChanged2(lockPath, serializeLock2(lock));
172793
173067
  }
172794
173068
  if (Object.keys(lock.icons).length)
172795
- writeIfChanged2(join11(designDir, "ICON-NOTICES.md"), renderNotices2(lock));
173069
+ writeIfChanged2(join14(designDir, "ICON-NOTICES.md"), renderNotices2(lock));
172796
173070
  return svgById;
172797
173071
  }, applyAssetIcons = async (entries, designDir, opts = {}) => {
172798
173072
  await applyAssetIconMacro(entries, (ids) => vendorIcons(designDir, ids, opts));
172799
- }, defaultCacheDir = () => join11(homedir2(), ".lecodes", "cache", "icons"), cacheIcons = async (ids, opts = {}) => {
173073
+ }, defaultCacheDir = () => join14(homedir3(), ".lecodes", "cache", "icons"), cacheIcons = async (ids, opts = {}) => {
172800
173074
  const registry = (opts.registry ?? DEFAULT_REGISTRY).replace(/\/+$/, "");
172801
173075
  const cacheDir = opts.cacheDir ?? defaultCacheDir();
172802
173076
  const svgById = new Map;
172803
173077
  for (const id of ids) {
172804
173078
  const [, pack, name] = ICON_ID.exec(id);
172805
- const cachePath = join11(cacheDir, pack, `${name}.svg`);
173079
+ const cachePath = join14(cacheDir, pack, `${name}.svg`);
172806
173080
  const cached = safeRead(cachePath);
172807
173081
  if (cached) {
172808
173082
  svgById.set(id, cached);
172809
173083
  continue;
172810
173084
  }
172811
173085
  const svg = await fetchIcon(registry, pack, name);
172812
- mkdirSync5(dirname7(cachePath), { recursive: true });
172813
- writeFileSync6(cachePath, svg);
173086
+ mkdirSync7(dirname7(cachePath), { recursive: true });
173087
+ writeFileSync8(cachePath, svg);
172814
173088
  svgById.set(id, svg);
172815
173089
  }
172816
173090
  return svgById;
@@ -177889,7 +178163,7 @@ var require_image_q = __commonJS((exports, module) => {
177889
178163
  colors,
177890
178164
  onProgress
177891
178165
  } = {}) {
177892
- return new Promise((resolve3, reject) => {
178166
+ return new Promise((resolve4, reject) => {
177893
178167
  const distanceCalculator = colorDistanceFormulaToColorDistance(colorDistanceFormula);
177894
178168
  const paletteQuantizer = paletteQuantizationToPaletteQuantizer(distanceCalculator, paletteQuantization, colors);
177895
178169
  images.forEach((image2) => paletteQuantizer.sample(image2));
@@ -177899,7 +178173,7 @@ var require_image_q = __commonJS((exports, module) => {
177899
178173
  try {
177900
178174
  const result = iterator.next();
177901
178175
  if (result.done) {
177902
- resolve3(palette2);
178176
+ resolve4(palette2);
177903
178177
  } else {
177904
178178
  if (result.value.palette)
177905
178179
  palette2 = result.value.palette;
@@ -177924,7 +178198,7 @@ var require_image_q = __commonJS((exports, module) => {
177924
178198
  imageQuantization,
177925
178199
  onProgress
177926
178200
  } = {}) {
177927
- return new Promise((resolve3, reject) => {
178201
+ return new Promise((resolve4, reject) => {
177928
178202
  const distanceCalculator = colorDistanceFormulaToColorDistance(colorDistanceFormula);
177929
178203
  const imageQuantizer = imageQuantizationToImageQuantizer(distanceCalculator, imageQuantization);
177930
178204
  let outPointContainer;
@@ -177933,7 +178207,7 @@ var require_image_q = __commonJS((exports, module) => {
177933
178207
  try {
177934
178208
  const result = iterator.next();
177935
178209
  if (result.done) {
177936
- resolve3(outPointContainer);
178210
+ resolve4(outPointContainer);
177937
178211
  } else {
177938
178212
  if (result.value.pointContainer) {
177939
178213
  outPointContainer = result.value.pointContainer;
@@ -178267,22 +178541,22 @@ var require_gifutil = __commonJS((exports) => {
178267
178541
  }
178268
178542
  }
178269
178543
  function _readBinary(path) {
178270
- return new Promise((resolve3, reject) => {
178544
+ return new Promise((resolve4, reject) => {
178271
178545
  fs.readFile(path, (err, buffer) => {
178272
178546
  if (err) {
178273
178547
  return reject(err);
178274
178548
  }
178275
- return resolve3(buffer);
178549
+ return resolve4(buffer);
178276
178550
  });
178277
178551
  });
178278
178552
  }
178279
178553
  function _writeBinary(path, buffer) {
178280
- return new Promise((resolve3, reject) => {
178554
+ return new Promise((resolve4, reject) => {
178281
178555
  fs.writeFile(path, buffer, (err) => {
178282
178556
  if (err) {
178283
178557
  return reject(err);
178284
178558
  }
178285
- return resolve3();
178559
+ return resolve4();
178286
178560
  });
178287
178561
  });
178288
178562
  }
@@ -195848,7 +196122,7 @@ var require_await_to_js_umd = __commonJS((exports, module) => {
195848
196122
 
195849
196123
  // ../../node_modules/.bun/@jimp+file-ops@1.6.1/node_modules/@jimp/file-ops/dist/esm/index.js
195850
196124
  import { promises as fs } from "fs";
195851
- import { existsSync as existsSync11 } from "fs";
196125
+ import { existsSync as existsSync14 } from "fs";
195852
196126
  var readFile, writeFile;
195853
196127
  var init_esm13 = __esm(() => {
195854
196128
  readFile = fs.readFile;
@@ -197515,9 +197789,9 @@ class Deferred {
197515
197789
  constructor() {
197516
197790
  this.resolve = () => null;
197517
197791
  this.reject = () => null;
197518
- this.promise = new Promise((resolve3, reject) => {
197792
+ this.promise = new Promise((resolve4, reject) => {
197519
197793
  this.reject = reject;
197520
- this.resolve = resolve3;
197794
+ this.resolve = resolve4;
197521
197795
  });
197522
197796
  }
197523
197797
  }
@@ -200209,7 +200483,7 @@ function readByobReaderWithSignal(reader, buffer, signal) {
200209
200483
  return reader.read(buffer);
200210
200484
  }
200211
200485
  signal.throwIfAborted();
200212
- return new Promise((resolve3, reject) => {
200486
+ return new Promise((resolve4, reject) => {
200213
200487
  const cleanup = () => {
200214
200488
  signal.removeEventListener("abort", onAbort);
200215
200489
  };
@@ -200228,7 +200502,7 @@ function readByobReaderWithSignal(reader, buffer, signal) {
200228
200502
  try {
200229
200503
  const result = await reader.read(buffer);
200230
200504
  cleanup();
200231
- resolve3(result);
200505
+ resolve4(result);
200232
200506
  } catch (error) {
200233
200507
  cleanup();
200234
200508
  reject(error);
@@ -202120,7 +202394,7 @@ var init_file_type = __esm(() => {
202120
202394
  const { signal } = this.options;
202121
202395
  const normalizedSampleSize = normalizeSampleSize(sampleSize);
202122
202396
  signal?.throwIfAborted();
202123
- return new Promise((resolve3, reject) => {
202397
+ return new Promise((resolve4, reject) => {
202124
202398
  let isSettled = false;
202125
202399
  const cleanup = () => {
202126
202400
  readableStream.off("error", onError);
@@ -202159,7 +202433,7 @@ var init_file_type = __esm(() => {
202159
202433
  settle(reject, error);
202160
202434
  }
202161
202435
  }
202162
- settle(resolve3, outputStream);
202436
+ settle(resolve4, outputStream);
202163
202437
  } catch (error) {
202164
202438
  settle(reject, error);
202165
202439
  }
@@ -202228,7 +202502,7 @@ function createJimp({ plugins: pluginsArg, formats: formatsArg } = {}) {
202228
202502
  if (Buffer.isBuffer(url) || url instanceof ArrayBuffer) {
202229
202503
  return this.fromBuffer(url);
202230
202504
  }
202231
- if (existsSync11(url)) {
202505
+ if (existsSync14(url)) {
202232
202506
  return this.fromBuffer(await readFile(url));
202233
202507
  }
202234
202508
  const [fetchErr, response] = await import_await_to_js.to(fetch(url));
@@ -212491,12 +212765,12 @@ var require_parser3 = __commonJS((exports) => {
212491
212765
  };
212492
212766
  Parser.prototype.parseStringPromise = function(str) {
212493
212767
  return new Promise(function(_this) {
212494
- return function(resolve3, reject) {
212768
+ return function(resolve4, reject) {
212495
212769
  return _this.parseString(str, function(err, value) {
212496
212770
  if (err) {
212497
212771
  return reject(err);
212498
212772
  } else {
212499
- return resolve3(value);
212773
+ return resolve4(value);
212500
212774
  }
212501
212775
  });
212502
212776
  };
@@ -213129,8 +213403,8 @@ var init_load_bitmap_font = __esm(() => {
213129
213403
  async function loadFont(file) {
213130
213404
  let fileOrBuffer = file;
213131
213405
  if (typeof window === "undefined" && !isWebWorker) {
213132
- const { existsSync: existsSync12, promises: fs3 } = await import("fs");
213133
- if (existsSync12(file)) {
213406
+ const { existsSync: existsSync15, promises: fs3 } = await import("fs");
213407
+ if (existsSync15(file)) {
213134
213408
  fileOrBuffer = await fs3.readFile(file);
213135
213409
  }
213136
213410
  }
@@ -213453,65 +213727,65 @@ var init_esm30 = __esm(() => {
213453
213727
  });
213454
213728
 
213455
213729
  // src/types.ts
213456
- import { dirname as dirname13, join as join18, relative as relative3, sep } from "node:path";
213457
- import { existsSync as existsSync17, mkdirSync as mkdirSync12, readdirSync as readdirSync8, readFileSync as readFileSync16, rmSync as rmSync5, statSync as statSync6, writeFileSync as writeFileSync14 } from "node:fs";
213730
+ import { dirname as dirname13, join as join21, relative as relative3, sep } from "node:path";
213731
+ import { existsSync as existsSync20, mkdirSync as mkdirSync14, readdirSync as readdirSync9, readFileSync as readFileSync17, rmSync as rmSync7, statSync as statSync7, writeFileSync as writeFileSync16 } from "node:fs";
213458
213732
  var TSCONFIG, listBundleFiles = (dir, base = dir) => {
213459
- if (!existsSync17(dir))
213733
+ if (!existsSync20(dir))
213460
213734
  return [];
213461
213735
  const out = [];
213462
- for (const name of readdirSync8(dir)) {
213463
- const path2 = join18(dir, name);
213464
- if (statSync6(path2).isDirectory())
213736
+ for (const name of readdirSync9(dir)) {
213737
+ const path2 = join21(dir, name);
213738
+ if (statSync7(path2).isDirectory())
213465
213739
  out.push(...listBundleFiles(path2, base));
213466
213740
  else
213467
213741
  out.push(relative3(base, path2).split(sep).join("/"));
213468
213742
  }
213469
213743
  return out;
213470
213744
  }, pruneEmptyDirs = (dir, root) => {
213471
- if (!existsSync17(dir) || !statSync6(dir).isDirectory())
213745
+ if (!existsSync20(dir) || !statSync7(dir).isDirectory())
213472
213746
  return;
213473
- for (const name of readdirSync8(dir))
213474
- pruneEmptyDirs(join18(dir, name), root);
213475
- if (dir !== root && readdirSync8(dir).length === 0)
213476
- rmSync5(dir, { recursive: true });
213747
+ for (const name of readdirSync9(dir))
213748
+ pruneEmptyDirs(join21(dir, name), root);
213749
+ if (dir !== root && readdirSync9(dir).length === 0)
213750
+ rmSync7(dir, { recursive: true });
213477
213751
  }, writeTypeBundle = (root, bundle) => {
213478
- const typesDir = join18(root, LECODES_DIR, "types");
213752
+ const typesDir = join21(root, LECODES_DIR, "types");
213479
213753
  const before = new Set(listBundleFiles(typesDir));
213480
213754
  const stats = { added: 0, updated: 0, removed: 0, total: Object.keys(bundle).length };
213481
213755
  for (const [path2, text] of Object.entries(bundle)) {
213482
- const dest = join18(typesDir, path2);
213756
+ const dest = join21(typesDir, path2);
213483
213757
  if (!before.has(path2))
213484
213758
  stats.added++;
213485
- else if (readFileSync16(dest, "utf8") !== text)
213759
+ else if (readFileSync17(dest, "utf8") !== text)
213486
213760
  stats.updated++;
213487
213761
  else
213488
213762
  continue;
213489
- mkdirSync12(dirname13(dest), { recursive: true });
213490
- writeFileSync14(dest, text);
213763
+ mkdirSync14(dirname13(dest), { recursive: true });
213764
+ writeFileSync16(dest, text);
213491
213765
  }
213492
213766
  for (const path2 of before) {
213493
213767
  if (path2 in bundle)
213494
213768
  continue;
213495
- rmSync5(join18(typesDir, path2));
213769
+ rmSync7(join21(typesDir, path2));
213496
213770
  stats.removed++;
213497
213771
  }
213498
213772
  pruneEmptyDirs(typesDir, typesDir);
213499
213773
  return stats;
213500
213774
  }, bundleHash = (bundle) => sha256(Object.keys(bundle).sort().map((k) => `${k}\x00${bundle[k]}`).join("\x00")), readProjectTypeBundle = (root) => {
213501
- const typesDir = join18(root, LECODES_DIR, "types");
213775
+ const typesDir = join21(root, LECODES_DIR, "types");
213502
213776
  const bundle = {};
213503
213777
  for (const path2 of listBundleFiles(typesDir))
213504
- bundle[path2] = readFileSync16(join18(typesDir, path2), "utf8");
213778
+ bundle[path2] = readFileSync17(join21(typesDir, path2), "utf8");
213505
213779
  return bundle;
213506
213780
  }, materializeTypes = async (root, apiUrl) => writeTypeBundle(root, await getTypes(apiUrl)), readLocalTypeBundle = () => {
213507
213781
  const candidates = [
213508
- join18(distRoot, "runtime", "sdk-types.json"),
213509
- join18(distRoot, "..", "sdk", "dist", "types.json")
213782
+ join21(distRoot, "runtime", "sdk-types.json"),
213783
+ join21(distRoot, "..", "sdk", "dist", "types.json")
213510
213784
  ];
213511
213785
  for (const path2 of candidates) {
213512
- if (existsSync17(path2)) {
213786
+ if (existsSync20(path2)) {
213513
213787
  try {
213514
- return JSON.parse(readFileSync16(path2, "utf8"));
213788
+ return JSON.parse(readFileSync17(path2, "utf8"));
213515
213789
  } catch {}
213516
213790
  }
213517
213791
  }
@@ -213523,7 +213797,7 @@ var TSCONFIG, listBundleFiles = (dir, base = dir) => {
213523
213797
  writeTypeBundle(root, bundle);
213524
213798
  return true;
213525
213799
  }, writeTsconfig = (root) => {
213526
- writeFileSync14(join18(root, "tsconfig.json"), JSON.stringify(TSCONFIG, null, 2));
213800
+ writeFileSync16(join21(root, "tsconfig.json"), JSON.stringify(TSCONFIG, null, 2));
213527
213801
  };
213528
213802
  var init_types4 = __esm(() => {
213529
213803
  init_api();
@@ -213549,14 +213823,14 @@ var init_types4 = __esm(() => {
213549
213823
  });
213550
213824
 
213551
213825
  // src/compile/designCompile.ts
213552
- import { readFileSync as readFileSync17 } from "node:fs";
213553
- import { basename as basename7 } from "node:path";
213826
+ import { readFileSync as readFileSync18 } from "node:fs";
213827
+ import { basename as basename8 } from "node:path";
213554
213828
  var LOCAL_ASSET_SCHEME = "lecodesfile://", compileDesignScreen = async (opts) => {
213555
213829
  const { root, manifest, designDir, designKey, screenId, state } = opts;
213556
213830
  const entries = [];
213557
213831
  const scan2 = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest?.files ?? {})) });
213558
213832
  for (const file of scan2.files) {
213559
- const type = manifest?.files[file.path]?.type ?? classifyKind(basename7(file.path));
213833
+ const type = manifest?.files[file.path]?.type ?? classifyKind(basename8(file.path));
213560
213834
  if (type === "resource") {
213561
213835
  entries.push({
213562
213836
  path: file.path,
@@ -213567,7 +213841,7 @@ var LOCAL_ASSET_SCHEME = "lecodesfile://", compileDesignScreen = async (opts) =>
213567
213841
  } else if (type === "shader") {
213568
213842
  entries.push({ path: file.path, type, shaders: [] });
213569
213843
  } else {
213570
- entries.push({ path: file.path, type, text: readFileSync17(file.absPath, "utf8") });
213844
+ entries.push({ path: file.path, type, text: readFileSync18(file.absPath, "utf8") });
213571
213845
  }
213572
213846
  }
213573
213847
  const invoke = state !== undefined ? `screen(${JSON.stringify(state)})` : "screen()";
@@ -213613,7 +213887,7 @@ var init_designCompile = __esm(() => {
213613
213887
  // src/browserAuth.ts
213614
213888
  import { createServer } from "node:http";
213615
213889
  import { spawn as spawn4 } from "node:child_process";
213616
- import { randomBytes } from "node:crypto";
213890
+ import { randomBytes as randomBytes2 } from "node:crypto";
213617
213891
  import { hostname } from "node:os";
213618
213892
  var SUCCESS_HTML = `<!doctype html><meta charset="utf-8"><title>lecodes</title>
213619
213893
  <body style="font-family:system-ui,sans-serif;background:#0f1115;color:#e6e6e6;display:grid;place-items:center;height:100vh;margin:0">
@@ -213642,11 +213916,11 @@ var SUCCESS_HTML = `<!doctype html><meta charset="utf-8"><title>lecodes</title>
213642
213916
  child.unref();
213643
213917
  } catch {}
213644
213918
  }, browserLogin = async (webBase, timeoutMs = 5 * 60 * 1000, open = openBrowser) => {
213645
- const state = randomBytes(16).toString("hex");
213646
- let resolve9;
213919
+ const state = randomBytes2(16).toString("hex");
213920
+ let resolve10;
213647
213921
  let reject;
213648
213922
  const tokenPromise = new Promise((res, rej) => {
213649
- resolve9 = res;
213923
+ resolve10 = res;
213650
213924
  reject = rej;
213651
213925
  });
213652
213926
  const server2 = createServer((req, res) => {
@@ -213664,7 +213938,7 @@ var SUCCESS_HTML = `<!doctype html><meta charset="utf-8"><title>lecodes</title>
213664
213938
  }
213665
213939
  res.writeHead(200, { "content-type": "text/html" });
213666
213940
  res.end(SUCCESS_HTML);
213667
- resolve9(token);
213941
+ resolve10(token);
213668
213942
  });
213669
213943
  await new Promise((res, rej) => {
213670
213944
  server2.once("error", rej);
@@ -214165,8 +214439,8 @@ __export(exports_design, {
214165
214439
  flushDesignComments: () => flushDesignComments,
214166
214440
  design: () => design
214167
214441
  });
214168
- import { existsSync as existsSync20, mkdirSync as mkdirSync15, readFileSync as readFileSync18, readdirSync as readdirSync11, writeFileSync as writeFileSync16, appendFileSync } from "node:fs";
214169
- import { basename as basename8, join as join20 } from "node:path";
214442
+ import { existsSync as existsSync23, mkdirSync as mkdirSync17, readFileSync as readFileSync19, readdirSync as readdirSync12, writeFileSync as writeFileSync18, appendFileSync } from "node:fs";
214443
+ import { basename as basename9, join as join23 } from "node:path";
214170
214444
  var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214171
214445
  let root;
214172
214446
  let manifest;
@@ -214181,13 +214455,13 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214181
214455
  return {
214182
214456
  root,
214183
214457
  manifest,
214184
- name: manifest?.name ?? basename8(root),
214458
+ name: manifest?.name ?? basename9(root),
214185
214459
  dirName,
214186
- designDir: join20(root, dirName),
214460
+ designDir: join23(root, dirName),
214187
214461
  designKey: "/" + dirName.replace(/\\/g, "/")
214188
214462
  };
214189
214463
  }, loadDesignServer = async (ctx, needs = []) => {
214190
- if (!existsSync20(ctx.designDir)) {
214464
+ if (!existsSync23(ctx.designDir)) {
214191
214465
  throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
214192
214466
  }
214193
214467
  let mod;
@@ -214219,13 +214493,13 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214219
214493
  `).find((l2) => l2.trim())?.trim() ?? "";
214220
214494
  return line.length > max ? line.slice(0, max - 1) + "…" : line;
214221
214495
  }, listScreens = (designDir) => {
214222
- const dir = join20(designDir, "screens");
214223
- if (!existsSync20(dir))
214496
+ const dir = join23(designDir, "screens");
214497
+ if (!existsSync23(dir))
214224
214498
  return [];
214225
- return readdirSync11(dir).filter((f3) => f3.endsWith(".ts") && !f3.startsWith(".")).map((f3) => f3.slice(0, -3)).sort();
214499
+ return readdirSync12(dir).filter((f3) => f3.endsWith(".ts") && !f3.startsWith(".")).map((f3) => f3.slice(0, -3)).sort();
214226
214500
  }, PHONE_SIZE, readMetaFrames = (designDir) => {
214227
214501
  try {
214228
- const meta = JSON.parse(readFileSync18(join20(designDir, "meta.json"), "utf8"));
214502
+ const meta = JSON.parse(readFileSync19(join23(designDir, "meta.json"), "utf8"));
214229
214503
  const d = meta?.defaultSize;
214230
214504
  const valid = Array.isArray(d) && d.length === 2 && d.every((n2) => typeof n2 === "number" && Number.isFinite(n2) && n2 >= 64 && n2 <= 8192);
214231
214505
  return {
@@ -214236,12 +214510,12 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214236
214510
  return { screens: {}, defaultSize: PHONE_SIZE };
214237
214511
  }
214238
214512
  }, writeMcpConfig = (ctx, port) => {
214239
- const path2 = join20(ctx.root, ".mcp.json");
214513
+ const path2 = join23(ctx.root, ".mcp.json");
214240
214514
  const entry = { type: "http", url: `http://localhost:${port}/mcp` };
214241
214515
  let config = {};
214242
- if (existsSync20(path2)) {
214516
+ if (existsSync23(path2)) {
214243
214517
  try {
214244
- config = JSON.parse(readFileSync18(path2, "utf8"));
214518
+ config = JSON.parse(readFileSync19(path2, "utf8"));
214245
214519
  } catch {
214246
214520
  warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-design server by hand.");
214247
214521
  return;
@@ -214254,7 +214528,7 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214254
214528
  return;
214255
214529
  }
214256
214530
  config.mcpServers["lecodes-design"] = entry;
214257
- writeFileSync16(path2, JSON.stringify(config, null, 2) + `
214531
+ writeFileSync18(path2, JSON.stringify(config, null, 2) + `
214258
214532
  `);
214259
214533
  log(`${c.green(existing ? "updated" : "created")} .mcp.json (lecodes-design MCP → ${entry.url})`);
214260
214534
  }, init = async (ctx, args) => {
@@ -214263,26 +214537,26 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214263
214537
  ["spec.md", SPEC_MD],
214264
214538
  ["README.md", README_HINT],
214265
214539
  ["CLAUDE.md", CLAUDE_MD],
214266
- [join20("shared", "tokens.ts"), TOKENS_TS],
214267
- [join20("shared", "tabs.ts"), TABS_TS],
214268
- [join20("screens", "home.ts"), HOME_SCREEN_TS]
214540
+ [join23("shared", "tokens.ts"), TOKENS_TS],
214541
+ [join23("shared", "tabs.ts"), TABS_TS],
214542
+ [join23("screens", "home.ts"), HOME_SCREEN_TS]
214269
214543
  ];
214270
- mkdirSync15(join20(ctx.designDir, "screens"), { recursive: true });
214271
- mkdirSync15(join20(ctx.designDir, "shared"), { recursive: true });
214544
+ mkdirSync17(join23(ctx.designDir, "screens"), { recursive: true });
214545
+ mkdirSync17(join23(ctx.designDir, "shared"), { recursive: true });
214272
214546
  let created = 0;
214273
214547
  for (const [rel, content] of files) {
214274
- const abs = join20(ctx.designDir, rel);
214275
- if (existsSync20(abs)) {
214548
+ const abs = join23(ctx.designDir, rel);
214549
+ if (existsSync23(abs)) {
214276
214550
  note(`kept ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
214277
214551
  continue;
214278
214552
  }
214279
- writeFileSync16(abs, content);
214553
+ writeFileSync18(abs, content);
214280
214554
  log(`${c.green("created")} ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
214281
214555
  created++;
214282
214556
  }
214283
- const ignorePath = join20(ctx.root, ".lecodesignore");
214557
+ const ignorePath = join23(ctx.root, ".lecodesignore");
214284
214558
  const ignoreLines = [`${ctx.dirName}/.snapshots/`, `${ctx.dirName}/comments/`];
214285
- const current = existsSync20(ignorePath) ? readFileSync18(ignorePath, "utf8") : "";
214559
+ const current = existsSync23(ignorePath) ? readFileSync19(ignorePath, "utf8") : "";
214286
214560
  const present = new Set(current.split(/\r?\n/));
214287
214561
  const missing = ignoreLines.filter((line) => !present.has(line));
214288
214562
  if (missing.length) {
@@ -214297,8 +214571,8 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214297
214571
  writeMcpConfig(ctx, Number(flagStr(args, "port")) || DEFAULT_PORT);
214298
214572
  const wroteTypes = materializeTypesLocal(ctx.designDir);
214299
214573
  writeTsconfig(ctx.designDir);
214300
- mkdirSync15(join20(ctx.designDir, ".lecodes", "types"), { recursive: true });
214301
- writeFileSync16(join20(ctx.designDir, ".lecodes", "types", "design-macros.d.ts"), DESIGN_MACROS_DTS);
214574
+ mkdirSync17(join23(ctx.designDir, ".lecodes", "types"), { recursive: true });
214575
+ writeFileSync18(join23(ctx.designDir, ".lecodes", "types", "design-macros.d.ts"), DESIGN_MACROS_DTS);
214302
214576
  log(`${c.green("created")} ${ctx.dirName}/tsconfig.json${wroteTypes ? ` + ${ctx.dirName}/.lecodes/types/` : ""} (IDE types)`);
214303
214577
  if (!wroteTypes)
214304
214578
  warnErr("Bundled SDK types not found — tsconfig written, but globals may show unresolved until the CLI is rebuilt with vendored types.");
@@ -214346,16 +214620,16 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214346
214620
  killer.unref?.();
214347
214621
  const png2 = flagBool(args, "png") || flagStr(args, "png") !== undefined;
214348
214622
  const frames = readMetaFrames(ctx.designDir);
214349
- const outDir = flagStr(args, "out-dir") ?? join20(ctx.designDir, ".snapshots");
214623
+ const outDir = flagStr(args, "out-dir") ?? join23(ctx.designDir, ".snapshots");
214350
214624
  const single = targets.length === 1 && requested.length === 1 && !png2 && !allStates && !flagStr(args, "out-dir");
214351
214625
  const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
214352
214626
  if (!single)
214353
- mkdirSync15(outDir, { recursive: true });
214627
+ mkdirSync17(outDir, { recursive: true });
214354
214628
  for (const id of targets) {
214355
214629
  const [width, height] = frames.screens[id]?.size ?? frames.defaultSize;
214356
214630
  let plan = [{ state: undefined, bases: [id] }];
214357
214631
  if (discover) {
214358
- const { states, canonical } = discover(readFileSync18(join20(ctx.designDir, "screens", `${id}.ts`), "utf8"));
214632
+ const { states, canonical } = discover(readFileSync19(join23(ctx.designDir, "screens", `${id}.ts`), "utf8"));
214359
214633
  if (stateFlag !== undefined) {
214360
214634
  if (!states.includes(stateFlag)) {
214361
214635
  if (requested.length === 1)
@@ -214396,11 +214670,11 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214396
214670
  for (const w of pngResult.warnings)
214397
214671
  warnErr(`${tag}: ${w}`);
214398
214672
  for (const base of bases) {
214399
- writeFileSync16(join20(outDir, `${base}.json`), result.text);
214400
- log(`${c.green("✓")} ${tag} → ${join20(outDir, `${base}.json`)}`);
214673
+ writeFileSync18(join23(outDir, `${base}.json`), result.text);
214674
+ log(`${c.green("✓")} ${tag} → ${join23(outDir, `${base}.json`)}`);
214401
214675
  if (pngResult) {
214402
- writeFileSync16(join20(outDir, `${base}.png`), pngResult.png);
214403
- log(`${c.green("✓")} ${tag} → ${join20(outDir, `${base}.png`)}`);
214676
+ writeFileSync18(join23(outDir, `${base}.png`), pngResult.png);
214677
+ log(`${c.green("✓")} ${tag} → ${join23(outDir, `${base}.png`)}`);
214404
214678
  }
214405
214679
  }
214406
214680
  }
@@ -214702,7 +214976,7 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214702
214976
  note("Read them with: lecodes design comments");
214703
214977
  }, flushDesignComments = async (args) => {
214704
214978
  const ctx = getContext(args);
214705
- if (!existsSync20(ctx.designDir))
214979
+ if (!existsSync23(ctx.designDir))
214706
214980
  return null;
214707
214981
  const platform = optionalPlatform(ctx);
214708
214982
  if (!platform)
@@ -215844,7 +216118,7 @@ var require_main = __commonJS((exports, module) => {
215844
216118
  // package.json
215845
216119
  var package_default = {
215846
216120
  name: "lecodes-cli",
215847
- version: "0.17.1",
216121
+ version: "0.18.0",
215848
216122
  dependencies: {
215849
216123
  "@letary/chisel": "^0.8.0",
215850
216124
  jimp: "^1.6.1"
@@ -215860,8 +216134,8 @@ var package_default = {
215860
216134
  },
215861
216135
  peerDependencies: {
215862
216136
  "lecodes-design": "^0.8.1",
215863
- "lecodes-renderer": "^0.8.6",
215864
- "lecodes-3d-editor": "^0.3.0",
216137
+ "lecodes-renderer": "^0.8.8",
216138
+ "lecodes-3d-editor": "^0.4.0",
215865
216139
  "lecodes-assets": "^0.1.0"
215866
216140
  },
215867
216141
  bin: {
@@ -215912,16 +216186,16 @@ var package_default = {
215912
216186
  init_manifest();
215913
216187
  init_config();
215914
216188
  import { execFileSync as execFileSync4 } from "node:child_process";
215915
- import { cpSync as cpSync3, existsSync as existsSync16, mkdirSync as mkdirSync11, readFileSync as readFileSync15, readdirSync as readdirSync7, rmSync as rmSync4, statSync as statSync5, writeFileSync as writeFileSync13 } from "node:fs";
215916
- import { homedir as homedir6 } from "node:os";
215917
- import { basename as basename6, dirname as dirname12, join as join17, resolve as resolve6 } from "node:path";
216189
+ import { cpSync as cpSync3, existsSync as existsSync19, mkdirSync as mkdirSync13, readFileSync as readFileSync16, readdirSync as readdirSync8, rmSync as rmSync6, statSync as statSync6, writeFileSync as writeFileSync15 } from "node:fs";
216190
+ import { homedir as homedir7 } from "node:os";
216191
+ import { basename as basename7, dirname as dirname12, join as join20, resolve as resolve7 } from "node:path";
215918
216192
 
215919
216193
  // src/commands/compile.ts
215920
216194
  init_api();
215921
216195
  init_config();
215922
216196
  init_manifest();
215923
- import { writeFileSync as writeFileSync8 } from "node:fs";
215924
- import { basename as basename3, resolve as resolve2 } from "node:path";
216197
+ import { writeFileSync as writeFileSync10 } from "node:fs";
216198
+ import { basename as basename4, resolve as resolve3 } from "node:path";
215925
216199
 
215926
216200
  // src/compile/collect.ts
215927
216201
  init_localFiles();
@@ -216011,7 +216285,7 @@ init_localFiles();
216011
216285
  init_ignore();
216012
216286
  import { readFileSync as readFileSync6 } from "node:fs";
216013
216287
  import { basename as basename2 } from "node:path";
216014
- var collectLocalEntries = (root, manifest, assetBase) => {
216288
+ var collectLocalEntries = (root, manifest, assetBase, opts = {}) => {
216015
216289
  const entries = [];
216016
216290
  const warnings = [];
216017
216291
  const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest?.files ?? {})) });
@@ -216020,8 +216294,10 @@ var collectLocalEntries = (root, manifest, assetBase) => {
216020
216294
  if (type === "resource") {
216021
216295
  entries.push({ path: file.path, type, fileSrc: assetBase + encodeURI(file.path), absPath: file.absPath });
216022
216296
  } else if (type === "shader") {
216023
- warnings.push(`${file.path}: shaders compile on the server — push the project once for shaders to work in dev mode.`);
216024
- entries.push({ path: file.path, type, shaders: [] });
216297
+ if (!opts.localShaders) {
216298
+ warnings.push(`${file.path}: shaders compile on the server — push the project once for shaders to work in dev mode.`);
216299
+ }
216300
+ entries.push({ path: file.path, type, shaders: [], absPath: file.absPath });
216025
216301
  } else {
216026
216302
  entries.push({ path: file.path, type, text: readFileSync6(file.absPath, "utf8") });
216027
216303
  }
@@ -216032,6 +216308,266 @@ var collectLocalEntries = (root, manifest, assetBase) => {
216032
216308
  // src/commands/compile.ts
216033
216309
  init_projectCompile();
216034
216310
  init_fonts();
216311
+
216312
+ // src/compile/shaders.ts
216313
+ init_compile();
216314
+ import { execFile } from "node:child_process";
216315
+ import { createHash as createHash4, randomBytes } from "node:crypto";
216316
+ import { copyFileSync, existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync10, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync7 } from "node:fs";
216317
+ import { tmpdir } from "node:os";
216318
+ import { basename as basename3, join as join13 } from "node:path";
216319
+
216320
+ // src/matcTool.ts
216321
+ import { existsSync as existsSync10, readdirSync as readdirSync4 } from "node:fs";
216322
+ import { homedir as homedir2 } from "node:os";
216323
+ import { join as join12, resolve } from "node:path";
216324
+
216325
+ // src/releases.ts
216326
+ init_util();
216327
+ import { createHash as createHash3 } from "node:crypto";
216328
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, mkdtempSync, renameSync, rmSync, writeFileSync as writeFileSync6 } from "node:fs";
216329
+ import { join as join11 } from "node:path";
216330
+ import { spawnSync as spawnSync2 } from "node:child_process";
216331
+ var MANIFEST_URL = "https://storage.yandexcloud.net/le-codes/releases/versions.json";
216332
+ var semverNewer = (a, b) => {
216333
+ const pa = a.split(/[.-]/).map(Number), pb = b.split(/[.-]/).map(Number);
216334
+ for (let i = 0;i < 3; i++)
216335
+ if ((pb[i] ?? 0) !== (pa[i] ?? 0))
216336
+ return (pb[i] ?? 0) - (pa[i] ?? 0);
216337
+ return 0;
216338
+ };
216339
+ var fetchReleaseProduct = async (product, label) => {
216340
+ let manifest;
216341
+ try {
216342
+ const res = await fetch(MANIFEST_URL);
216343
+ if (!res.ok)
216344
+ throw new Error(`HTTP ${res.status}`);
216345
+ manifest = await res.json();
216346
+ } catch (e) {
216347
+ throw new CliError(`Can't fetch the release manifest (${MANIFEST_URL}): ${e instanceof Error ? e.message : String(e)}`);
216348
+ }
216349
+ const entry = manifest.products?.[product];
216350
+ if (!entry)
216351
+ throw new CliError(`The release manifest has no ${label} product.`);
216352
+ return entry;
216353
+ };
216354
+ var downloadReleaseArchive = async (file, opts) => {
216355
+ let bytes;
216356
+ try {
216357
+ const res = await fetch(file.url);
216358
+ if (!res.ok)
216359
+ throw new Error(`HTTP ${res.status}`);
216360
+ bytes = Buffer.from(await res.arrayBuffer());
216361
+ } catch (e) {
216362
+ throw new CliError(`Download failed (${file.url}): ${e instanceof Error ? e.message : String(e)}`);
216363
+ }
216364
+ const sha = createHash3("sha256").update(bytes).digest("hex");
216365
+ if (sha !== file.sha256)
216366
+ throw new CliError(`Downloaded archive is corrupt (sha256 mismatch) — try again.`);
216367
+ mkdirSync5(opts.cacheRoot, { recursive: true });
216368
+ const stage = mkdtempSync(join11(opts.cacheRoot, "dl-"));
216369
+ try {
216370
+ const archiveName = file.url.split("/").pop() ?? "archive";
216371
+ const archive = join11(stage, archiveName);
216372
+ writeFileSync6(archive, bytes);
216373
+ const winTar = join11(process.env.SystemRoot ?? "C:\\Windows", "System32", "tar.exe");
216374
+ const tarExe = process.platform === "win32" && existsSync9(winTar) ? winTar : "tar";
216375
+ const tar = spawnSync2(tarExe, ["-xf", archiveName], { cwd: stage, stdio: ["ignore", "ignore", "pipe"] });
216376
+ if (tar.status !== 0)
216377
+ throw new CliError(`Couldn't extract the archive: ${tar.stderr?.toString() ?? "tar failed"}`);
216378
+ rmSync(archive);
216379
+ const bad = opts.validate(stage);
216380
+ if (bad)
216381
+ throw new CliError(bad);
216382
+ try {
216383
+ renameSync(stage, opts.dest);
216384
+ } catch {
216385
+ let destBad;
216386
+ try {
216387
+ destBad = opts.validate(opts.dest);
216388
+ } catch {
216389
+ destBad = "missing";
216390
+ }
216391
+ if (destBad)
216392
+ throw new CliError(`Couldn't move the download into ${opts.dest}.`);
216393
+ }
216394
+ } finally {
216395
+ rmSync(stage, { recursive: true, force: true });
216396
+ }
216397
+ };
216398
+
216399
+ // src/matcTool.ts
216400
+ init_util();
216401
+ var matcName = () => process.platform === "win32" ? "matc.exe" : "matc";
216402
+ var platformKey = () => {
216403
+ if (process.platform === "win32")
216404
+ return "windows-x64";
216405
+ if (process.platform === "linux")
216406
+ return "linux-x64";
216407
+ throw new CliError(process.platform === "darwin" ? "matc isn't published for macOS yet — push the project once so shaders compile on the server." : `matc isn't published for ${process.platform}.`);
216408
+ };
216409
+ var cacheRoot = () => join12(homedir2(), ".lecodes", "matc");
216410
+ var findMatc = (dir) => {
216411
+ const name = matcName();
216412
+ for (const entry of readdirSync4(dir, { withFileTypes: true })) {
216413
+ const p = join12(dir, entry.name);
216414
+ if (entry.isFile() && entry.name === name)
216415
+ return p;
216416
+ if (entry.isDirectory()) {
216417
+ const found = findMatc(p);
216418
+ if (found)
216419
+ return found;
216420
+ }
216421
+ }
216422
+ return null;
216423
+ };
216424
+ var installedMatcVersions = () => {
216425
+ try {
216426
+ return readdirSync4(cacheRoot(), { withFileTypes: true }).filter((e) => e.isDirectory() && e.name.startsWith("matc-")).map((e) => e.name.slice("matc-".length)).filter((v) => findMatc(join12(cacheRoot(), `matc-${v}`)) !== null).sort(semverNewer);
216427
+ } catch {
216428
+ return [];
216429
+ }
216430
+ };
216431
+ var fetchMatcManifest = () => fetchReleaseProduct("matc", "matc (shader compiler)");
216432
+ var downloadMatc = async (version, versions, dest) => {
216433
+ const key = platformKey();
216434
+ const file = versions.find((v) => v.version === version)?.files[key];
216435
+ if (!file) {
216436
+ throw new CliError(`matc ${version} [${key}] isn't published — push the project once so shaders compile on the ` + `server, or set LECODES_MATC to a locally built matc.`);
216437
+ }
216438
+ note(`Downloading the shader compiler (matc ${version}, ${(file.size / 1024 / 1024).toFixed(1)} MB)…`);
216439
+ await downloadReleaseArchive(file, {
216440
+ cacheRoot: cacheRoot(),
216441
+ dest,
216442
+ validate: (dir) => findMatc(dir) ? null : "The downloaded archive doesn't contain the matc binary."
216443
+ });
216444
+ };
216445
+ var resolveMatcExeCached = () => {
216446
+ const override = process.env.LECODES_MATC;
216447
+ if (override) {
216448
+ if (!existsSync10(override))
216449
+ throw new CliError(`LECODES_MATC points at "${override}" which doesn't exist.`);
216450
+ return resolve(override);
216451
+ }
216452
+ const cached = installedMatcVersions()[0];
216453
+ return cached ? findMatc(join12(cacheRoot(), `matc-${cached}`)) : null;
216454
+ };
216455
+ var resolveMatcExe = async () => {
216456
+ const cached = resolveMatcExeCached();
216457
+ if (cached)
216458
+ return cached;
216459
+ const { latest, versions } = await fetchMatcManifest();
216460
+ const dir = join12(cacheRoot(), `matc-${latest}`);
216461
+ await downloadMatc(latest, versions, dir);
216462
+ const exe = findMatc(dir);
216463
+ if (!exe)
216464
+ throw new CliError(`matc cache at ${dir} is broken — delete the folder and retry.`);
216465
+ return exe;
216466
+ };
216467
+ var updateMatc = async () => {
216468
+ const { latest, versions } = await fetchMatcManifest();
216469
+ const before = installedMatcVersions()[0];
216470
+ const dir = join12(cacheRoot(), `matc-${latest}`);
216471
+ if (existsSync10(dir) && findMatc(dir) !== null) {
216472
+ note(`Already up to date: matc ${latest}.`);
216473
+ return;
216474
+ }
216475
+ await downloadMatc(latest, versions, dir);
216476
+ note(before ? `matc updated to ${latest} (was ${before}).` : `matc ${latest} installed.`);
216477
+ };
216478
+
216479
+ // src/compile/shaders.ts
216480
+ init_manifest();
216481
+ init_util();
216482
+ var SHADERS_DIR = "shaders";
216483
+ var shadersCacheDir = (root) => join13(root, LECODES_DIR, SHADERS_DIR);
216484
+ var isShaderArtifactName = (name) => /^[0-9a-f]{16}_[a-z-]+\.filamat$/.test(name);
216485
+ var matcIdentity = (exe) => {
216486
+ const stat = statSync2(exe);
216487
+ return `${stat.size}:${Math.trunc(stat.mtimeMs)}`;
216488
+ };
216489
+ var shaderHash = (text, matcId) => createHash4("sha256").update(matcId + "\x00" + text).digest("hex").slice(0, 16);
216490
+ var artifactFamily = (text) => {
216491
+ const matcPlatforms = matcPlatformsFor(text);
216492
+ return [...matcPlatforms, ...matcPlatforms.includes("webgl") ? [] : ["webgl"], "webgl-lite"];
216493
+ };
216494
+ var shaderIsCompiled = (root, text, matcId) => {
216495
+ const hash = shaderHash(text, matcId);
216496
+ return artifactFamily(text).every((p) => existsSync11(join13(shadersCacheDir(root), `${hash}_${p}.filamat`)));
216497
+ };
216498
+ var runMatc = (exe, args) => new Promise((resolve2) => {
216499
+ execFile(exe, args, { windowsHide: true }, (error, _stdout, stderr) => {
216500
+ const text = String(stderr ?? "") || (error ? error.message : "");
216501
+ resolve2({ ok: !error, stderr: text });
216502
+ });
216503
+ });
216504
+ var cleanMatcOutput = (err, tempName, realName) => {
216505
+ const output = err.replaceAll("\r", "").replaceAll(tempName, realName);
216506
+ const cut = output.indexOf(`
216507
+
216508
+ `);
216509
+ return (cut >= 0 ? output.slice(0, cut) : output).trim();
216510
+ };
216511
+ var NATIVE_PLATFORMS = ["opengl", "metal", "desktop"];
216512
+ var applyLocalShaders = async (entries, opts) => {
216513
+ const shaderEntries = entries.filter((e) => e.type === "shader" && e.absPath && (opts.local || !e.shaders?.length));
216514
+ if (shaderEntries.length === 0)
216515
+ return { warnings: [], artifacts: [] };
216516
+ let matcExe;
216517
+ try {
216518
+ matcExe = await resolveMatcExe();
216519
+ } catch (e) {
216520
+ const reason = e instanceof CliError ? e.message : String(e);
216521
+ return { warnings: [`Local shader compile unavailable (${reason})
216522
+ Shaders fall back to the server — push the project once for them to work.`], artifacts: [] };
216523
+ }
216524
+ const matcId = matcIdentity(matcExe);
216525
+ const cacheDir = shadersCacheDir(opts.root);
216526
+ mkdirSync6(cacheDir, { recursive: true });
216527
+ const artifacts = [];
216528
+ for (const entry of shaderEntries) {
216529
+ const text = readFileSync10(entry.absPath, "utf8");
216530
+ const hash = shaderHash(text, matcId);
216531
+ const matcPlatforms = matcPlatformsFor(text);
216532
+ const artifact = (platform) => `${hash}_${platform}.filamat`;
216533
+ if (!artifactFamily(text).every((p) => existsSync11(join13(cacheDir, artifact(p))))) {
216534
+ const tempName = `lecodes-matc-${randomBytes(6).toString("hex")}.mat`;
216535
+ const tempFile = join13(tmpdir(), tempName);
216536
+ try {
216537
+ for (const platform of matcPlatforms) {
216538
+ const build = PLATFORM_BUILD[platform];
216539
+ writeFileSync7(tempFile, build.replaceExternal ? text.replace(externalSamplerReg, "type:sampler2d") : text);
216540
+ const out = join13(cacheDir, artifact(platform));
216541
+ const { ok, stderr } = await runMatc(matcExe, matcArgs(platform, out, tempFile));
216542
+ if (!ok) {
216543
+ rmSync2(out, { force: true });
216544
+ throw new CliError(`${entry.path}: shader compile failed [${platform}]
216545
+ ${cleanMatcOutput(stderr, tempName, basename3(entry.path))}`);
216546
+ }
216547
+ }
216548
+ } finally {
216549
+ rmSync2(tempFile, { force: true });
216550
+ }
216551
+ if (!matcPlatforms.includes("webgl")) {
216552
+ copyFileSync(join13(cacheDir, artifact("opengl")), join13(cacheDir, artifact("webgl")));
216553
+ }
216554
+ writeFileSync7(join13(cacheDir, artifact("webgl-lite")), JSON.stringify(buildLiteMaterialArtifact(text)));
216555
+ }
216556
+ if (opts.local) {
216557
+ entry.shaders = NATIVE_PLATFORMS.map((platform) => ({ platform, src: artifact(platform) }));
216558
+ entry.shadersLocal = true;
216559
+ for (const platform of NATIVE_PLATFORMS) {
216560
+ artifacts.push({ name: artifact(platform), absPath: join13(cacheDir, artifact(platform)) });
216561
+ }
216562
+ } else {
216563
+ const order = ["opengl", "metal", "desktop", "webgl", "webgl-lite"];
216564
+ entry.shaders = order.map((platform) => ({ platform, src: `${opts.srcPrefix}/${artifact(platform)}` }));
216565
+ }
216566
+ }
216567
+ return { warnings: [], artifacts };
216568
+ };
216569
+
216570
+ // src/commands/compile.ts
216035
216571
  init_assetIcons();
216036
216572
  init_ignore();
216037
216573
 
@@ -216040,16 +216576,16 @@ init_manifest();
216040
216576
  init_ignore();
216041
216577
  init_util();
216042
216578
  import { execFileSync } from "node:child_process";
216043
- import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "node:fs";
216044
- import { homedir as homedir3 } from "node:os";
216045
- import { dirname as dirname8, join as join12, resolve } from "node:path";
216579
+ import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "node:fs";
216580
+ import { homedir as homedir4 } from "node:os";
216581
+ import { dirname as dirname8, join as join15, resolve as resolve2 } from "node:path";
216046
216582
  var FIRST_PARTY_PLUGINS = {
216047
216583
  camera: "letary/lecodes-plugins",
216048
216584
  "qr-scanner": "letary/lecodes-plugins",
216049
216585
  geolocation: "letary/lecodes-plugins",
216050
216586
  push: "letary/lecodes-plugins"
216051
216587
  };
216052
- var readAppConfigAt = (root) => readJson(join12(root, "app.json"));
216588
+ var readAppConfigAt = (root) => readJson(join15(root, "app.json"));
216053
216589
  var DESKTOP_DEFAULT_WINDOW = { width: 1280, height: 720 };
216054
216590
  var desktopDefaultViewport = (root) => {
216055
216591
  const cfg = readAppConfigAt(root);
@@ -216061,15 +216597,15 @@ var desktopDefaultViewport = (root) => {
216061
216597
  };
216062
216598
  };
216063
216599
  var readJson = (path) => {
216064
- if (!existsSync10(path))
216600
+ if (!existsSync13(path))
216065
216601
  return null;
216066
216602
  try {
216067
- return JSON.parse(readFileSync11(path, "utf8").replace(/^\uFEFF/, ""));
216603
+ return JSON.parse(readFileSync12(path, "utf8").replace(/^\uFEFF/, ""));
216068
216604
  } catch {
216069
216605
  throw new CliError(`Invalid JSON in ${path}`);
216070
216606
  }
216071
216607
  };
216072
- var writeJson = (path, value) => writeFileSync7(path, JSON.stringify(value, null, 2) + `
216608
+ var writeJson = (path, value) => writeFileSync9(path, JSON.stringify(value, null, 2) + `
216073
216609
  `);
216074
216610
  var projectRoot = () => {
216075
216611
  try {
@@ -216094,12 +216630,12 @@ ${tail}`);
216094
216630
  }
216095
216631
  };
216096
216632
  var ensureShellIgnored = (root, dir) => {
216097
- const ignorePath = join12(root, ".lecodesignore");
216098
- const ignore = existsSync10(ignorePath) ? readFileSync11(ignorePath, "utf8") : DEFAULT_IGNORE_FILE;
216633
+ const ignorePath = join15(root, ".lecodesignore");
216634
+ const ignore = existsSync13(ignorePath) ? readFileSync12(ignorePath, "utf8") : DEFAULT_IGNORE_FILE;
216099
216635
  if (new RegExp(`^${dir}\\/$`, "m").test(ignore))
216100
216636
  return;
216101
216637
  const comment = ignore.includes("# Native app shells") ? "" : "# Native app shells — generated by `lecodes app init`, never pushed to LeCodes.\n";
216102
- writeFileSync7(ignorePath, ignore.replace(/\n*$/, `
216638
+ writeFileSync9(ignorePath, ignore.replace(/\n*$/, `
216103
216639
  `) + `
216104
216640
  ` + comment + `${dir}/
216105
216641
  `);
@@ -216175,12 +216711,12 @@ var ensureRepo = (repo, wantTag, lock) => {
216175
216711
  const override = process.env.LECODES_PLUGINS_DIR;
216176
216712
  if (override) {
216177
216713
  note(`Using local plugin checkout ${override} (LECODES_PLUGINS_DIR)`);
216178
- return { dir: resolve(override), tag: "local", commit: "local" };
216714
+ return { dir: resolve2(override), tag: "local", commit: "local" };
216179
216715
  }
216180
- const cache = join12(homedir3(), ".lecodes", "plugins", repo.replace("/", "__"));
216716
+ const cache = join15(homedir4(), ".lecodes", "plugins", repo.replace("/", "__"));
216181
216717
  const url = `https://github.com/${repo}.git`;
216182
- if (!existsSync10(join12(cache, ".git"))) {
216183
- mkdirSync6(dirname8(cache), { recursive: true });
216718
+ if (!existsSync13(join15(cache, ".git"))) {
216719
+ mkdirSync8(dirname8(cache), { recursive: true });
216184
216720
  note(`Cloning ${url}…`);
216185
216721
  git(dirname8(cache), "clone", "--quiet", url, cache);
216186
216722
  }
@@ -216222,8 +216758,8 @@ var resolvePlugin = (id, spec, lock, platform) => {
216222
216758
  subdir = `plugins/${id}`;
216223
216759
  }
216224
216760
  const checkout = ensureRepo(repo, wantTag, lock?.repo === repo ? lock : undefined);
216225
- const dir = subdir ? join12(checkout.dir, subdir) : checkout.dir;
216226
- const manifest = readJson(join12(dir, "lecodes-plugin.json"));
216761
+ const dir = subdir ? join15(checkout.dir, subdir) : checkout.dir;
216762
+ const manifest = readJson(join15(dir, "lecodes-plugin.json"));
216227
216763
  if (!manifest)
216228
216764
  throw new CliError(`Plugin "${id}": no lecodes-plugin.json in ${dir}`);
216229
216765
  if (manifest.id !== id)
@@ -216265,7 +216801,7 @@ var compileImpl = async (args, shell) => {
216265
216801
  if (out && isNullSink(out)) {
216266
216802
  note(`Discarded output (${out})`);
216267
216803
  } else if (out) {
216268
- writeFileSync8(resolve2(out), js);
216804
+ writeFileSync10(resolve3(out), js);
216269
216805
  note(`Wrote ${out} (${js.length} bytes)`);
216270
216806
  } else {
216271
216807
  process.stdout.write(js);
@@ -216322,12 +216858,18 @@ var buildBundle = async (args, shell, opts = {}) => {
216322
216858
  if (opts.localAssets && flagBool(args, "remote-assets")) {
216323
216859
  warnErr("This is a local project (no server) — assets always load from the working tree.");
216324
216860
  }
216325
- ({ entries, warnings } = collectLocalEntries(root, null, ""));
216326
- name = basename3(root);
216861
+ ({ entries, warnings } = collectLocalEntries(root, null, "", { localShaders: localAssets && (opts.localShaders ?? !shell) }));
216862
+ name = basename4(root);
216327
216863
  if (shell && shell.remotePatterns.length > 0) {
216328
216864
  warnErr("resources.remote needs a server-backed project (a streamed asset needs a server URL) — bundling everything.");
216329
216865
  }
216330
216866
  }
216867
+ let stagedShaders = [];
216868
+ if (localAssets && (opts.localShaders ?? !shell)) {
216869
+ const local = await applyLocalShaders(entries, { root, local: true });
216870
+ warnings.push(...local.warnings);
216871
+ stagedShaders = local.artifacts;
216872
+ }
216331
216873
  const fontsHeader = await applyProjectFonts(entries, manifest && !localAssets ? { mode: "cdn", root, publicUrl } : {
216332
216874
  mode: "vendor",
216333
216875
  root,
@@ -216364,6 +216906,9 @@ var buildBundle = async (args, shell, opts = {}) => {
216364
216906
  absPath: e.absPath,
216365
216907
  remote: !!e.remote
216366
216908
  }));
216909
+ for (const artifact of stagedShaders) {
216910
+ resources.push({ path: artifact.name, name: artifact.name, absPath: artifact.absPath, remote: false, shader: true });
216911
+ }
216367
216912
  return { js, resources };
216368
216913
  };
216369
216914
  var previewLegacy = async (args) => {
@@ -217213,17 +217758,17 @@ buildNumber) — edit it there, then re-run \`lecodes app sync\`. Plugins are ma
217213
217758
 
217214
217759
  // src/commands/appAndroid.ts
217215
217760
  init_manifest();
217216
- import { execFileSync as execFileSync2, spawn, spawnSync as spawnSync2 } from "node:child_process";
217217
- import { createHash as createHash4 } from "node:crypto";
217218
- import { chmodSync, cpSync, existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync13, readdirSync as readdirSync4, renameSync, rmSync, statSync as statSync2, writeFileSync as writeFileSync10 } from "node:fs";
217219
- import { homedir as homedir4 } from "node:os";
217220
- import { basename as basename4, dirname as dirname10, join as join14 } from "node:path";
217761
+ import { execFileSync as execFileSync2, spawn, spawnSync as spawnSync3 } from "node:child_process";
217762
+ import { createHash as createHash6 } from "node:crypto";
217763
+ import { chmodSync, cpSync, existsSync as existsSync16, mkdirSync as mkdirSync10, readFileSync as readFileSync14, readdirSync as readdirSync5, renameSync as renameSync2, rmSync as rmSync3, statSync as statSync3, writeFileSync as writeFileSync12 } from "node:fs";
217764
+ import { homedir as homedir5 } from "node:os";
217765
+ import { basename as basename5, dirname as dirname10, join as join17 } from "node:path";
217221
217766
  init_util();
217222
217767
 
217223
217768
  // src/commands/appIcon.ts
217224
217769
  init_util();
217225
- import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "node:fs";
217226
- import { dirname as dirname9, join as join13, resolve as resolve3 } from "node:path";
217770
+ import { existsSync as existsSync15, mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync11 } from "node:fs";
217771
+ import { dirname as dirname9, join as join16, resolve as resolve4 } from "node:path";
217227
217772
  var MASTER_PX = 1024;
217228
217773
  var DENSITIES = [
217229
217774
  { qualifier: "mdpi", scale: 1 },
@@ -217234,8 +217779,8 @@ var DENSITIES = [
217234
217779
  ];
217235
217780
  var loadIconMaster = async (root, config) => {
217236
217781
  const rel = config.icon;
217237
- const path2 = resolve3(root, rel);
217238
- if (!existsSync12(path2))
217782
+ const path2 = resolve4(root, rel);
217783
+ if (!existsSync15(path2))
217239
217784
  throw new CliError(`app.json "icon": ${rel} not found`);
217240
217785
  const raw = (config.iconBackground ?? "#FFFFFF").trim();
217241
217786
  const expanded = /^#[0-9a-fA-F]{3}$/.test(raw) ? "#" + [...raw.slice(1)].map((ch) => ch + ch).join("") : raw;
@@ -217299,12 +217844,12 @@ var IOS_CONTENTS_JSON = `{
217299
217844
  `;
217300
217845
  var writeIosAppIcon = async (iosDir, master) => {
217301
217846
  const { Jimp: Jimp2 } = await Promise.resolve().then(() => (init_esm30(), exports_esm2));
217302
- const dir = join13(iosDir, "App", "Assets.xcassets", "AppIcon.appiconset");
217303
- mkdirSync7(dir, { recursive: true });
217847
+ const dir = join16(iosDir, "App", "Assets.xcassets", "AppIcon.appiconset");
217848
+ mkdirSync9(dir, { recursive: true });
217304
217849
  const flat = new Jimp2({ width: MASTER_PX, height: MASTER_PX, color: master.background });
217305
217850
  flat.composite(master.image, 0, 0);
217306
- writeFileSync9(join13(dir, "AppIcon.png"), await flat.getBuffer("image/png", { colorType: 2 }));
217307
- writeFileSync9(join13(dir, "Contents.json"), IOS_CONTENTS_JSON);
217851
+ writeFileSync11(join16(dir, "AppIcon.png"), await flat.getBuffer("image/png", { colorType: 2 }));
217852
+ writeFileSync11(join16(dir, "Contents.json"), IOS_CONTENTS_JSON);
217308
217853
  log(`${c.green("icon")} AppIcon.png (${MASTER_PX}×${MASTER_PX}) → ios/App/Assets.xcassets/AppIcon.appiconset/`);
217309
217854
  };
217310
217855
  var ADAPTIVE_ICON_XML = `<?xml version="1.0" encoding="utf-8"?>
@@ -217323,11 +217868,11 @@ var iconColorXml = (hex) => `<?xml version="1.0" encoding="utf-8"?>
217323
217868
  `;
217324
217869
  var writeAndroidIcons = async (androidDir, master) => {
217325
217870
  const { Jimp: Jimp2 } = await Promise.resolve().then(() => (init_esm30(), exports_esm2));
217326
- const resDir = join13(androidDir, "app", "src", "main", "res");
217871
+ const resDir = join16(androidDir, "app", "src", "main", "res");
217327
217872
  const writeRes = (rel, content) => {
217328
- const abs = join13(resDir, rel);
217329
- mkdirSync7(dirname9(abs), { recursive: true });
217330
- writeFileSync9(abs, content);
217873
+ const abs = join16(resDir, rel);
217874
+ mkdirSync9(dirname9(abs), { recursive: true });
217875
+ writeFileSync11(abs, content);
217331
217876
  };
217332
217877
  const legacy = new Jimp2({ width: MASTER_PX, height: MASTER_PX, color: master.background });
217333
217878
  legacy.composite(master.image, 0, 0);
@@ -217349,17 +217894,17 @@ var writeAndroidIcons = async (androidDir, master) => {
217349
217894
  log(`${c.green("icon")} adaptive + legacy launcher icons → android/app/src/main/res/mipmap-*/`);
217350
217895
  };
217351
217896
  var restampManifestIcon = (androidDir) => {
217352
- const path2 = join13(androidDir, "app", "src", "main", "AndroidManifest.xml");
217353
- if (!existsSync12(path2))
217897
+ const path2 = join16(androidDir, "app", "src", "main", "AndroidManifest.xml");
217898
+ if (!existsSync15(path2))
217354
217899
  return;
217355
- const content = readFileSync12(path2, "utf8");
217900
+ const content = readFileSync13(path2, "utf8");
217356
217901
  if (content.includes(`android:icon="@mipmap/ic_launcher"`))
217357
217902
  return;
217358
217903
  if (!/android:icon="[^"]*"/.test(content)) {
217359
217904
  warn(`couldn't find android:icon in app/src/main/AndroidManifest.xml — set it to "@mipmap/ic_launcher" yourself.`);
217360
217905
  return;
217361
217906
  }
217362
- writeFileSync9(path2, content.replace(/android:icon="[^"]*"/, `android:icon="@mipmap/ic_launcher"`));
217907
+ writeFileSync11(path2, content.replace(/android:icon="[^"]*"/, `android:icon="@mipmap/ic_launcher"`));
217363
217908
  log(`${c.green("updated")} android/app/src/main/AndroidManifest.xml: android:icon → @mipmap/ic_launcher`);
217364
217909
  };
217365
217910
 
@@ -218269,42 +218814,42 @@ var ANDROID_SDK_URL_DEFAULT = `https://github.com/${ANDROID_SDK_REPO}/releases/d
218269
218814
  var sdkRepoUrl = () => process.env.LECODES_ANDROID_SDK_URL ?? ANDROID_SDK_URL_DEFAULT;
218270
218815
  var ANDROID_SDK_OPTS = { repo: ANDROID_SDK_REPO, minVersion: ANDROID_SDK_MIN_VERSION, configKey: "android.sdk" };
218271
218816
  var shellSdkVersionAndroid = (androidDir) => {
218272
- const path2 = join14(androidDir, "lecodes-runtime", "build.gradle.kts");
218273
- if (!existsSync13(path2))
218817
+ const path2 = join17(androidDir, "lecodes-runtime", "build.gradle.kts");
218818
+ if (!existsSync16(path2))
218274
218819
  return null;
218275
- const m = /io\.letary:lecodes-sdk(?:-(?:core|2d|3d))?:(\d+\.\d+\.\d+)@aar/.exec(readFileSync13(path2, "utf8"));
218820
+ const m = /io\.letary:lecodes-sdk(?:-(?:core|2d|3d))?:(\d+\.\d+\.\d+)@aar/.exec(readFileSync14(path2, "utf8"));
218276
218821
  return m ? m[1] : null;
218277
218822
  };
218278
218823
  var GRADLE_WRAPPER_JAR_URL = "https://raw.githubusercontent.com/gradle/gradle/v8.13.0/gradle/wrapper/gradle-wrapper.jar";
218279
218824
  var GRADLE_WRAPPER_JAR_SHA256 = "81a82aaea5abcc8ff68b3dfcb58b3c3c429378efd98e7433460610fecd7ae45f";
218280
218825
  var ensureGradleWrapperJar = async () => {
218281
- const cached = join14(homedir4(), ".lecodes", "gradle-wrapper", GRADLE_VERSION, "gradle-wrapper.jar");
218282
- if (existsSync13(cached))
218283
- return new Uint8Array(readFileSync13(cached));
218826
+ const cached = join17(homedir5(), ".lecodes", "gradle-wrapper", GRADLE_VERSION, "gradle-wrapper.jar");
218827
+ if (existsSync16(cached))
218828
+ return new Uint8Array(readFileSync14(cached));
218284
218829
  try {
218285
218830
  const res = await fetch(GRADLE_WRAPPER_JAR_URL);
218286
218831
  if (!res.ok)
218287
218832
  throw new Error(`HTTP ${res.status}`);
218288
218833
  const bytes = new Uint8Array(await res.arrayBuffer());
218289
- const hash = createHash4("sha256").update(bytes).digest("hex");
218834
+ const hash = createHash6("sha256").update(bytes).digest("hex");
218290
218835
  if (hash !== GRADLE_WRAPPER_JAR_SHA256)
218291
218836
  throw new Error(`sha256 mismatch (${hash})`);
218292
- mkdirSync8(dirname10(cached), { recursive: true });
218293
- writeFileSync10(cached, bytes);
218837
+ mkdirSync10(dirname10(cached), { recursive: true });
218838
+ writeFileSync12(cached, bytes);
218294
218839
  return bytes;
218295
218840
  } catch (e2) {
218296
218841
  warn(`Could not fetch gradle-wrapper.jar (${e2.message}) — run \`gradle wrapper --gradle-version ${GRADLE_VERSION}\` inside android/, or open the project in Android Studio (it offers to repair the wrapper).`);
218297
218842
  return null;
218298
218843
  }
218299
218844
  };
218300
- var rmrf = (path2) => rmSync(path2, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 });
218845
+ var rmrf = (path2) => rmSync3(path2, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 });
218301
218846
  var initAndroid = async (args) => {
218302
218847
  const root = projectRoot();
218303
- const androidDir = join14(root, "android");
218304
- if (existsSync13(androidDir)) {
218848
+ const androidDir = join17(root, "android");
218849
+ if (existsSync16(androidDir)) {
218305
218850
  throw new CliError("android/ already exists — run `lecodes app sync` to update it.");
218306
218851
  }
218307
- const existing = readJson(join14(root, "app.json")) ?? {};
218852
+ const existing = readJson(join17(root, "app.json")) ?? {};
218308
218853
  const variant = flagStr(args, "variant") ?? existing.android?.variant ?? "auto";
218309
218854
  if (variant !== "auto" && !SDK_VARIANTS.includes(variant)) {
218310
218855
  throw new CliError(`--variant must be auto, ${SDK_VARIANTS.join(", ")} (got "${variant}")`);
@@ -218312,7 +218857,7 @@ var initAndroid = async (args) => {
218312
218857
  const sdkFlag = flagStr(args, "sdk") ?? existing.android?.sdk;
218313
218858
  const sdkVersion = await resolveSdkVersion(process.env.LECODES_ANDROID_SDK ?? sdkFlag ?? "latest", null, ANDROID_SDK_OPTS);
218314
218859
  const sdk = sdkFlag === "latest" ? "latest" : sdkVersion;
218315
- const name = flagStr(args, "name") ?? existing.name ?? basename4(root);
218860
+ const name = flagStr(args, "name") ?? existing.name ?? basename5(root);
218316
218861
  const defaultAppId = `com.example.${slugify(name).replace(/-/g, "")}`;
218317
218862
  const config = {
218318
218863
  ...existing,
@@ -218327,7 +218872,7 @@ var initAndroid = async (args) => {
218327
218872
  ...existing.android?.signing ? { signing: existing.android.signing } : {}
218328
218873
  }
218329
218874
  };
218330
- writeJson(join14(root, "app.json"), config);
218875
+ writeJson(join17(root, "app.json"), config);
218331
218876
  log(`${c.green("written")} app.json`);
218332
218877
  ensureShellIgnored(root, "android");
218333
218878
  const identity = {
@@ -218338,9 +218883,9 @@ var initAndroid = async (args) => {
218338
218883
  };
218339
218884
  const pkgPath = identity.applicationId.replace(/\./g, "/");
218340
218885
  const write = (rel, content) => {
218341
- const abs = join14(androidDir, rel);
218342
- mkdirSync8(dirname10(abs), { recursive: true });
218343
- writeFileSync10(abs, content);
218886
+ const abs = join17(androidDir, rel);
218887
+ mkdirSync10(dirname10(abs), { recursive: true });
218888
+ writeFileSync12(abs, content);
218344
218889
  log(`${c.green("created")} android/${rel}`);
218345
218890
  };
218346
218891
  write("README.md", ANDROID_README);
@@ -218353,7 +218898,7 @@ var initAndroid = async (args) => {
218353
218898
  write("gradlew.bat", GRADLEW_BAT.replace(/\n/g, `\r
218354
218899
  `));
218355
218900
  if (process.platform !== "win32")
218356
- chmodSync(join14(androidDir, "gradlew"), 493);
218901
+ chmodSync(join17(androidDir, "gradlew"), 493);
218357
218902
  const wrapperJar = await ensureGradleWrapperJar();
218358
218903
  if (wrapperJar)
218359
218904
  write("gradle/wrapper/gradle-wrapper.jar", wrapperJar);
@@ -218388,7 +218933,7 @@ var resolveGradlePlugin = (pluginId, entry) => {
218388
218933
  return { id: entry, version };
218389
218934
  };
218390
218935
  var syncAndroid = async (root, config, prior) => {
218391
- const androidDir = join14(root, "android");
218936
+ const androidDir = join17(root, "android");
218392
218937
  const linkedSdk = shellSdkVersionAndroid(androidDir);
218393
218938
  const sdkPin = process.env.LECODES_ANDROID_SDK ?? config.android?.sdk ?? linkedSdk ?? "latest";
218394
218939
  const sdkVersion = await resolveSdkVersion(sdkPin, linkedSdk, ANDROID_SDK_OPTS);
@@ -218396,18 +218941,18 @@ var syncAndroid = async (root, config, prior) => {
218396
218941
  if (!Array.isArray(remotePatterns) || remotePatterns.some((p2) => typeof p2 !== "string")) {
218397
218942
  throw new CliError('app.json "resources.remote" must be an array of glob strings (e.g. ["videos/**"]).');
218398
218943
  }
218399
- const assetsDir = join14(androidDir, "app", "src", "main", "assets");
218944
+ const assetsDir = join17(androidDir, "app", "src", "main", "assets");
218400
218945
  rmrf(assetsDir);
218401
- mkdirSync8(assetsDir, { recursive: true });
218946
+ mkdirSync10(assetsDir, { recursive: true });
218402
218947
  let bundleJs;
218403
218948
  let resources;
218404
218949
  if (prior) {
218405
- writeFileSync10(join14(assetsDir, "app.js"), prior.bundleJs);
218950
+ writeFileSync12(join17(assetsDir, "app.js"), prior.bundleJs);
218406
218951
  bundleJs = prior.bundleJs;
218407
218952
  resources = prior.resources;
218408
218953
  } else {
218409
- resources = await compileShell({ _: [], flags: { o: join14(assetsDir, "app.js") } }, { remotePatterns });
218410
- bundleJs = readFileSync13(join14(assetsDir, "app.js"), "utf8");
218954
+ resources = await compileShell({ _: [], flags: { o: join17(assetsDir, "app.js") } }, { remotePatterns });
218955
+ bundleJs = readFileSync14(join17(assetsDir, "app.js"), "utf8");
218411
218956
  }
218412
218957
  const requestedVariant = config.android?.variant ?? "auto";
218413
218958
  if (requestedVariant !== "auto" && !SDK_VARIANTS.includes(requestedVariant)) {
@@ -218426,14 +218971,14 @@ var syncAndroid = async (root, config, prior) => {
218426
218971
  core: "no 3D/2D engine use detected"
218427
218972
  };
218428
218973
  log(`${c.green("variant")} ${variant} → ${sdkArtifact}${requestedVariant === "auto" ? c.dim(` (auto: ${WHY[detected]})`) : c.dim(" (forced by android.variant)")}`);
218429
- const lockPath = join14(root, "plugins.lock.json");
218974
+ const lockPath = join17(root, "plugins.lock.json");
218430
218975
  const oldLock = readJson(lockPath)?.plugins ?? {};
218431
218976
  const pluginSpecs = Object.entries(config.plugins ?? {});
218432
218977
  const runtimePlugins = [];
218433
218978
  const gradlePlugins = new Map;
218434
218979
  const aarWrapperPaths = [];
218435
218980
  const newLock = {};
218436
- const pluginsDir = join14(androidDir, "plugins");
218981
+ const pluginsDir = join17(androidDir, "plugins");
218437
218982
  for (const [id, spec] of pluginSpecs) {
218438
218983
  const plugin = resolvePlugin(id, spec, oldLock[id], "android");
218439
218984
  const android = plugin.manifest.android;
@@ -218441,10 +218986,10 @@ var syncAndroid = async (root, config, prior) => {
218441
218986
  const register = android.register;
218442
218987
  if (!module || !register)
218443
218988
  throw new CliError(`Plugin "${id}": manifest android.module / android.register missing`);
218444
- const src = join14(plugin.dir, android.sources ?? "android");
218445
- if (!existsSync13(src))
218989
+ const src = join17(plugin.dir, android.sources ?? "android");
218990
+ if (!existsSync16(src))
218446
218991
  throw new CliError(`Plugin "${id}": sources dir ${src} missing`);
218447
- const dest = join14(pluginsDir, module);
218992
+ const dest = join17(pluginsDir, module);
218448
218993
  rmrf(dest);
218449
218994
  cpSync(src, dest, { recursive: true });
218450
218995
  if (android.minSdk && VERSION_RE.test(android.minSdk) && cmpVersion(sdkVersion, android.minSdk) < 0) {
@@ -218455,26 +219000,26 @@ var syncAndroid = async (root, config, prior) => {
218455
219000
  gradlePlugins.set(resolved.id, resolved.version);
218456
219001
  }
218457
219002
  const namespace = register.split(".").slice(0, -1).join(".");
218458
- const libsDir = join14(dest, "libs");
218459
- const aarNames = existsSync13(libsDir) ? readdirSync4(libsDir).filter((f3) => f3.endsWith(".aar")) : [];
219003
+ const libsDir = join17(dest, "libs");
219004
+ const aarNames = existsSync16(libsDir) ? readdirSync5(libsDir).filter((f3) => f3.endsWith(".aar")) : [];
218460
219005
  for (const aar of aarNames) {
218461
219006
  const base = aar.replace(/\.aar$/, "");
218462
- const wrapDir = join14(libsDir, base);
218463
- mkdirSync8(wrapDir, { recursive: true });
218464
- renameSync(join14(libsDir, aar), join14(wrapDir, aar));
218465
- writeFileSync10(join14(wrapDir, "build.gradle.kts"), aarWrapperBuildGradle(aar));
219007
+ const wrapDir = join17(libsDir, base);
219008
+ mkdirSync10(wrapDir, { recursive: true });
219009
+ renameSync2(join17(libsDir, aar), join17(wrapDir, aar));
219010
+ writeFileSync12(join17(wrapDir, "build.gradle.kts"), aarWrapperBuildGradle(aar));
218466
219011
  aarWrapperPaths.push(`:plugins:${module}:libs:${base}`);
218467
219012
  }
218468
- const hasConsumerRules = existsSync13(join14(dest, "consumer-rules.pro"));
218469
- writeFileSync10(join14(dest, "build.gradle.kts"), pluginBuildGradleTemplate(id, module, namespace, sdkVersion, android.dependencies ?? [], aarNames, hasConsumerRules, sdkArtifact));
219013
+ const hasConsumerRules = existsSync16(join17(dest, "consumer-rules.pro"));
219014
+ writeFileSync12(join17(dest, "build.gradle.kts"), pluginBuildGradleTemplate(id, module, namespace, sdkVersion, android.dependencies ?? [], aarNames, hasConsumerRules, sdkArtifact));
218470
219015
  runtimePlugins.push({ id, module, register });
218471
219016
  newLock[id] = plugin.lock;
218472
219017
  log(`${c.green("plugin")} ${id}@${plugin.lock.version} (${plugin.lock.tag} → plugins/${module})`);
218473
219018
  }
218474
219019
  const keep = new Set(runtimePlugins.map((p2) => p2.module));
218475
- for (const entry of existsSync13(pluginsDir) ? readdirSync4(pluginsDir) : []) {
218476
- if (!keep.has(entry) && statSync2(join14(pluginsDir, entry)).isDirectory()) {
218477
- rmrf(join14(pluginsDir, entry));
219020
+ for (const entry of existsSync16(pluginsDir) ? readdirSync5(pluginsDir) : []) {
219021
+ if (!keep.has(entry) && statSync3(join17(pluginsDir, entry)).isDirectory()) {
219022
+ rmrf(join17(pluginsDir, entry));
218478
219023
  log(`${c.yellow("removed")} android/plugins/${entry} (plugin uninstalled)`);
218479
219024
  }
218480
219025
  }
@@ -218501,7 +219046,7 @@ var syncAndroid = async (root, config, prior) => {
218501
219046
  }
218502
219047
  if (candidates.length > 1)
218503
219048
  warn(`asset name collision: ${name} (${candidates.map((r2) => r2.path).join(", ")}) — embedding ${candidates[0].path}`);
218504
- cpSync(candidates[0].absPath, join14(assetsDir, name));
219049
+ cpSync(candidates[0].absPath, join17(assetsDir, name));
218505
219050
  embedded++;
218506
219051
  }
218507
219052
  if (embedded > 0)
@@ -218516,15 +219061,15 @@ var syncAndroid = async (root, config, prior) => {
218516
219061
  if (shellUuid)
218517
219062
  log(`${c.green("identity")} project ${shellUuid} baked into the shell (world identity)`);
218518
219063
  const modules = runtimePlugins.map((p2) => p2.module);
218519
- writeFileSync10(join14(androidDir, "README.md"), ANDROID_README);
218520
- writeFileSync10(join14(androidDir, "settings.gradle.kts"), settingsGradleTemplate(slugify(config.name ?? basename4(root)), modules, aarWrapperPaths, sdkRepoUrl()));
218521
- writeFileSync10(join14(androidDir, "build.gradle.kts"), rootBuildGradleTemplate([...gradlePlugins].map(([id, version]) => ({ id, version }))));
218522
- writeFileSync10(join14(androidDir, "gradle.properties"), GRADLE_PROPERTIES);
218523
- writeFileSync10(join14(androidDir, "lecodes-runtime", "build.gradle.kts"), runtimeBuildGradleTemplate(sdkVersion, modules, sdkArtifact));
218524
- const runtimeKt = join14(androidDir, "lecodes-runtime", "src", "main", "kotlin", "io", "letary", "lecodes", "runtime", "LeCodesRuntime.kt");
218525
- mkdirSync8(dirname10(runtimeKt), { recursive: true });
218526
- writeFileSync10(runtimeKt, runtimeKotlinTemplate(runtimePlugins, shellUuid));
218527
- if (pluginSpecs.length > 0 || existsSync13(lockPath))
219064
+ writeFileSync12(join17(androidDir, "README.md"), ANDROID_README);
219065
+ writeFileSync12(join17(androidDir, "settings.gradle.kts"), settingsGradleTemplate(slugify(config.name ?? basename5(root)), modules, aarWrapperPaths, sdkRepoUrl()));
219066
+ writeFileSync12(join17(androidDir, "build.gradle.kts"), rootBuildGradleTemplate([...gradlePlugins].map(([id, version]) => ({ id, version }))));
219067
+ writeFileSync12(join17(androidDir, "gradle.properties"), GRADLE_PROPERTIES);
219068
+ writeFileSync12(join17(androidDir, "lecodes-runtime", "build.gradle.kts"), runtimeBuildGradleTemplate(sdkVersion, modules, sdkArtifact));
219069
+ const runtimeKt = join17(androidDir, "lecodes-runtime", "src", "main", "kotlin", "io", "letary", "lecodes", "runtime", "LeCodesRuntime.kt");
219070
+ mkdirSync10(dirname10(runtimeKt), { recursive: true });
219071
+ writeFileSync12(runtimeKt, runtimeKotlinTemplate(runtimePlugins, shellUuid));
219072
+ if (pluginSpecs.length > 0 || existsSync16(lockPath))
218528
219073
  writeJson(lockPath, { plugins: newLock });
218529
219074
  restampAppBuildGradle(androidDir, config, gradlePlugins);
218530
219075
  if (config.icon) {
@@ -218539,12 +219084,12 @@ var syncAndroid = async (root, config, prior) => {
218539
219084
  note("Build & run from Android Studio (`lecodes app open android`) — user files in android/app/ were not touched.");
218540
219085
  };
218541
219086
  var restampAppBuildGradle = (androidDir, config, gradlePlugins) => {
218542
- const path2 = join14(androidDir, "app", "build.gradle.kts");
218543
- if (!existsSync13(path2)) {
219087
+ const path2 = join17(androidDir, "app", "build.gradle.kts");
219088
+ if (!existsSync16(path2)) {
218544
219089
  warn("android/app/build.gradle.kts is missing — run `lecodes app init android` on a clean tree to regenerate the shell.");
218545
219090
  return;
218546
219091
  }
218547
- let content = readFileSync13(path2, "utf8");
219092
+ let content = readFileSync14(path2, "utf8");
218548
219093
  const before = content;
218549
219094
  const versionName = config.version ?? "1.0.0";
218550
219095
  const versionCode = config.android?.versionCode ?? 1;
@@ -218560,7 +219105,7 @@ var restampAppBuildGradle = (androidDir, config, gradlePlugins) => {
218560
219105
  }
218561
219106
  const applied = new Set;
218562
219107
  for (const id of gradlePlugins.keys()) {
218563
- if (id === "com.google.gms.google-services" && !existsSync13(join14(androidDir, "app", "google-services.json"))) {
219108
+ if (id === "com.google.gms.google-services" && !existsSync16(join17(androidDir, "app", "google-services.json"))) {
218564
219109
  warn(`push needs android/app/google-services.json (download it from the le.codes Firebase project — the app's package name must be registered there). The google-services Gradle plugin stays un-applied until the file exists, so FCM registration will fail.`);
218565
219110
  continue;
218566
219111
  }
@@ -218587,13 +219132,13 @@ var restampAppBuildGradle = (androidDir, config, gradlePlugins) => {
218587
219132
  log(`${c.green("updated")} android/app/build.gradle.kts: + id("${id}")`);
218588
219133
  }
218589
219134
  if (content !== before)
218590
- writeFileSync10(path2, content);
219135
+ writeFileSync12(path2, content);
218591
219136
  };
218592
219137
  var gradlew = (androidDir, tasks) => {
218593
219138
  if (process.platform !== "win32")
218594
219139
  return runTool(androidDir, "sh", ["gradlew", ...tasks]);
218595
- const line = `""${join14(androidDir, "gradlew.bat")}" ${tasks.join(" ")}"`;
218596
- const r2 = spawnSync2("cmd.exe", ["/d", "/s", "/c", line], {
219140
+ const line = `""${join17(androidDir, "gradlew.bat")}" ${tasks.join(" ")}"`;
219141
+ const r2 = spawnSync3("cmd.exe", ["/d", "/s", "/c", line], {
218597
219142
  cwd: androidDir,
218598
219143
  encoding: "utf8",
218599
219144
  stdio: ["ignore", "pipe", "pipe"],
@@ -218612,19 +219157,19 @@ ${tail}`);
218612
219157
  };
218613
219158
  var buildAndroid = async (args) => {
218614
219159
  const root = projectRoot();
218615
- const config = readJson(join14(root, "app.json"));
218616
- const androidDir = join14(root, "android");
218617
- if (!config?.android || !existsSync13(androidDir)) {
219160
+ const config = readJson(join17(root, "app.json"));
219161
+ const androidDir = join17(root, "android");
219162
+ if (!config?.android || !existsSync16(androidDir)) {
218618
219163
  throw new CliError("No Android shell here — run `lecodes app init android` first.");
218619
219164
  }
218620
219165
  if (!flagBool(args, "no-sync"))
218621
219166
  await syncAndroid(root, config, null);
218622
- if (!existsSync13(join14(androidDir, "local.properties"))) {
219167
+ if (!existsSync16(join17(androidDir, "local.properties"))) {
218623
219168
  const sdkHome = process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT;
218624
219169
  if (!sdkHome) {
218625
219170
  throw new CliError("Android SDK not found — set ANDROID_HOME, or create android/local.properties with sdk.dir=…");
218626
219171
  }
218627
- writeFileSync10(join14(androidDir, "local.properties"), `sdk.dir=${sdkHome.replace(/\\/g, "\\\\")}
219172
+ writeFileSync12(join17(androidDir, "local.properties"), `sdk.dir=${sdkHome.replace(/\\/g, "\\\\")}
218628
219173
  `);
218629
219174
  note("Wrote android/local.properties (sdk.dir from the environment).");
218630
219175
  }
@@ -218633,27 +219178,27 @@ var buildAndroid = async (args) => {
218633
219178
  note(`Building (${tasks.join(" ")})… the first run downloads Gradle + dependencies`);
218634
219179
  gradlew(androidDir, tasks);
218635
219180
  const outputs = [];
218636
- const apkDir = join14(androidDir, "app", "build", "outputs", "apk", configuration);
218637
- const aabDir = join14(androidDir, "app", "build", "outputs", "bundle", configuration);
219181
+ const apkDir = join17(androidDir, "app", "build", "outputs", "apk", configuration);
219182
+ const aabDir = join17(androidDir, "app", "build", "outputs", "bundle", configuration);
218638
219183
  for (const dir of [apkDir, aabDir]) {
218639
- if (!existsSync13(dir))
219184
+ if (!existsSync16(dir))
218640
219185
  continue;
218641
- for (const f3 of readdirSync4(dir)) {
219186
+ for (const f3 of readdirSync5(dir)) {
218642
219187
  if (f3.endsWith(".apk") || f3.endsWith(".aab"))
218643
- outputs.push(join14(dir, f3));
219188
+ outputs.push(join17(dir, f3));
218644
219189
  }
218645
219190
  }
218646
219191
  for (const out of outputs)
218647
219192
  log(`${c.green("output")} ${out}`);
218648
- if (outputs.some((o2) => basename4(o2).includes("unsigned"))) {
219193
+ if (outputs.some((o2) => basename5(o2).includes("unsigned"))) {
218649
219194
  note("The APK is UNSIGNED — create android/keystore.properties (see android/README.md) and rebuild to ship it.");
218650
219195
  }
218651
219196
  success(configuration === "debug" ? "Debug build done — `adb install` the APK to test." : "Release build done — upload the .aab in the Play Console (the .apk side-loads for testing).");
218652
219197
  };
218653
219198
  var openAndroid = async () => {
218654
219199
  const root = projectRoot();
218655
- const androidDir = join14(root, "android");
218656
- if (!existsSync13(join14(androidDir, "settings.gradle.kts"))) {
219200
+ const androidDir = join17(root, "android");
219201
+ if (!existsSync16(join17(androidDir, "settings.gradle.kts"))) {
218657
219202
  throw new CliError("No Android shell here — run `lecodes app init android` first.");
218658
219203
  }
218659
219204
  if (process.platform === "darwin") {
@@ -218664,11 +219209,11 @@ var openAndroid = async () => {
218664
219209
  } catch {}
218665
219210
  } else if (process.platform === "win32") {
218666
219211
  const candidates = [
218667
- join14(process.env.LOCALAPPDATA ?? "", "Programs", "Android Studio", "bin", "studio64.exe"),
219212
+ join17(process.env.LOCALAPPDATA ?? "", "Programs", "Android Studio", "bin", "studio64.exe"),
218668
219213
  "C:\\Program Files\\Android\\Android Studio\\bin\\studio64.exe"
218669
219214
  ];
218670
219215
  for (const exe of candidates) {
218671
- if (!existsSync13(exe))
219216
+ if (!existsSync16(exe))
218672
219217
  continue;
218673
219218
  spawn(exe, [androidDir], { detached: true, stdio: "ignore" }).unref();
218674
219219
  success("Opened android/ in Android Studio.");
@@ -218684,19 +219229,19 @@ var openAndroid = async () => {
218684
219229
  note("Open the android/ folder in Android Studio.");
218685
219230
  };
218686
219231
  var doctorAndroidChecks = async (root, config, check) => {
218687
- const androidDir = join14(root, "android");
218688
- const java = spawnSync2("java", ["-version"], { encoding: "utf8" });
219232
+ const androidDir = join17(root, "android");
219233
+ const java = spawnSync3("java", ["-version"], { encoding: "utf8" });
218689
219234
  const javaOut = `${java.stderr ?? ""}${java.stdout ?? ""}`;
218690
219235
  const javaMajor = Number(/version "(\d+)/.exec(javaOut)?.[1] ?? 0);
218691
219236
  check(javaMajor >= 17, `JDK ${javaMajor || "not found"}`, "install JDK 17+ (Android Studio ships one)");
218692
- const hasSdkHome = !!(process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT) || existsSync13(join14(androidDir, "local.properties"));
219237
+ const hasSdkHome = !!(process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT) || existsSync16(join17(androidDir, "local.properties"));
218693
219238
  check(hasSdkHome, "Android SDK location", "set ANDROID_HOME or create android/local.properties");
218694
219239
  const requestedVariant = config.android?.variant ?? "auto";
218695
219240
  const validVariant = requestedVariant === "auto" || SDK_VARIANTS.includes(requestedVariant);
218696
219241
  check(validVariant, `android.variant "${requestedVariant}"`, `use "auto" or one of ${SDK_VARIANTS.join(", ")}`);
218697
- const wrapperJar = join14(androidDir, "gradle", "wrapper", "gradle-wrapper.jar");
218698
- check(existsSync13(wrapperJar) && statSync2(wrapperJar).size > 0, "Gradle wrapper jar", `run \`gradle wrapper --gradle-version ${GRADLE_VERSION}\` in android/ (or let Android Studio repair it)`);
218699
- check(existsSync13(join14(androidDir, "app", "src", "main", "assets", "app.js")), "Bundled app.js", "run `lecodes app sync`");
219242
+ const wrapperJar = join17(androidDir, "gradle", "wrapper", "gradle-wrapper.jar");
219243
+ check(existsSync16(wrapperJar) && statSync3(wrapperJar).size > 0, "Gradle wrapper jar", `run \`gradle wrapper --gradle-version ${GRADLE_VERSION}\` in android/ (or let Android Studio repair it)`);
219244
+ check(existsSync16(join17(androidDir, "app", "src", "main", "assets", "app.js")), "Bundled app.js", "run `lecodes app sync`");
218700
219245
  const linked = shellSdkVersionAndroid(androidDir);
218701
219246
  const pin = config.android?.sdk ?? "latest";
218702
219247
  check(!!linked, `SDK ${linked ?? "version unreadable"}`, "run `lecodes app sync`");
@@ -218709,25 +219254,24 @@ var doctorAndroidChecks = async (root, config, check) => {
218709
219254
  check(cmpVersion(linked, latest) >= 0, `SDK is the latest release (${latest})`, "run `lecodes app update`", false);
218710
219255
  } catch {}
218711
219256
  }
218712
- const hasKeystore = existsSync13(join14(androidDir, "keystore.properties")) || !!process.env.LECODES_ANDROID_KEYSTORE_FILE;
219257
+ const hasKeystore = existsSync16(join17(androidDir, "keystore.properties")) || !!process.env.LECODES_ANDROID_KEYSTORE_FILE;
218713
219258
  check(hasKeystore, "Release signing configured", "android/keystore.properties or LECODES_ANDROID_* env (release builds come out unsigned without it)", false);
218714
219259
  if (Object.keys(config.plugins ?? {}).includes("push")) {
218715
- check(existsSync13(join14(androidDir, "app", "google-services.json")), "google-services.json present (push)", "download it from the le.codes Firebase project — FCM registration fails without it", false);
219260
+ check(existsSync16(join17(androidDir, "app", "google-services.json")), "google-services.json present (push)", "download it from the le.codes Firebase project — FCM registration fails without it", false);
218716
219261
  }
218717
219262
  };
218718
219263
 
218719
219264
  // src/commands/appDesktop.ts
218720
219265
  import { execFileSync as execFileSync3, spawn as spawn3 } from "node:child_process";
218721
- import { cpSync as cpSync2, existsSync as existsSync15, mkdirSync as mkdirSync10, readdirSync as readdirSync6, rmSync as rmSync3, statSync as statSync4, writeFileSync as writeFileSync12 } from "node:fs";
218722
- import { basename as basename5, dirname as dirname11, join as join16, resolve as resolve5 } from "node:path";
219266
+ import { cpSync as cpSync2, existsSync as existsSync18, mkdirSync as mkdirSync12, readdirSync as readdirSync7, rmSync as rmSync5, statSync as statSync5, writeFileSync as writeFileSync14 } from "node:fs";
219267
+ import { basename as basename6, dirname as dirname11, join as join19, resolve as resolve6 } from "node:path";
218723
219268
 
218724
219269
  // src/desktopRenderer.ts
219270
+ import { spawn as spawn2 } from "node:child_process";
219271
+ import { copyFileSync as copyFileSync2, existsSync as existsSync17, mkdirSync as mkdirSync11, mkdtempSync as mkdtempSync2, readdirSync as readdirSync6, readFileSync as readFileSync15, rmSync as rmSync4, statSync as statSync4, writeFileSync as writeFileSync13 } from "node:fs";
219272
+ import { homedir as homedir6, tmpdir as tmpdir2 } from "node:os";
219273
+ import { join as join18, resolve as resolve5 } from "node:path";
218725
219274
  init_util();
218726
- import { createHash as createHash5 } from "node:crypto";
218727
- import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
218728
- import { copyFileSync, existsSync as existsSync14, mkdirSync as mkdirSync9, mkdtempSync, readdirSync as readdirSync5, readFileSync as readFileSync14, renameSync as renameSync2, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync11 } from "node:fs";
218729
- import { homedir as homedir5, tmpdir } from "node:os";
218730
- import { join as join15, resolve as resolve4 } from "node:path";
218731
219275
 
218732
219276
  // ../viewer-lite/src/sourcemapError.ts
218733
219277
  var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
@@ -218828,16 +219372,8 @@ var makeStackDemangler = (bundleJs) => {
218828
219372
  };
218829
219373
 
218830
219374
  // src/desktopRenderer.ts
218831
- var MANIFEST_URL = "https://storage.yandexcloud.net/le-codes/releases/versions.json";
218832
219375
  var DESKTOP_RENDERER_VERSION = "1.1.0";
218833
- var semverNewer = (a2, b) => {
218834
- const pa = a2.split(/[.-]/).map(Number), pb = b.split(/[.-]/).map(Number);
218835
- for (let i2 = 0;i2 < 3; i2++)
218836
- if ((pb[i2] ?? 0) !== (pa[i2] ?? 0))
218837
- return (pb[i2] ?? 0) - (pa[i2] ?? 0);
218838
- return 0;
218839
- };
218840
- var platformKey = () => {
219376
+ var platformKey2 = () => {
218841
219377
  if (process.platform === "win32")
218842
219378
  return "windows-x64";
218843
219379
  if (process.platform === "linux")
@@ -218847,8 +219383,8 @@ var platformKey = () => {
218847
219383
  var exeName = () => process.platform === "win32" ? "lecodes-desktop.exe" : "lecodes-desktop";
218848
219384
  var findExe = (dir) => {
218849
219385
  const name = exeName();
218850
- for (const entry of readdirSync5(dir, { withFileTypes: true })) {
218851
- const p2 = join15(dir, entry.name);
219386
+ for (const entry of readdirSync6(dir, { withFileTypes: true })) {
219387
+ const p2 = join18(dir, entry.name);
218852
219388
  if (entry.isFile() && entry.name === name)
218853
219389
  return p2;
218854
219390
  if (entry.isDirectory()) {
@@ -218859,40 +219395,26 @@ var findExe = (dir) => {
218859
219395
  }
218860
219396
  return null;
218861
219397
  };
218862
- var cacheRoot = () => join15(homedir5(), ".lecodes", "renderer");
218863
- var currentFile = () => join15(cacheRoot(), "current.json");
219398
+ var cacheRoot2 = () => join18(homedir6(), ".lecodes", "renderer");
219399
+ var currentFile = () => join18(cacheRoot2(), "current.json");
218864
219400
  var installedDesktopVersions = () => {
218865
219401
  try {
218866
- return readdirSync5(cacheRoot(), { withFileTypes: true }).filter((e2) => e2.isDirectory() && e2.name.startsWith("desktop-")).map((e2) => e2.name.slice("desktop-".length)).filter((v) => findExe(join15(cacheRoot(), `desktop-${v}`)) !== null).sort(semverNewer);
219402
+ return readdirSync6(cacheRoot2(), { withFileTypes: true }).filter((e2) => e2.isDirectory() && e2.name.startsWith("desktop-")).map((e2) => e2.name.slice("desktop-".length)).filter((v) => findExe(join18(cacheRoot2(), `desktop-${v}`)) !== null).sort(semverNewer);
218867
219403
  } catch {
218868
219404
  return [];
218869
219405
  }
218870
219406
  };
218871
219407
  var selectedDesktopVersion = () => {
218872
219408
  try {
218873
- const saved = JSON.parse(readFileSync14(currentFile(), "utf8")).version;
219409
+ const saved = JSON.parse(readFileSync15(currentFile(), "utf8")).version;
218874
219410
  if (saved && /^\d+\.\d+\.\d+/.test(saved) && semverNewer(DESKTOP_RENDERER_VERSION, saved) > 0)
218875
219411
  return saved;
218876
219412
  } catch {}
218877
219413
  return DESKTOP_RENDERER_VERSION;
218878
219414
  };
218879
- var fetchDesktopManifest = async () => {
218880
- let manifest;
218881
- try {
218882
- const res = await fetch(MANIFEST_URL);
218883
- if (!res.ok)
218884
- throw new Error(`HTTP ${res.status}`);
218885
- manifest = await res.json();
218886
- } catch (e2) {
218887
- throw new CliError(`Can't fetch the release manifest (${MANIFEST_URL}): ${e2 instanceof Error ? e2.message : String(e2)}`);
218888
- }
218889
- const desktop = manifest.products?.desktop;
218890
- if (!desktop)
218891
- throw new CliError("The release manifest has no desktop product.");
218892
- return desktop;
218893
- };
219415
+ var fetchDesktopManifest = () => fetchReleaseProduct("desktop", "desktop");
218894
219416
  var downloadDesktop = async (version, dest) => {
218895
- const key = platformKey();
219417
+ const key = platformKey2();
218896
219418
  const entry = (await fetchDesktopManifest()).versions.find((v) => v.version === version);
218897
219419
  const file = entry?.files[key];
218898
219420
  if (!file) {
@@ -218900,49 +219422,22 @@ var downloadDesktop = async (version, dest) => {
218900
219422
  Set LECODES_DESKTOP_EXE to a locally built host, or update lecodes-cli.`);
218901
219423
  }
218902
219424
  note(`Downloading the native renderer (LeCodes Desktop ${version}, ${(file.size / 1024 / 1024).toFixed(1)} MB)…`);
218903
- let bytes;
218904
- try {
218905
- const res = await fetch(file.url);
218906
- if (!res.ok)
218907
- throw new Error(`HTTP ${res.status}`);
218908
- bytes = Buffer.from(await res.arrayBuffer());
218909
- } catch (e2) {
218910
- throw new CliError(`Download failed (${file.url}): ${e2 instanceof Error ? e2.message : String(e2)}`);
218911
- }
218912
- const sha = createHash5("sha256").update(bytes).digest("hex");
218913
- if (sha !== file.sha256)
218914
- throw new CliError(`Downloaded archive is corrupt (sha256 mismatch) — try again.`);
218915
- mkdirSync9(cacheRoot(), { recursive: true });
218916
- const stage = mkdtempSync(join15(cacheRoot(), "dl-"));
218917
- try {
218918
- const archive = join15(stage, file.url.split("/").pop() ?? "archive");
218919
- writeFileSync11(archive, bytes);
218920
- const tar = spawnSync3("tar", ["-xf", archive, "-C", stage], { stdio: ["ignore", "ignore", "pipe"] });
218921
- if (tar.status !== 0)
218922
- throw new CliError(`Couldn't extract the renderer archive: ${tar.stderr?.toString() ?? "tar failed"}`);
218923
- rmSync2(archive);
218924
- if (!findExe(stage))
218925
- throw new CliError("The downloaded archive doesn't contain the desktop host binary.");
218926
- try {
218927
- renameSync2(stage, dest);
218928
- } catch {
218929
- if (!findExe(dest))
218930
- throw new CliError(`Couldn't move the renderer into ${dest}.`);
218931
- }
218932
- } finally {
218933
- rmSync2(stage, { recursive: true, force: true });
218934
- }
219425
+ await downloadReleaseArchive(file, {
219426
+ cacheRoot: cacheRoot2(),
219427
+ dest,
219428
+ validate: (dir) => findExe(dir) ? null : "The downloaded archive doesn't contain the desktop host binary."
219429
+ });
218935
219430
  };
218936
219431
  var resolveDesktopExe = async () => {
218937
219432
  const override = process.env.LECODES_DESKTOP_EXE;
218938
219433
  if (override) {
218939
- if (!existsSync14(override))
219434
+ if (!existsSync17(override))
218940
219435
  throw new CliError(`LECODES_DESKTOP_EXE points at "${override}" which doesn't exist.`);
218941
- return resolve4(override);
219436
+ return resolve5(override);
218942
219437
  }
218943
219438
  const version = selectedDesktopVersion();
218944
- const dir = join15(cacheRoot(), `desktop-${version}`);
218945
- let exe = existsSync14(dir) ? findExe(dir) : null;
219439
+ const dir = join18(cacheRoot2(), `desktop-${version}`);
219440
+ let exe = existsSync17(dir) ? findExe(dir) : null;
218946
219441
  if (!exe) {
218947
219442
  await downloadDesktop(version, dir);
218948
219443
  exe = findExe(dir);
@@ -218957,12 +219452,12 @@ var updateDesktop = async () => {
218957
219452
  throw new CliError(`The newest published LeCodes Desktop is ${latest}, but this CLI needs ≥ ${DESKTOP_RENDERER_VERSION} ` + `for the render contract. Publish the desktop release first.`);
218958
219453
  }
218959
219454
  const before = selectedDesktopVersion();
218960
- const dir = join15(cacheRoot(), `desktop-${latest}`);
218961
- const hadIt = existsSync14(dir) && findExe(dir) !== null;
219455
+ const dir = join18(cacheRoot2(), `desktop-${latest}`);
219456
+ const hadIt = existsSync17(dir) && findExe(dir) !== null;
218962
219457
  if (!hadIt)
218963
219458
  await downloadDesktop(latest, dir);
218964
- mkdirSync9(cacheRoot(), { recursive: true });
218965
- writeFileSync11(currentFile(), JSON.stringify({ version: latest }, null, 2));
219459
+ mkdirSync11(cacheRoot2(), { recursive: true });
219460
+ writeFileSync13(currentFile(), JSON.stringify({ version: latest }, null, 2));
218966
219461
  if (latest === before && hadIt)
218967
219462
  note(`Already up to date: LeCodes Desktop ${latest}.`);
218968
219463
  else if (latest === before)
@@ -218988,14 +219483,19 @@ var windowEnv = (opts) => {
218988
219483
  env.CREATOR_UI_SCALE = "fit";
218989
219484
  else if (typeof opts.scale === "number")
218990
219485
  env.CREATOR_UI_SCALE = String(opts.scale);
219486
+ if (typeof opts.renderScale === "number")
219487
+ env.CREATOR_RENDER_SCALE = String(opts.renderScale);
219488
+ if (opts.maxRenderSize && opts.maxRenderSize[0] > 0 && opts.maxRenderSize[1] > 0) {
219489
+ env.CREATOR_RENDER_MAX = `${Math.round(opts.maxRenderSize[0])}x${Math.round(opts.maxRenderSize[1])}`;
219490
+ }
218991
219491
  return env;
218992
219492
  };
218993
219493
  var stageBundle = (js, resources) => {
218994
- const runDir = mkdtempSync(join15(tmpdir(), "lecodes-render-"));
218995
- writeFileSync11(join15(runDir, "app.js"), js);
219494
+ const runDir = mkdtempSync2(join18(tmpdir2(), "lecodes-render-"));
219495
+ writeFileSync13(join18(runDir, "app.js"), js);
218996
219496
  for (const r2 of resources) {
218997
- if (r2.absPath && existsSync14(r2.absPath) && statSync3(r2.absPath).isFile()) {
218998
- copyFileSync(r2.absPath, join15(runDir, r2.name));
219497
+ if (r2.absPath && existsSync17(r2.absPath) && statSync4(r2.absPath).isFile()) {
219498
+ copyFileSync2(r2.absPath, join18(runDir, r2.name));
218999
219499
  }
219000
219500
  }
219001
219501
  return runDir;
@@ -219023,7 +219523,7 @@ var runDesktopApp = async (js, resources, opts = {}) => {
219023
219523
  const env = { ...process.env, ...windowEnv(opts) };
219024
219524
  const demangle = makeStackDemangler(js);
219025
219525
  await new Promise((done, fail) => {
219026
- const child = demangle ? spawn2(exe, [join15(runDir, "app.js")], { cwd: runDir, env, stdio: ["ignore", "pipe", "pipe"] }) : spawn2(exe, [join15(runDir, "app.js")], { cwd: runDir, env, stdio: ["ignore", "inherit", "inherit"] });
219526
+ const child = demangle ? spawn2(exe, [join18(runDir, "app.js")], { cwd: runDir, env, stdio: ["ignore", "pipe", "pipe"] }) : spawn2(exe, [join18(runDir, "app.js")], { cwd: runDir, env, stdio: ["ignore", "inherit", "inherit"] });
219027
219527
  if (demangle) {
219028
219528
  pipeThroughDemangler(child.stdout, process.stdout, demangle);
219029
219529
  pipeThroughDemangler(child.stderr, process.stderr, demangle);
@@ -219037,7 +219537,7 @@ var runDesktopApp = async (js, resources, opts = {}) => {
219037
219537
  });
219038
219538
  });
219039
219539
  } finally {
219040
- rmSync2(runDir, { recursive: true, force: true });
219540
+ rmSync4(runDir, { recursive: true, force: true });
219041
219541
  }
219042
219542
  };
219043
219543
  var runDesktopDev = async (url, opts = {}) => {
@@ -219053,8 +219553,8 @@ var runDesktopRender = async (js, resources, opts) => {
219053
219553
  const exe = await resolveDesktopExe();
219054
219554
  const runDir = stageBundle(js, resources);
219055
219555
  try {
219056
- const appJs = join15(runDir, "app.js");
219057
- const outAbs = resolve4(opts.outPng);
219556
+ const appJs = join18(runDir, "app.js");
219557
+ const outAbs = resolve5(opts.outPng);
219058
219558
  const env = {
219059
219559
  ...process.env,
219060
219560
  CREATOR_DUMP_PNG: outAbs,
@@ -219063,8 +219563,8 @@ var runDesktopRender = async (js, resources, opts) => {
219063
219563
  CREATOR_HIDDEN: "1"
219064
219564
  };
219065
219565
  if (opts.script !== undefined) {
219066
- const scriptPath = join15(runDir, "script.txt");
219067
- writeFileSync11(scriptPath, opts.script);
219566
+ const scriptPath = join18(runDir, "script.txt");
219567
+ writeFileSync13(scriptPath, opts.script);
219068
219568
  env.CREATOR_SCRIPT = scriptPath;
219069
219569
  }
219070
219570
  if (opts.fixedDtMs)
@@ -219096,7 +219596,7 @@ var runDesktopRender = async (js, resources, opts) => {
219096
219596
  child.on("exit", (code) => {
219097
219597
  clearTimeout(killer);
219098
219598
  const output = demangle(chunks.join(""));
219099
- if (code === 0 && existsSync14(outAbs))
219599
+ if (code === 0 && existsSync17(outAbs))
219100
219600
  return done({ output, exitCode: code });
219101
219601
  const tail = demangle(chunks.slice(-80).join("")).trimEnd();
219102
219602
  const detail = tail.length > 0 ? `
@@ -219105,21 +219605,21 @@ ${tail}` : "";
219105
219605
  });
219106
219606
  });
219107
219607
  } finally {
219108
- rmSync2(runDir, { recursive: true, force: true });
219608
+ rmSync4(runDir, { recursive: true, force: true });
219109
219609
  }
219110
219610
  };
219111
219611
 
219112
219612
  // src/commands/appDesktop.ts
219113
219613
  init_util();
219114
219614
  var mb = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)} MB`;
219115
- var dirSize = (dir) => readdirSync6(dir, { withFileTypes: true }).reduce((sum, e2) => {
219116
- const p2 = join16(dir, e2.name);
219117
- return sum + (e2.isDirectory() ? dirSize(p2) : statSync4(p2).size);
219615
+ var dirSize = (dir) => readdirSync7(dir, { withFileTypes: true }).reduce((sum, e2) => {
219616
+ const p2 = join19(dir, e2.name);
219617
+ return sum + (e2.isDirectory() ? dirSize(p2) : statSync5(p2).size);
219118
219618
  }, 0);
219119
219619
  var initDesktop = async (args) => {
219120
219620
  const root = projectRoot();
219121
- const existing = readJson(join16(root, "app.json")) ?? {};
219122
- const name = flagStr(args, "name") ?? existing.name ?? basename5(root);
219621
+ const existing = readJson(join19(root, "app.json")) ?? {};
219622
+ const name = flagStr(args, "name") ?? existing.name ?? basename6(root);
219123
219623
  const window2 = {
219124
219624
  width: 1280,
219125
219625
  height: 800,
@@ -219129,19 +219629,19 @@ var initDesktop = async (args) => {
219129
219629
  ...flagBool(args, "fullscreen") ? { fullscreen: true } : {}
219130
219630
  };
219131
219631
  const config = { ...existing, name, desktop: { ...existing.desktop, window: window2 } };
219132
- writeJson(join16(root, "app.json"), config);
219632
+ writeJson(join19(root, "app.json"), config);
219133
219633
  log(`${c.green("written")} app.json (desktop.window ${window2.width}×${window2.height}${window2.fullscreen ? ", fullscreen" : ""})`);
219134
219634
  log(`Next: ${c.bold("lecodes dev --desktop")} to develop, ${c.bold("lecodes app build desktop")} to package.`);
219135
219635
  };
219136
219636
  var cleanFileName = (name) => name.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "").replace(/[. ]+$/, "").trim() || "app";
219137
219637
  var buildDesktop = async (args) => {
219138
219638
  const root = projectRoot();
219139
- const config = readJson(join16(root, "app.json")) ?? {};
219140
- const name = (config.name ?? basename5(root)).trim() || "app";
219639
+ const config = readJson(join19(root, "app.json")) ?? {};
219640
+ const name = (config.name ?? basename6(root)).trim() || "app";
219141
219641
  const slug = flagStr(args, "slug") ?? cleanFileName(name);
219142
- const out = resolve5(root, flagStr(args, "o", "out") ?? join16(root, "dist", slug));
219143
- if (existsSync15(out)) {
219144
- const entries = readdirSync6(out);
219642
+ const out = resolve6(root, flagStr(args, "o", "out") ?? join19(root, "dist", slug));
219643
+ if (existsSync18(out)) {
219644
+ const entries = readdirSync7(out);
219145
219645
  const looksLikeBuild = entries.length === 0 || entries.includes("app.js");
219146
219646
  if (!looksLikeBuild) {
219147
219647
  throw new CliError(`${out} exists and doesn't look like a previous desktop build — pick another -o or clear it yourself.`);
@@ -219150,12 +219650,12 @@ var buildDesktop = async (args) => {
219150
219650
  if (config.desktop?.entry && flagStr(args, "entry") === undefined)
219151
219651
  args.flags.entry = config.desktop.entry;
219152
219652
  note(`Building "${name}" for desktop…`);
219153
- const { js, resources } = await buildBundle(args, { remotePatterns: config.resources?.remote ?? [] });
219653
+ const { js, resources } = await buildBundle(args, { remotePatterns: config.resources?.remote ?? [] }, { localShaders: true });
219154
219654
  const wanted = new Set([...js.matchAll(/fetchLocal\("([^"]+)"\)/g)].map((m) => m[1]));
219155
- const shipped = resources.filter((r2) => !r2.remote && wanted.has(r2.name));
219655
+ const shipped = resources.filter((r2) => !r2.remote && (r2.shader || wanted.has(r2.name)));
219156
219656
  const byName = new Map;
219157
219657
  for (const r2 of resources) {
219158
- if (r2.remote || !wanted.has(r2.name))
219658
+ if (r2.remote || r2.shader || !wanted.has(r2.name))
219159
219659
  continue;
219160
219660
  const set = byName.get(r2.name) ?? new Set;
219161
219661
  set.add(r2.path);
@@ -219169,45 +219669,51 @@ var buildDesktop = async (args) => {
219169
219669
  `)).join(`
219170
219670
  `));
219171
219671
  }
219672
+ const exe = await resolveDesktopExe();
219673
+ const hostDir = dirname11(exe);
219674
+ const IMPLICIT_PRELOADS = new Set(["uberarchive", "capture"]);
219675
+ const hostProvided = new Set;
219676
+ try {
219677
+ for (const f3 of readdirSync7(join19(hostDir, "assets")))
219678
+ hostProvided.add(f3);
219679
+ } catch {}
219172
219680
  const shippedNames = new Set(shipped.map((r2) => r2.name));
219173
- const missing = [...wanted].filter((n2) => !shippedNames.has(n2));
219681
+ const missing = [...wanted].filter((n2) => !shippedNames.has(n2) && !hostProvided.has(n2) && !IMPLICIT_PRELOADS.has(n2));
219174
219682
  if (missing.length > 0) {
219175
219683
  throw new CliError(`The bundle references assets with no local file: ${missing.join(", ")}`);
219176
219684
  }
219177
- const exe = await resolveDesktopExe();
219178
- const hostDir = dirname11(exe);
219179
219685
  const version = process.env.LECODES_DESKTOP_EXE ? "local build" : selectedDesktopVersion();
219180
- rmSync3(out, { recursive: true, force: true });
219181
- mkdirSync10(out, { recursive: true });
219182
- for (const entry of readdirSync6(hostDir, { withFileTypes: true })) {
219686
+ rmSync5(out, { recursive: true, force: true });
219687
+ mkdirSync12(out, { recursive: true });
219688
+ for (const entry of readdirSync7(hostDir, { withFileTypes: true })) {
219183
219689
  const keep = entry.isFile() && entry.name.toLowerCase().endsWith(".dll") || entry.isDirectory() && (entry.name === "assets" || entry.name === "fonts");
219184
219690
  if (keep)
219185
- cpSync2(join16(hostDir, entry.name), join16(out, entry.name), { recursive: true });
219691
+ cpSync2(join19(hostDir, entry.name), join19(out, entry.name), { recursive: true });
219186
219692
  }
219187
- if (!existsSync15(join16(out, "assets", "initial.js"))) {
219693
+ if (!existsSync18(join19(out, "assets", "initial.js"))) {
219188
219694
  throw new CliError(`The host at ${hostDir} has no assets/initial.js next to it — a staged build can't boot. ` + `(LECODES_DESKTOP_EXE must point at an exe with its runtime assets/ beside it.)`);
219189
219695
  }
219190
219696
  const exeName2 = process.platform === "win32" ? `${cleanFileName(name)}.exe` : cleanFileName(name);
219191
- cpSync2(exe, join16(out, exeName2));
219697
+ cpSync2(exe, join19(out, exeName2));
219192
219698
  note(` host: LeCodes Desktop ${version}`);
219193
- writeFileSync12(join16(out, "app.js"), js);
219699
+ writeFileSync14(join19(out, "app.js"), js);
219194
219700
  let assetBytes = 0, assetCount = 0;
219195
219701
  for (const r2 of shipped) {
219196
- if (!r2.absPath || !existsSync15(r2.absPath) || !statSync4(r2.absPath).isFile()) {
219702
+ if (!r2.absPath || !existsSync18(r2.absPath) || !statSync5(r2.absPath).isFile()) {
219197
219703
  warn(`asset "${r2.name}" (${r2.path}) has no local file — skipped`);
219198
219704
  continue;
219199
219705
  }
219200
- cpSync2(r2.absPath, join16(out, r2.name));
219201
- assetBytes += statSync4(r2.absPath).size;
219706
+ cpSync2(r2.absPath, join19(out, r2.name));
219707
+ assetBytes += statSync5(r2.absPath).size;
219202
219708
  assetCount++;
219203
219709
  }
219204
219710
  note(` assets: ${assetCount} files, ${mb(assetBytes)}`);
219205
- writeJson(join16(out, "app.json"), { name, ...config.desktop ? { desktop: { window: config.desktop.window } } : {} });
219711
+ writeJson(join19(out, "app.json"), { name, ...config.desktop ? { desktop: { window: config.desktop.window } } : {} });
219206
219712
  if (config.icon) {
219207
- await writeIco(root, config, join16(out, "app.ico"));
219713
+ await writeIco(root, config, join19(out, "app.ico"));
219208
219714
  note(" app.ico rendered from " + config.icon);
219209
219715
  }
219210
- writeFileSync12(join16(out, "README.txt"), "\uFEFF" + readmeText(name, exeName2, version));
219716
+ writeFileSync14(join19(out, "README.txt"), "\uFEFF" + readmeText(name, exeName2, version));
219211
219717
  success(`Portable build: ${out} (${mb(dirSize(out))})`);
219212
219718
  if (flagBool(args, "probe"))
219213
219719
  await probeBuild(out, exeName2, config, args);
@@ -219240,15 +219746,15 @@ var writeIco = async (root, config, dest) => {
219240
219746
  header.writeUInt32LE(offset, e2 + 12);
219241
219747
  offset += pngs[i2].length;
219242
219748
  });
219243
- writeFileSync12(dest, Buffer.concat([header, ...pngs]));
219749
+ writeFileSync14(dest, Buffer.concat([header, ...pngs]));
219244
219750
  };
219245
219751
  var probeBuild = async (out, exeName2, config, args) => {
219246
219752
  const win = config.desktop?.window;
219247
219753
  const frames = Number(flagStr(args, "frames")) || 90;
219248
- const shot = join16(dirname11(out), `${basename5(out)}-probe.png`);
219754
+ const shot = join19(dirname11(out), `${basename6(out)}-probe.png`);
219249
219755
  note(`Probe: hidden render of the staged folder (${frames} frames)…`);
219250
219756
  await new Promise((done, fail) => {
219251
- const child = spawn3(join16(out, exeName2), [], {
219757
+ const child = spawn3(join19(out, exeName2), [], {
219252
219758
  cwd: out,
219253
219759
  env: {
219254
219760
  ...process.env,
@@ -219277,7 +219783,7 @@ var probeBuild = async (out, exeName2, config, args) => {
219277
219783
  });
219278
219784
  child.on("exit", (code) => {
219279
219785
  clearTimeout(killer);
219280
- if (code === 0 && existsSync15(shot)) {
219786
+ if (code === 0 && existsSync18(shot)) {
219281
219787
  success(`Probe: ${shot}`);
219282
219788
  done();
219283
219789
  } else {
@@ -219290,7 +219796,7 @@ ${tail.join("").trimEnd()}` : "";
219290
219796
  };
219291
219797
  var zipBuild = (out) => {
219292
219798
  const zip = `${out}.zip`;
219293
- rmSync3(zip, { force: true });
219799
+ rmSync5(zip, { force: true });
219294
219800
  note("Zipping…");
219295
219801
  if (process.platform === "win32") {
219296
219802
  const q = (s2) => s2.replaceAll("'", "''");
@@ -219304,9 +219810,9 @@ var zipBuild = (out) => {
219304
219810
  "$zip.Dispose()"
219305
219811
  ].join(" ")], { stdio: ["ignore", "ignore", "pipe"] });
219306
219812
  } else {
219307
- execFileSync3("zip", ["-rq", zip, basename5(out)], { cwd: dirname11(out) });
219813
+ execFileSync3("zip", ["-rq", zip, basename6(out)], { cwd: dirname11(out) });
219308
219814
  }
219309
- success(`Zip: ${zip} (${mb(statSync4(zip).size)})`);
219815
+ success(`Zip: ${zip} (${mb(statSync5(zip).size)})`);
219310
219816
  };
219311
219817
  var readmeText = (name, exeName2, version) => `${name}
219312
219818
  ${"=".repeat(Math.max(name.length, 4))}
@@ -219327,10 +219833,10 @@ var SDK_REPO = "letary/lecodes-ios-sdk";
219327
219833
  var SDK_MIN_VERSION = "1.2.0";
219328
219834
  var SDK_IDENTITY_VERSION = "1.6.0";
219329
219835
  var shellSdkVersion = (iosDir) => {
219330
- const path2 = join17(iosDir, "LeCodesRuntime", "Package.swift");
219331
- if (!existsSync16(path2))
219836
+ const path2 = join20(iosDir, "LeCodesRuntime", "Package.swift");
219837
+ if (!existsSync19(path2))
219332
219838
  return null;
219333
- const m = /lecodes-ios-sdk\.git",\s*(?:exact|from):\s*"([^"]+)"/.exec(readFileSync15(path2, "utf8"));
219839
+ const m = /lecodes-ios-sdk\.git",\s*(?:exact|from):\s*"([^"]+)"/.exec(readFileSync16(path2, "utf8"));
219334
219840
  return m && VERSION_RE.test(m[1]) ? m[1] : null;
219335
219841
  };
219336
219842
  var IOS_SDK_OPTS = { repo: SDK_REPO, minVersion: SDK_MIN_VERSION, configKey: "ios.sdk" };
@@ -219344,11 +219850,11 @@ var appInit = async (args) => {
219344
219850
  throw new CliError(platform ? `Unsupported platform "${platform}" — use ios, android or desktop.` : "Usage: lecodes app init <ios|android|desktop> [--name <display name>] [--bundle-id <id>] [--team <team id>] [--variant auto|core|2d|3d|full] [--app-id <application id>] [--sdk <version|latest>]");
219345
219851
  }
219346
219852
  const root = projectRoot();
219347
- const iosDir = join17(root, "ios");
219348
- if (existsSync16(iosDir)) {
219853
+ const iosDir = join20(root, "ios");
219854
+ if (existsSync19(iosDir)) {
219349
219855
  throw new CliError("ios/ already exists — run `lecodes app sync` to update it.");
219350
219856
  }
219351
- const existing = readJson(join17(root, "app.json")) ?? {};
219857
+ const existing = readJson(join20(root, "app.json")) ?? {};
219352
219858
  const variant = flagStr(args, "variant") ?? existing.ios?.variant ?? "auto";
219353
219859
  if (variant !== "auto" && !SDK_VARIANTS.includes(variant)) {
219354
219860
  throw new CliError(`--variant must be auto, ${SDK_VARIANTS.join(", ")} (got "${variant}")`);
@@ -219356,7 +219862,7 @@ var appInit = async (args) => {
219356
219862
  const sdkFlag = flagStr(args, "sdk") ?? existing.ios?.sdk;
219357
219863
  const sdkVersion = await resolveSdkVersion(process.env.LECODES_IOS_SDK ?? sdkFlag ?? "latest", null, IOS_SDK_OPTS);
219358
219864
  const sdk = sdkFlag === "latest" ? "latest" : sdkVersion;
219359
- const name = flagStr(args, "name") ?? existing.name ?? basename6(root);
219865
+ const name = flagStr(args, "name") ?? existing.name ?? basename7(root);
219360
219866
  const config = {
219361
219867
  ...existing,
219362
219868
  name,
@@ -219370,7 +219876,7 @@ var appInit = async (args) => {
219370
219876
  buildNumber: existing.ios?.buildNumber ?? 1
219371
219877
  }
219372
219878
  };
219373
- writeJson(join17(root, "app.json"), config);
219879
+ writeJson(join20(root, "app.json"), config);
219374
219880
  log(`${c.green("written")} app.json`);
219375
219881
  ensureShellIgnored(root, "ios");
219376
219882
  const identity = {
@@ -219381,9 +219887,9 @@ var appInit = async (args) => {
219381
219887
  buildNumber: config.ios.buildNumber
219382
219888
  };
219383
219889
  const write = (rel, content) => {
219384
- const abs = join17(iosDir, rel);
219385
- mkdirSync11(dirname12(abs), { recursive: true });
219386
- writeFileSync13(abs, content);
219890
+ const abs = join20(iosDir, rel);
219891
+ mkdirSync13(dirname12(abs), { recursive: true });
219892
+ writeFileSync15(abs, content);
219387
219893
  log(`${c.green("created")} ios/${rel}`);
219388
219894
  };
219389
219895
  write("README.md", IOS_README);
@@ -219411,23 +219917,23 @@ var ensureSdkResourcesBundle = async (sdkVersion) => {
219411
219917
  const override = process.env.LECODES_SDK_RESOURCES;
219412
219918
  if (override) {
219413
219919
  note(`Using local resources bundle ${override} (LECODES_SDK_RESOURCES)`);
219414
- return resolve6(override);
219920
+ return resolve7(override);
219415
219921
  }
219416
- const dir = join17(homedir6(), ".lecodes", "sdk-resources", sdkVersion);
219417
- const bundle = join17(dir, "LeCodesSDKResources.bundle");
219418
- if (existsSync16(bundle))
219922
+ const dir = join20(homedir7(), ".lecodes", "sdk-resources", sdkVersion);
219923
+ const bundle = join20(dir, "LeCodesSDKResources.bundle");
219924
+ if (existsSync19(bundle))
219419
219925
  return bundle;
219420
219926
  const url = `https://github.com/${SDK_REPO}/releases/download/${sdkVersion}/LeCodesSDKResources.bundle.zip`;
219421
219927
  note(`Downloading LeCodesSDKResources.bundle (SDK ${sdkVersion})…`);
219422
219928
  const res = await fetch(url);
219423
219929
  if (!res.ok)
219424
219930
  throw new CliError(`Resources bundle download failed (HTTP ${res.status}) — ${url}`);
219425
- mkdirSync11(dir, { recursive: true });
219426
- const zip = join17(dir, "LeCodesSDKResources.bundle.zip");
219427
- writeFileSync13(zip, new Uint8Array(await res.arrayBuffer()));
219931
+ mkdirSync13(dir, { recursive: true });
219932
+ const zip = join20(dir, "LeCodesSDKResources.bundle.zip");
219933
+ writeFileSync15(zip, new Uint8Array(await res.arrayBuffer()));
219428
219934
  execFileSync4("ditto", ["-x", "-k", zip, dir]);
219429
- rmSync4(zip);
219430
- if (!existsSync16(bundle))
219935
+ rmSync6(zip);
219936
+ if (!existsSync19(bundle))
219431
219937
  throw new CliError(`No LeCodesSDKResources.bundle inside the release asset ${url}`);
219432
219938
  return bundle;
219433
219939
  };
@@ -219452,13 +219958,13 @@ var resolveUpdateUrl = (update, root) => {
219452
219958
  };
219453
219959
  var appSync = async (args) => {
219454
219960
  const root = projectRoot();
219455
- const config = readJson(join17(root, "app.json"));
219961
+ const config = readJson(join20(root, "app.json"));
219456
219962
  const platform = args._[1];
219457
219963
  if (platform !== undefined && platform !== "ios" && platform !== "android") {
219458
219964
  throw new CliError(`Unknown platform "${platform}" — use ios or android.`);
219459
219965
  }
219460
- const hasIos = !!config?.ios && existsSync16(join17(root, "ios"));
219461
- const hasAndroid = !!config?.android && existsSync16(join17(root, "android"));
219966
+ const hasIos = !!config?.ios && existsSync19(join20(root, "ios"));
219967
+ const hasAndroid = !!config?.android && existsSync19(join20(root, "android"));
219462
219968
  if (platform === "ios" && !hasIos)
219463
219969
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
219464
219970
  if (platform === "android" && !hasAndroid)
@@ -219473,7 +219979,7 @@ var appSync = async (args) => {
219473
219979
  await syncAndroid(root, config, iosArtifacts);
219474
219980
  };
219475
219981
  var syncIos = async (root, config) => {
219476
- const iosDir = join17(root, "ios");
219982
+ const iosDir = join20(root, "ios");
219477
219983
  if (!config.ios)
219478
219984
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
219479
219985
  const requestedVariant = config.ios.variant ?? "auto";
@@ -219483,14 +219989,14 @@ var syncIos = async (root, config) => {
219483
219989
  const linkedSdk = shellSdkVersion(iosDir);
219484
219990
  const sdkPin = process.env.LECODES_IOS_SDK ?? config.ios.sdk ?? linkedSdk ?? "latest";
219485
219991
  const sdkVersion = await resolveSdkVersion(sdkPin, linkedSdk, IOS_SDK_OPTS);
219486
- const lockPath = join17(root, "plugins.lock.json");
219992
+ const lockPath = join20(root, "plugins.lock.json");
219487
219993
  const oldLock = readJson(lockPath)?.plugins ?? {};
219488
219994
  const pluginSpecs = Object.entries(config.plugins ?? {});
219489
219995
  const runtimePlugins = [];
219490
219996
  const infoValues = {};
219491
219997
  const entitlementValues = {};
219492
219998
  const newLock = {};
219493
- const sourcesDir = join17(iosDir, "LeCodesRuntime", "Sources");
219999
+ const sourcesDir = join20(iosDir, "LeCodesRuntime", "Sources");
219494
220000
  for (const [id, spec] of pluginSpecs) {
219495
220001
  const plugin = resolvePlugin(id, spec, oldLock[id], "ios");
219496
220002
  const ios = plugin.manifest.ios;
@@ -219498,11 +220004,11 @@ var syncIos = async (root, config) => {
219498
220004
  const register = ios.register;
219499
220005
  if (!module || !register)
219500
220006
  throw new CliError(`Plugin "${id}": manifest ios.module / ios.register missing`);
219501
- const src = join17(plugin.dir, ios.sources ?? "ios");
219502
- if (!existsSync16(src))
220007
+ const src = join20(plugin.dir, ios.sources ?? "ios");
220008
+ if (!existsSync19(src))
219503
220009
  throw new CliError(`Plugin "${id}": sources dir ${src} missing`);
219504
- const dest = join17(sourcesDir, module);
219505
- rmSync4(dest, { recursive: true, force: true });
220010
+ const dest = join20(sourcesDir, module);
220011
+ rmSync6(dest, { recursive: true, force: true });
219506
220012
  cpSync3(src, dest, { recursive: true });
219507
220013
  if (ios.minSdk && VERSION_RE.test(ios.minSdk) && cmpVersion(sdkVersion, ios.minSdk) < 0) {
219508
220014
  throw new CliError(`Plugin "${id}" needs lecodes-ios-sdk ${ios.minSdk} but the shell links ${sdkVersion} — run \`lecodes app update\` (or set app.json ios.sdk).`);
@@ -219518,9 +220024,9 @@ var syncIos = async (root, config) => {
219518
220024
  log(`${c.green("plugin")} ${id}@${plugin.lock.version} (${plugin.lock.tag} → Sources/${module})`);
219519
220025
  }
219520
220026
  const keep = new Set(["LeCodesRuntime", ...runtimePlugins.map((p2) => p2.module)]);
219521
- for (const entry of existsSync16(sourcesDir) ? readdirSync7(sourcesDir) : []) {
219522
- if (!keep.has(entry) && statSync5(join17(sourcesDir, entry)).isDirectory()) {
219523
- rmSync4(join17(sourcesDir, entry), { recursive: true });
220027
+ for (const entry of existsSync19(sourcesDir) ? readdirSync8(sourcesDir) : []) {
220028
+ if (!keep.has(entry) && statSync6(join20(sourcesDir, entry)).isDirectory()) {
220029
+ rmSync6(join20(sourcesDir, entry), { recursive: true });
219524
220030
  log(`${c.yellow("removed")} ios/LeCodesRuntime/Sources/${entry} (plugin uninstalled)`);
219525
220031
  }
219526
220032
  }
@@ -219528,11 +220034,11 @@ var syncIos = async (root, config) => {
219528
220034
  if (!Array.isArray(remotePatterns) || remotePatterns.some((p2) => typeof p2 !== "string")) {
219529
220035
  throw new CliError('app.json "resources.remote" must be an array of glob strings (e.g. ["videos/**"]).');
219530
220036
  }
219531
- const resourcesDir = join17(iosDir, "App", "Resources");
219532
- rmSync4(resourcesDir, { recursive: true, force: true });
219533
- mkdirSync11(resourcesDir, { recursive: true });
219534
- const resources = await compileShell({ _: [], flags: { o: join17(resourcesDir, "app.js") } }, { remotePatterns });
219535
- const bundleJs = readFileSync15(join17(resourcesDir, "app.js"), "utf8");
220037
+ const resourcesDir = join20(iosDir, "App", "Resources");
220038
+ rmSync6(resourcesDir, { recursive: true, force: true });
220039
+ mkdirSync13(resourcesDir, { recursive: true });
220040
+ const resources = await compileShell({ _: [], flags: { o: join20(resourcesDir, "app.js") } }, { remotePatterns });
220041
+ const bundleJs = readFileSync16(join20(resourcesDir, "app.js"), "utf8");
219536
220042
  const { variant, detected } = resolveVariant(requestedVariant, bundleJs);
219537
220043
  const WHY = {
219538
220044
  core: "UI only",
@@ -219546,8 +220052,8 @@ var syncIos = async (root, config) => {
219546
220052
  const sdkProvided = new Set;
219547
220053
  if (variant === "3d" || variant === "full") {
219548
220054
  const sdkResources = await ensureSdkResourcesBundle(sdkVersion);
219549
- cpSync3(sdkResources, join17(resourcesDir, "LeCodesSDKResources.bundle"), { recursive: true });
219550
- for (const f3 of readdirSync7(sdkResources))
220055
+ cpSync3(sdkResources, join20(resourcesDir, "LeCodesSDKResources.bundle"), { recursive: true });
220056
+ for (const f3 of readdirSync8(sdkResources))
219551
220057
  if (f3 !== "Info.plist")
219552
220058
  sdkProvided.add(f3);
219553
220059
  log(`${c.green("resources")} LeCodesSDKResources.bundle (filament) → ios/App/Resources/`);
@@ -219574,7 +220080,7 @@ var syncIos = async (root, config) => {
219574
220080
  }
219575
220081
  if (candidates.length > 1)
219576
220082
  warn(`asset name collision: ${name} (${candidates.map((r2) => r2.path).join(", ")}) — embedding ${candidates[0].path}`);
219577
- cpSync3(candidates[0].absPath, join17(resourcesDir, name));
220083
+ cpSync3(candidates[0].absPath, join20(resourcesDir, name));
219578
220084
  embedded++;
219579
220085
  }
219580
220086
  if (embedded > 0)
@@ -219594,14 +220100,14 @@ var syncIos = async (root, config) => {
219594
220100
  }
219595
220101
  if (shellUuid)
219596
220102
  log(`${c.green("identity")} project ${shellUuid} baked into the shell (world identity)`);
219597
- writeFileSync13(join17(iosDir, "README.md"), IOS_README);
219598
- writeFileSync13(join17(iosDir, "LeCodesRuntime", "Package.swift"), runtimePackageTemplate(sdkVersion, variant, runtimePlugins));
219599
- writeFileSync13(join17(sourcesDir, "LeCodesRuntime", "LeCodesRuntime.swift"), runtimeSwiftTemplate(runtimePlugins, shellUuid));
220103
+ writeFileSync15(join20(iosDir, "README.md"), IOS_README);
220104
+ writeFileSync15(join20(iosDir, "LeCodesRuntime", "Package.swift"), runtimePackageTemplate(sdkVersion, variant, runtimePlugins));
220105
+ writeFileSync15(join20(sourcesDir, "LeCodesRuntime", "LeCodesRuntime.swift"), runtimeSwiftTemplate(runtimePlugins, shellUuid));
219600
220106
  const mergedKeys = {};
219601
220107
  for (const [key, values] of Object.entries(infoValues))
219602
220108
  mergedKeys[key] = [...new Set(values)].join(" ");
219603
220109
  const updateUrl = resolveUpdateUrl(config.update, root);
219604
- writeFileSync13(join17(iosDir, "App", "Info.plist"), infoPlistTemplate(mergedKeys, updateUrl));
220110
+ writeFileSync15(join20(iosDir, "App", "Info.plist"), infoPlistTemplate(mergedKeys, updateUrl));
219605
220111
  if (updateUrl)
219606
220112
  log(`${c.green("update")} OTA source ${updateUrl} — checked natively at launch, new code applies next launch`);
219607
220113
  else if (!config.update) {
@@ -219612,10 +220118,10 @@ var syncIos = async (root, config) => {
219612
220118
  if (manifest)
219613
220119
  note(`tip: add "update": true to app.json for over-the-air JS updates — ship with \`lecodes push --publish\`, no store release`);
219614
220120
  }
219615
- if (pluginSpecs.length > 0 || existsSync16(lockPath))
220121
+ if (pluginSpecs.length > 0 || existsSync19(lockPath))
219616
220122
  writeJson(lockPath, { plugins: newLock });
219617
- const pbxPath = join17(iosDir, "App.xcodeproj", "project.pbxproj");
219618
- let pbx = readFileSync15(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};`);
220123
+ const pbxPath = join20(iosDir, "App.xcodeproj", "project.pbxproj");
220124
+ let pbx = readFileSync16(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};`);
219619
220125
  const mergedEntitlements = {};
219620
220126
  for (const [key, values] of Object.entries(entitlementValues)) {
219621
220127
  if (values.every((v) => Array.isArray(v))) {
@@ -219636,7 +220142,7 @@ var syncIos = async (root, config) => {
219636
220142
  }
219637
220143
  const hasEntitlements = Object.keys(mergedEntitlements).length > 0;
219638
220144
  if (pbx.includes("CODE_SIGN_ENTITLEMENTS") || hasEntitlements) {
219639
- writeFileSync13(join17(iosDir, "App", "App.entitlements"), entitlementsTemplate(mergedEntitlements));
220145
+ writeFileSync15(join20(iosDir, "App", "App.entitlements"), entitlementsTemplate(mergedEntitlements));
219640
220146
  if (hasEntitlements)
219641
220147
  log(`${c.green("caps")} App/App.entitlements: ${Object.keys(mergedEntitlements).sort().join(", ")}`);
219642
220148
  if (!pbx.includes("CODE_SIGN_ENTITLEMENTS")) {
@@ -219652,7 +220158,7 @@ $2Info.plist,`);
219652
220158
  }
219653
220159
  }
219654
220160
  }
219655
- writeFileSync13(pbxPath, pbx);
220161
+ writeFileSync15(pbxPath, pbx);
219656
220162
  if (config.icon) {
219657
220163
  await writeIosAppIcon(iosDir, await loadIconMaster(root, config));
219658
220164
  }
@@ -219664,7 +220170,7 @@ $2Info.plist,`);
219664
220170
  var appPlugin = async (args) => {
219665
220171
  const sub = args._[1];
219666
220172
  const root = projectRoot();
219667
- const configPath = join17(root, "app.json");
220173
+ const configPath = join20(root, "app.json");
219668
220174
  const config = readJson(configPath);
219669
220175
  if (!config)
219670
220176
  throw new CliError("No app.json here — run `lecodes app init <ios|android>` first.");
@@ -219675,7 +220181,7 @@ var appPlugin = async (args) => {
219675
220181
  log("No plugins installed.");
219676
220182
  return;
219677
220183
  }
219678
- const lock = readJson(join17(root, "plugins.lock.json"))?.plugins ?? {};
220184
+ const lock = readJson(join20(root, "plugins.lock.json"))?.plugins ?? {};
219679
220185
  for (const [id2, spec] of entries) {
219680
220186
  const pin = lock[id2] ? c.dim(` ${lock[id2].repo} ${lock[id2].tag} (${lock[id2].commit.slice(0, 7)})`) : c.dim(" (not synced)");
219681
220187
  log(`${c.bold(id2)} ${spec}${pin}`);
@@ -219728,14 +220234,14 @@ var exportOptionsPlist = (method, teamId, destination) => `<?xml version="1.0" e
219728
220234
  `;
219729
220235
  var requireShell = () => {
219730
220236
  const root = projectRoot();
219731
- const config = readJson(join17(root, "app.json"));
219732
- if (!config?.ios || !existsSync16(join17(root, "ios"))) {
220237
+ const config = readJson(join20(root, "app.json"));
220238
+ if (!config?.ios || !existsSync19(join20(root, "ios"))) {
219733
220239
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
219734
220240
  }
219735
220241
  return { root, config };
219736
220242
  };
219737
220243
  var updateSdkPin = async (root, config, args, checkOnly, platform) => {
219738
- const [label, repo, minVersion, shellVersion] = platform === "ios" ? ["lecodes-ios-sdk", SDK_REPO, SDK_MIN_VERSION, () => shellSdkVersion(join17(root, "ios"))] : ["lecodes-android-sdk", ANDROID_SDK_REPO, ANDROID_SDK_MIN_VERSION, () => shellSdkVersionAndroid(join17(root, "android"))];
220244
+ const [label, repo, minVersion, shellVersion] = platform === "ios" ? ["lecodes-ios-sdk", SDK_REPO, SDK_MIN_VERSION, () => shellSdkVersion(join20(root, "ios"))] : ["lecodes-android-sdk", ANDROID_SDK_REPO, ANDROID_SDK_MIN_VERSION, () => shellSdkVersionAndroid(join20(root, "android"))];
219739
220245
  const section = config[platform];
219740
220246
  const wanted = flagStr(args, "sdk");
219741
220247
  let changed = false;
@@ -219766,19 +220272,19 @@ var updateSdkPin = async (root, config, args, checkOnly, platform) => {
219766
220272
  }
219767
220273
  if (!checkOnly && nextPin !== pin) {
219768
220274
  section.sdk = nextPin;
219769
- writeJson(join17(root, "app.json"), config);
220275
+ writeJson(join20(root, "app.json"), config);
219770
220276
  }
219771
220277
  return changed;
219772
220278
  };
219773
220279
  var appUpdate = async (args) => {
219774
220280
  const root = projectRoot();
219775
- const config = readJson(join17(root, "app.json"));
220281
+ const config = readJson(join20(root, "app.json"));
219776
220282
  const platform = args._[1];
219777
220283
  if (platform !== undefined && platform !== "ios" && platform !== "android") {
219778
220284
  throw new CliError(`Unknown platform "${platform}" — use ios or android.`);
219779
220285
  }
219780
- const hasIos = !!config?.ios && existsSync16(join17(root, "ios"));
219781
- const hasAndroid = !!config?.android && existsSync16(join17(root, "android"));
220286
+ const hasIos = !!config?.ios && existsSync19(join20(root, "ios"));
220287
+ const hasAndroid = !!config?.android && existsSync19(join20(root, "android"));
219782
220288
  if (platform === "ios" && !hasIos)
219783
220289
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
219784
220290
  if (platform === "android" && !hasAndroid)
@@ -219796,7 +220302,7 @@ var appUpdate = async (args) => {
219796
220302
  changed = await updateSdkPin(root, config, args, checkOnly, "ios") || changed;
219797
220303
  if (doAndroid)
219798
220304
  changed = await updateSdkPin(root, config, args, checkOnly, "android") || changed;
219799
- const lockPath = join17(root, "plugins.lock.json");
220305
+ const lockPath = join20(root, "plugins.lock.json");
219800
220306
  const lock = readJson(lockPath)?.plugins ?? {};
219801
220307
  const specs = Object.entries(config.plugins ?? {});
219802
220308
  if (specs.length > 0 && !flagBool(args, "no-plugins")) {
@@ -219825,7 +220331,7 @@ var appUpdate = async (args) => {
219825
220331
  log(`${c.green("plugin")} ${id} ${entry?.tag ?? "—"} → ${c.bold(tag)}`);
219826
220332
  changed = true;
219827
220333
  }
219828
- if (!checkOnly && existsSync16(lockPath))
220334
+ if (!checkOnly && existsSync19(lockPath))
219829
220335
  writeJson(lockPath, { plugins: kept });
219830
220336
  }
219831
220337
  log("");
@@ -219845,9 +220351,9 @@ var appUpdate = async (args) => {
219845
220351
  };
219846
220352
  var appDoctor = async (_args) => {
219847
220353
  const root = projectRoot();
219848
- const config = readJson(join17(root, "app.json"));
219849
- const hasIos = !!config?.ios && existsSync16(join17(root, "ios"));
219850
- const hasAndroid = !!config?.android && existsSync16(join17(root, "android"));
220354
+ const config = readJson(join20(root, "app.json"));
220355
+ const hasIos = !!config?.ios && existsSync19(join20(root, "ios"));
220356
+ const hasAndroid = !!config?.android && existsSync19(join20(root, "android"));
219851
220357
  if (!hasIos && !hasAndroid) {
219852
220358
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
219853
220359
  }
@@ -219858,14 +220364,14 @@ var appDoctor = async (_args) => {
219858
220364
  failed++;
219859
220365
  };
219860
220366
  if (config?.icon) {
219861
- check(existsSync16(join17(root, config.icon)), `Icon master ${config.icon}`, `app.json "icon" points at a missing file — sync will fail`);
220367
+ check(existsSync19(join20(root, config.icon)), `Icon master ${config.icon}`, `app.json "icon" points at a missing file — sync will fail`);
219862
220368
  }
219863
220369
  if (hasIos)
219864
220370
  await doctorIosChecks(root, config, check);
219865
220371
  if (hasAndroid) {
219866
220372
  await doctorAndroidChecks(root, config, check);
219867
220373
  if (!hasIos) {
219868
- const lock = readJson(join17(root, "plugins.lock.json"))?.plugins ?? {};
220374
+ const lock = readJson(join20(root, "plugins.lock.json"))?.plugins ?? {};
219869
220375
  const wanted = Object.keys(config.plugins ?? {});
219870
220376
  const inSync = wanted.every((id) => lock[id]) && Object.keys(lock).every((id) => wanted.includes(id));
219871
220377
  check(wanted.length === 0 || inSync, "Plugins synced", "run `lecodes app sync`");
@@ -219884,22 +220390,22 @@ var doctorIosChecks = async (root, config, check) => {
219884
220390
  } catch {}
219885
220391
  check(xcodeVersion !== "", `Xcode CLI (${xcodeVersion || "not found"})`, "install Xcode + command line tools");
219886
220392
  check(!!config.ios.teamId, "Development team set", "lecodes app init wrote app.json — fill ios.teamId", false);
219887
- const iconDir = join17(root, "ios", "App", "Assets.xcassets", "AppIcon.appiconset");
219888
- const hasIcon = existsSync16(iconDir) && readdirSync7(iconDir).some((f3) => f3.endsWith(".png"));
220393
+ const iconDir = join20(root, "ios", "App", "Assets.xcassets", "AppIcon.appiconset");
220394
+ const hasIcon = existsSync19(iconDir) && readdirSync8(iconDir).some((f3) => f3.endsWith(".png"));
219889
220395
  check(hasIcon, "App icon present", "drop a 1024×1024 PNG into ios/App/Assets.xcassets/AppIcon.appiconset/ (App Store requires it)", false);
219890
- const bundle = join17(root, "ios", "App", "Resources", "app.js");
219891
- check(existsSync16(bundle), "Bundled app.js", "run `lecodes app sync`");
220396
+ const bundle = join20(root, "ios", "App", "Resources", "app.js");
220397
+ check(existsSync19(bundle), "Bundled app.js", "run `lecodes app sync`");
219892
220398
  const requestedVariant = config.ios.variant ?? "auto";
219893
220399
  const validVariant = requestedVariant === "auto" || SDK_VARIANTS.includes(requestedVariant);
219894
220400
  check(validVariant, `ios.variant "${requestedVariant}"`, `use "auto" or one of ${SDK_VARIANTS.join(", ")}`);
219895
- if (existsSync16(bundle) && validVariant) {
219896
- const { variant } = resolveVariant(requestedVariant, readFileSync15(bundle, "utf8"));
219897
- const pkg = readFileSync15(join17(root, "ios", "LeCodesRuntime", "Package.swift"), "utf8");
220401
+ if (existsSync19(bundle) && validVariant) {
220402
+ const { variant } = resolveVariant(requestedVariant, readFileSync16(bundle, "utf8"));
220403
+ const pkg = readFileSync16(join20(root, "ios", "LeCodesRuntime", "Package.swift"), "utf8");
219898
220404
  const needsResources = variant === "3d" || variant === "full";
219899
- const hasResources = existsSync16(join17(root, "ios", "App", "Resources", "LeCodesSDKResources.bundle"));
220405
+ const hasResources = existsSync19(join20(root, "ios", "App", "Resources", "LeCodesSDKResources.bundle"));
219900
220406
  check(pkg.includes(`"LeCodesSDK-${variant}"`) && (!needsResources || hasResources), `SDK variant ${variant}${needsResources ? " (+ filament resources)" : ""}`, "run `lecodes app sync`");
219901
220407
  }
219902
- const linked = shellSdkVersion(join17(root, "ios"));
220408
+ const linked = shellSdkVersion(join20(root, "ios"));
219903
220409
  const pin = config.ios.sdk ?? "latest";
219904
220410
  check(!!linked, `SDK ${linked ?? "version unreadable"}`, "run `lecodes app sync`");
219905
220411
  if (linked && pin !== "latest") {
@@ -219911,27 +220417,27 @@ var doctorIosChecks = async (root, config, check) => {
219911
220417
  check(cmpVersion(linked, latest) >= 0, `SDK is the latest release (${latest})`, "run `lecodes app update`", false);
219912
220418
  } catch {}
219913
220419
  }
219914
- const lock = readJson(join17(root, "plugins.lock.json"))?.plugins ?? {};
220420
+ const lock = readJson(join20(root, "plugins.lock.json"))?.plugins ?? {};
219915
220421
  const wanted = Object.keys(config.plugins ?? {});
219916
220422
  const inSync = wanted.every((id) => lock[id]) && Object.keys(lock).every((id) => wanted.includes(id));
219917
220423
  check(wanted.length === 0 || inSync, "Plugins synced", "run `lecodes app sync`");
219918
- const pbxFile = join17(root, "ios", "App.xcodeproj", "project.pbxproj");
219919
- if (existsSync16(pbxFile) && readFileSync15(pbxFile, "utf8").includes("CODE_SIGN_ENTITLEMENTS")) {
219920
- check(existsSync16(join17(root, "ios", "App", "App.entitlements")), "App/App.entitlements present", "run `lecodes app sync`");
220424
+ const pbxFile = join20(root, "ios", "App.xcodeproj", "project.pbxproj");
220425
+ if (existsSync19(pbxFile) && readFileSync16(pbxFile, "utf8").includes("CODE_SIGN_ENTITLEMENTS")) {
220426
+ check(existsSync19(join20(root, "ios", "App", "App.entitlements")), "App/App.entitlements present", "run `lecodes app sync`");
219921
220427
  }
219922
220428
  if (wanted.includes("push")) {
219923
- const runtimeSwift = join17(root, "ios", "LeCodesRuntime", "Sources", "LeCodesRuntime", "LeCodesRuntime.swift");
219924
- const baked = existsSync16(runtimeSwift) && readFileSync15(runtimeSwift, "utf8").includes("bootProjectUuid");
220429
+ const runtimeSwift = join20(root, "ios", "LeCodesRuntime", "Sources", "LeCodesRuntime", "LeCodesRuntime.swift");
220430
+ const baked = existsSync19(runtimeSwift) && readFileSync16(runtimeSwift, "utf8").includes("bootProjectUuid");
219925
220431
  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);
219926
220432
  }
219927
220433
  if (config.update) {
219928
- const plist = readFileSync15(join17(root, "ios", "App", "Info.plist"), "utf8");
220434
+ const plist = readFileSync16(join20(root, "ios", "App", "Info.plist"), "utf8");
219929
220435
  check(plist.includes("<key>LeCodesUpdateURL</key>"), "OTA update URL in Info.plist", "run `lecodes app sync`", false);
219930
- const appDir = join17(root, "ios", "App");
219931
- const callsUpdater = readdirSync7(appDir).some((f3) => f3.endsWith(".swift") && readFileSync15(join17(appDir, f3), "utf8").includes("LeCodesUpdater.activeBundle"));
220436
+ const appDir = join20(root, "ios", "App");
220437
+ const callsUpdater = readdirSync8(appDir).some((f3) => f3.endsWith(".swift") && readFileSync16(join20(appDir, f3), "utf8").includes("LeCodesUpdater.activeBundle"));
219932
220438
  check(callsUpdater, "App code launches via LeCodesUpdater", `no App/*.swift calls LeCodesUpdater.activeBundle() — downloaded updates would never run; see "Over-the-air updates" in ios/README.md for the ViewController snippet`, false);
219933
220439
  }
219934
- const asc = readJson(join17(homedir6(), ".lecodes", "asc.json"));
220440
+ const asc = readJson(join20(homedir7(), ".lecodes", "asc.json"));
219935
220441
  check(!!asc?.keyPath, "App Store Connect API key configured", "pass --key-path/--key-id/--issuer-id to `lecodes app upload` once to store it", false);
219936
220442
  };
219937
220443
  var ARCHIVE_PATH = "ios/build/App.xcarchive";
@@ -219967,8 +220473,8 @@ var appBuild = async (args) => {
219967
220473
  success(`Archive: ios/build/App.xcarchive`);
219968
220474
  if (flagBool(args, "no-export"))
219969
220475
  return;
219970
- const optionsPath = join17(root, "ios", "build", "ExportOptions.plist");
219971
- writeFileSync13(optionsPath, exportOptionsPlist(method, config.ios.teamId ?? ""));
220476
+ const optionsPath = join20(root, "ios", "build", "ExportOptions.plist");
220477
+ writeFileSync15(optionsPath, exportOptionsPlist(method, config.ios.teamId ?? ""));
219972
220478
  note(`Exporting (.ipa, method ${method})…`);
219973
220479
  runTool(root, "xcodebuild", [
219974
220480
  "-exportArchive",
@@ -219987,10 +220493,10 @@ var appUpload = async (args) => {
219987
220493
  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).");
219988
220494
  }
219989
220495
  const { root, config } = requireShell();
219990
- if (!existsSync16(join17(root, ARCHIVE_PATH))) {
220496
+ if (!existsSync19(join20(root, ARCHIVE_PATH))) {
219991
220497
  throw new CliError("No archive at ios/build/App.xcarchive — run `lecodes app build ios` first.");
219992
220498
  }
219993
- const ascPath = join17(homedir6(), ".lecodes", "asc.json");
220499
+ const ascPath = join20(homedir7(), ".lecodes", "asc.json");
219994
220500
  const stored = readJson(ascPath) ?? {};
219995
220501
  const keyPath = flagStr(args, "key-path") ?? stored.keyPath;
219996
220502
  const keyId = flagStr(args, "key-id") ?? stored.keyId;
@@ -219999,15 +220505,15 @@ var appUpload = async (args) => {
219999
220505
  throw new CliError(`App Store Connect API key needed: --key-path <AuthKey.p8> --key-id <id> --issuer-id <id>
220000
220506
  (create one at appstoreconnect.apple.com → Users and Access → Integrations; stored in ~/.lecodes/asc.json after first use)`);
220001
220507
  }
220002
- if (!existsSync16(keyPath))
220508
+ if (!existsSync19(keyPath))
220003
220509
  throw new CliError(`API key file not found: ${keyPath}`);
220004
220510
  if (flagStr(args, "key-path")) {
220005
- mkdirSync11(dirname12(ascPath), { recursive: true });
220511
+ mkdirSync13(dirname12(ascPath), { recursive: true });
220006
220512
  writeJson(ascPath, { keyPath, keyId, issuerId });
220007
220513
  note("Stored the key reference in ~/.lecodes/asc.json");
220008
220514
  }
220009
- const optionsPath = join17(root, "ios", "build", "UploadOptions.plist");
220010
- writeFileSync13(optionsPath, exportOptionsPlist("app-store-connect", config.ios.teamId ?? "", "upload"));
220515
+ const optionsPath = join20(root, "ios", "build", "UploadOptions.plist");
220516
+ writeFileSync15(optionsPath, exportOptionsPlist("app-store-connect", config.ios.teamId ?? "", "upload"));
220011
220517
  note("Uploading to App Store Connect… (processing continues on Apple's side)");
220012
220518
  runTool(root, "xcodebuild", [
220013
220519
  "-exportArchive",
@@ -220018,14 +220524,14 @@ var appUpload = async (args) => {
220018
220524
  "-exportPath",
220019
220525
  "ios/build/upload",
220020
220526
  "-authenticationKeyPath",
220021
- resolve6(keyPath),
220527
+ resolve7(keyPath),
220022
220528
  "-authenticationKeyID",
220023
220529
  keyId,
220024
220530
  "-authenticationKeyIssuerID",
220025
220531
  issuerId,
220026
220532
  "-allowProvisioningUpdates"
220027
220533
  ]);
220028
- const configPath = join17(root, "app.json");
220534
+ const configPath = join20(root, "app.json");
220029
220535
  const fresh = readJson(configPath);
220030
220536
  fresh.ios.buildNumber = (fresh.ios.buildNumber ?? 1) + 1;
220031
220537
  writeJson(configPath, fresh);
@@ -220036,15 +220542,15 @@ var appOpen = async (args) => {
220036
220542
  const root = projectRoot();
220037
220543
  let platform = args._[1];
220038
220544
  if (!platform) {
220039
- const hasIos = existsSync16(join17(root, "ios"));
220040
- const hasAndroid = existsSync16(join17(root, "android"));
220545
+ const hasIos = existsSync19(join20(root, "ios"));
220546
+ const hasAndroid = existsSync19(join20(root, "android"));
220041
220547
  if (hasIos && hasAndroid)
220042
220548
  throw new CliError("Both shells exist — `lecodes app open ios` or `lecodes app open android`.");
220043
220549
  platform = hasAndroid ? "android" : "ios";
220044
220550
  }
220045
220551
  if (platform === "ios") {
220046
- const proj = join17(root, "ios", "App.xcodeproj");
220047
- if (!existsSync16(proj))
220552
+ const proj = join20(root, "ios", "App.xcodeproj");
220553
+ if (!existsSync19(proj))
220048
220554
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
220049
220555
  if (process.platform !== "darwin")
220050
220556
  throw new CliError("Opening Xcode requires macOS.");
@@ -220081,8 +220587,8 @@ var app = async (args) => {
220081
220587
  init_api();
220082
220588
  init_config();
220083
220589
  init_manifest();
220084
- import { existsSync as existsSync18, mkdirSync as mkdirSync13, readdirSync as readdirSync9 } from "node:fs";
220085
- import { resolve as resolve7 } from "node:path";
220590
+ import { existsSync as existsSync21, mkdirSync as mkdirSync15, readdirSync as readdirSync10 } from "node:fs";
220591
+ import { resolve as resolve8 } from "node:path";
220086
220592
  init_types4();
220087
220593
  init_ignore();
220088
220594
  init_util();
@@ -220103,11 +220609,11 @@ var clone3 = async (args) => {
220103
220609
  const uuid = parseUuid(args._[0]);
220104
220610
  const project = await getProject(apiUrl, token, uuid);
220105
220611
  const dir = args._[1] ?? slug(project.name);
220106
- const root = resolve7(dir);
220107
- if (existsSync18(root) && readdirSync9(root).length > 0) {
220612
+ const root = resolve8(dir);
220613
+ if (existsSync21(root) && readdirSync10(root).length > 0) {
220108
220614
  throw new CliError(`Directory "${dir}" already exists and is not empty.`);
220109
220615
  }
220110
- mkdirSync13(root, { recursive: true });
220616
+ mkdirSync15(root, { recursive: true });
220111
220617
  info(c.dim(`Fetching project ${c.bold(project.name)}…`));
220112
220618
  const files = await materializeAssets(root, project, apiUrl, token, progress);
220113
220619
  endProgress();
@@ -220130,8 +220636,8 @@ init_config();
220130
220636
  init_manifest();
220131
220637
  init_types4();
220132
220638
  init_ignore();
220133
- import { existsSync as existsSync19, mkdirSync as mkdirSync14, readdirSync as readdirSync10, writeFileSync as writeFileSync15 } from "node:fs";
220134
- import { join as join19, resolve as resolve8 } from "node:path";
220639
+ import { existsSync as existsSync22, mkdirSync as mkdirSync16, readdirSync as readdirSync11, writeFileSync as writeFileSync17 } from "node:fs";
220640
+ import { join as join22, resolve as resolve9 } from "node:path";
220135
220641
 
220136
220642
  // src/commands/projectTemplates.ts
220137
220643
  var appClaudeMd = (name) => `# ${name} — LeCodes app
@@ -220173,7 +220679,11 @@ are the regression suite. A failing step dumps the failing frame (JSON + PNG) un
220173
220679
 
220174
220680
  One action per step (\`tap\`, \`type\`+\`text\`, \`fill\`, \`scroll\` [+\`by\`], \`wait\`, and for
220175
220681
  games: \`frames\` (advance N fixed 1/60 steps, no sleeping), \`drag\`+\`to\` [+\`ms\`], \`hold\`+\`ms\`,
220176
- \`key\`+\`ms\` / \`keyDown\` / \`keyUp\` (KeyboardEvent.code: "ArrowRight", "KeyW", "Space")), then an
220682
+ \`key\`+\`ms\` / \`keyDown\` / \`keyUp\` (KeyboardEvent.code: "ArrowRight", "KeyW", "Space" — mouse
220683
+ buttons are keys too: "MouseLeft"/"MouseRight"), \`look: [dx, dy]\` [+\`ms\`] (mouse motion for
220684
+ \`Input.mouse.delta\`), \`mouseTo: [x, y]\` (cursor position), \`wheel: [dx, dy]\`, gamepad:
220685
+ \`stick: { left: [x, y], right: [x, y], triggers: [l, r] }\` (values persist; \`index\` = pad),
220686
+ \`gamepad: "connect" | "disconnect"\`, pad buttons as keys: \`keyDown: "GamepadSouth"\`), then an
220177
220687
  optional \`expect\` (\`screen\`, \`node\`/\`notNode\`, \`text\`/\`notText\`,
220178
220688
  \`value: { target, equals }\`, \`log\` — a \`console.log\` line contains it), then outputs
220179
220689
  (\`screenshot\` [+\`clip\`: a node name or \`{ x, y, width, height }\`], \`json\`). Targets resolve by
@@ -220192,7 +220702,9 @@ the 2D scene (sprite \`click\`/\`touchstart\`, \`ev.track()\`). Envelope keys: \
220192
220702
 
220193
220703
  Game time is deterministic headless: \`setLoop\`, 2D physics/animation and aspect \`update(dt)\`
220194
220704
  advance in fixed 1/60 steps driven by \`wait\`/\`frames\`/\`drag\`/\`key\` — never by a wall clock —
220195
- so a flow that passes once passes every time. \`Input.key(code)\` reads the keys a flow holds.
220705
+ so a flow that passes once passes every time. \`Input.key(code)\` reads the keys a flow holds and
220706
+ \`Input.on('keydown' | 'keyup', e => e.code)\` fires for each \`key\`/\`keyDown\`/\`keyUp\` step
220707
+ (events land before the next loop tick); \`stick\` values are what \`Input.gamepad(0).axis()\` reads.
220196
220708
  Headless draws UI + the 2D scene (sprites, tilemaps, Box2D); 3D is captured as JSON and Jolt does
220197
220709
  not simulate — use \`--desktop\` for 3D pixels. \`console.log\` + \`expect.log\` is the assertion
220198
220710
  channel for game state that has no UI.
@@ -220262,21 +220774,21 @@ var create = async (args) => {
220262
220774
  if (!name)
220263
220775
  throw new CliError("Usage: lecodes create <name> [directory]");
220264
220776
  const dir = args._[1] ?? slug2(name);
220265
- const root = resolve8(dir);
220266
- if (existsSync19(root) && readdirSync10(root).length > 0) {
220777
+ const root = resolve9(dir);
220778
+ if (existsSync22(root) && readdirSync11(root).length > 0) {
220267
220779
  throw new CliError(`Directory "${dir}" already exists and is not empty.`);
220268
220780
  }
220269
- mkdirSync14(root, { recursive: true });
220781
+ mkdirSync16(root, { recursive: true });
220270
220782
  info(c.dim(`Creating project ${c.bold(name)}…`));
220271
220783
  const { uuid } = await createProject(apiUrl, token, name, flagStr(args, "team"));
220272
220784
  info(c.dim("Fetching type definitions…"));
220273
220785
  await materializeTypes(root, apiUrl);
220274
220786
  writeTsconfig(root);
220275
220787
  writeDefaultIgnore(root);
220276
- writeFileSync15(join19(root, "main.ts"), STARTER);
220277
- writeFileSync15(join19(root, "CLAUDE.md"), appClaudeMd(name));
220278
- mkdirSync14(join19(root, ".claude"), { recursive: true });
220279
- writeFileSync15(join19(root, ".claude", "settings.json"), CLAUDE_SETTINGS);
220788
+ writeFileSync17(join22(root, "main.ts"), STARTER);
220789
+ writeFileSync17(join22(root, "CLAUDE.md"), appClaudeMd(name));
220790
+ mkdirSync16(join22(root, ".claude"), { recursive: true });
220791
+ writeFileSync17(join22(root, ".claude", "settings.json"), CLAUDE_SETTINGS);
220280
220792
  const commits = await getCommits(apiUrl, token, uuid);
220281
220793
  writeManifest(root, { uuid, apiUrl, name, baseHeadId: commits.headId, files: {} });
220282
220794
  success(`Created ${c.bold(name)} in ${c.bold(dir)}/`);
@@ -220298,6 +220810,15 @@ var resolveScale = (flag, cfg) => {
220298
220810
  }
220299
220811
  return n2;
220300
220812
  };
220813
+ var resolveRenderScale = (flag, cfg) => {
220814
+ const raw = flag ?? cfg;
220815
+ if (raw === undefined)
220816
+ return;
220817
+ const n2 = Number(raw);
220818
+ if (!Number.isFinite(n2) || n2 < 0.25 || n2 > 1)
220819
+ throw new CliError(`render-scale must be a number 0.25..1 (got "${raw}")`);
220820
+ return n2;
220821
+ };
220301
220822
  var run = async (args) => {
220302
220823
  let root;
220303
220824
  try {
@@ -220317,7 +220838,9 @@ var run = async (args) => {
220317
220838
  resizable: win?.resizable,
220318
220839
  title: flagStr(args, "title") ?? win?.title ?? appCfg?.name,
220319
220840
  fullscreen: flagBool(args, "fullscreen") ? true : flagBool(args, "windowed") ? false : win?.fullscreen,
220320
- scale: resolveScale(flagStr(args, "scale"), win?.scale)
220841
+ scale: resolveScale(flagStr(args, "scale"), win?.scale),
220842
+ renderScale: resolveRenderScale(flagStr(args, "render-scale"), appCfg?.desktop?.renderScale),
220843
+ maxRenderSize: appCfg?.desktop?.maxRenderSize
220321
220844
  });
220322
220845
  };
220323
220846
  var status = async () => {
@@ -220353,15 +220876,16 @@ init_design();
220353
220876
  // src/commands/dev.ts
220354
220877
  init_manifest();
220355
220878
  init_ignore();
220356
- import { existsSync as existsSync21, mkdirSync as mkdirSync16, readFileSync as readFileSync19, watch, writeFileSync as writeFileSync17 } from "node:fs";
220357
- import { randomBytes as randomBytes2 } from "node:crypto";
220879
+ import { existsSync as existsSync24, mkdirSync as mkdirSync18, readFileSync as readFileSync20, watch, writeFileSync as writeFileSync19 } from "node:fs";
220880
+ import { randomBytes as randomBytes3 } from "node:crypto";
220358
220881
  import { networkInterfaces } from "node:os";
220359
- import { basename as basename9, join as join21 } from "node:path";
220882
+ import { basename as basename10, join as join25 } from "node:path";
220360
220883
 
220361
220884
  // src/dev/devServer.ts
220362
220885
  init_localFiles();
220363
220886
  import { createServer as createServer2 } from "node:http";
220364
- import { createReadStream, statSync as statSync7 } from "node:fs";
220887
+ import { createReadStream, statSync as statSync8 } from "node:fs";
220888
+ import { join as join24 } from "node:path";
220365
220889
  init_projectCompile();
220366
220890
  init_fonts();
220367
220891
  init_assetIcons();
@@ -220485,9 +221009,9 @@ var renderPrelude = (p2) => DEV_PRELUDE_JS.split("__DEV_ORIGIN__").join(p2.origi
220485
221009
  var renderDevScreen = (name) => DEV_SCREEN_TS.split("__DEV_NAME__").join(JSON.stringify(name));
220486
221010
 
220487
221011
  // src/dev/wsServer.ts
220488
- import { createHash as createHash6 } from "node:crypto";
221012
+ import { createHash as createHash7 } from "node:crypto";
220489
221013
  var WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
220490
- var acceptKey = (key) => createHash6("sha1").update(key + WS_GUID).digest("base64");
221014
+ var acceptKey = (key) => createHash7("sha1").update(key + WS_GUID).digest("base64");
220491
221015
  var encodeTextFrame = (text) => {
220492
221016
  const payload = Buffer.from(text, "utf8");
220493
221017
  const len = payload.length;
@@ -220635,14 +221159,14 @@ var sendText = (res, status2, type, body) => {
220635
221159
  res.end(body);
220636
221160
  };
220637
221161
  var sendJson = (res, status2, body) => sendText(res, status2, "application/json", JSON.stringify(body));
220638
- var readBody = (req) => new Promise((resolve9) => {
221162
+ var readBody = (req) => new Promise((resolve10) => {
220639
221163
  let body = "";
220640
221164
  req.on("data", (c4) => {
220641
221165
  if (body.length < 256 * 1024)
220642
221166
  body += c4.toString("utf8");
220643
221167
  });
220644
- req.on("end", () => resolve9(body));
220645
- req.on("error", () => resolve9(body));
221168
+ req.on("end", () => resolve10(body));
221169
+ req.on("error", () => resolve10(body));
220646
221170
  });
220647
221171
  var startDevServer = async (options) => {
220648
221172
  const { root, manifest, name, origin, token, entryOverride } = options;
@@ -220654,7 +221178,8 @@ var startDevServer = async (options) => {
220654
221178
  const stripVolatile = (js) => js.replace(/^\/\/ last-updated: .*$/m, "");
220655
221179
  const compileOnce = async () => {
220656
221180
  try {
220657
- const { entries, warnings } = collectLocalEntries(root, manifest, `${base}/assets`);
221181
+ const { entries, warnings } = collectLocalEntries(root, manifest, `${base}/assets`, { localShaders: true });
221182
+ warnings.push(...(await applyLocalShaders(entries, { root, srcPrefix: `${base}/shaders` })).warnings);
220658
221183
  const fontsHeader = await applyProjectFonts(entries, {
220659
221184
  mode: "vendor",
220660
221185
  root,
@@ -220742,6 +221267,21 @@ var startDevServer = async (options) => {
220742
221267
  printDeviceMessage(await readBody(req));
220743
221268
  return sendJson(res, 200, { ok: true });
220744
221269
  }
221270
+ if (route.startsWith("/shaders/")) {
221271
+ const name2 = route.slice("/shaders/".length);
221272
+ if (!isShaderArtifactName(name2))
221273
+ return sendText(res, 400, "text/plain", "Bad path");
221274
+ const abs = join24(shadersCacheDir(root), name2);
221275
+ try {
221276
+ if (!statSync8(abs).isFile())
221277
+ throw new Error("not a file");
221278
+ } catch {
221279
+ return sendText(res, 404, "text/plain", "Not found");
221280
+ }
221281
+ res.writeHead(200, { "content-type": "application/octet-stream", "cache-control": "no-store" });
221282
+ createReadStream(abs).pipe(res);
221283
+ return;
221284
+ }
220745
221285
  if (route.startsWith("/assets/")) {
220746
221286
  let key;
220747
221287
  try {
@@ -220750,7 +221290,7 @@ var startDevServer = async (options) => {
220750
221290
  return sendText(res, 400, "text/plain", "Bad path");
220751
221291
  }
220752
221292
  const abs = resolveLocalPath(root, key);
220753
- if (!abs || !statSync7(abs).isFile())
221293
+ if (!abs || !statSync8(abs).isFile())
220754
221294
  return sendText(res, 404, "text/plain", "Not found");
220755
221295
  res.writeHead(200, { "content-type": contentType(key), "cache-control": "no-store" });
220756
221296
  createReadStream(abs).pipe(res);
@@ -220793,11 +221333,11 @@ var startDevServer = async (options) => {
220793
221333
  if (conn)
220794
221334
  clients.add(conn);
220795
221335
  });
220796
- await new Promise((resolve9, reject) => {
221336
+ await new Promise((resolve10, reject) => {
220797
221337
  server2.once("error", reject);
220798
221338
  server2.listen(options.port, options.bind ?? "0.0.0.0", () => {
220799
221339
  server2.removeListener("error", reject);
220800
- resolve9();
221340
+ resolve10();
220801
221341
  });
220802
221342
  });
220803
221343
  const address = server2.address();
@@ -220845,24 +221385,24 @@ var lanCandidates = () => {
220845
221385
  return out.sort((x2, y2) => x2.rank - y2.rank).map(({ address, iface }) => ({ address, iface }));
220846
221386
  };
220847
221387
  var loadDevToken = (root) => {
220848
- const path2 = join21(root, LECODES_DIR, "dev.json");
221388
+ const path2 = join25(root, LECODES_DIR, "dev.json");
220849
221389
  try {
220850
- const token2 = JSON.parse(readFileSync19(path2, "utf8")).token;
221390
+ const token2 = JSON.parse(readFileSync20(path2, "utf8")).token;
220851
221391
  if (token2 && /^[a-z0-9]{8,}$/.test(token2))
220852
221392
  return token2;
220853
221393
  } catch {}
220854
- const token = randomBytes2(6).toString("hex");
220855
- mkdirSync16(join21(root, LECODES_DIR), { recursive: true });
220856
- writeFileSync17(path2, JSON.stringify({ token }, null, 2) + `
221394
+ const token = randomBytes3(6).toString("hex");
221395
+ mkdirSync18(join25(root, LECODES_DIR), { recursive: true });
221396
+ writeFileSync19(path2, JSON.stringify({ token }, null, 2) + `
220857
221397
  `);
220858
221398
  return token;
220859
221399
  };
220860
221400
  var printQr = async (url) => {
220861
221401
  try {
220862
221402
  const qrcode = (await Promise.resolve().then(() => __toESM(require_main(), 1))).default;
220863
- await new Promise((resolve9) => qrcode.generate(url, { small: true }, (out) => {
221403
+ await new Promise((resolve10) => qrcode.generate(url, { small: true }, (out) => {
220864
221404
  log(out);
220865
- resolve9();
221405
+ resolve10();
220866
221406
  }));
220867
221407
  } catch {
220868
221408
  note("(couldn't render a QR code — open the URL below on the device)");
@@ -220878,7 +221418,7 @@ var dev = async (args) => {
220878
221418
  root = process.cwd();
220879
221419
  manifest = null;
220880
221420
  }
220881
- const name = manifest?.name ?? basename9(root);
221421
+ const name = manifest?.name ?? basename10(root);
220882
221422
  if (typeof globalThis.Bun !== "undefined") {
220883
221423
  warnErr("Running under Bun: the websocket reload channel doesn't work here — run `lecodes dev` with Node (or use ?t=poll on the device URL).");
220884
221424
  }
@@ -220970,7 +221510,7 @@ ${result.error}`);
220970
221510
  if (alternatives.length > 0) {
220971
221511
  note(`Device can't reach it? This machine also has ${alternatives.map((a2) => `${a2.address} (${a2.iface})`).join(", ")} — retry with --host <ip>.`);
220972
221512
  }
220973
- if (existsSync21(join21(root, LECODES_DIR, "manifest.json")))
221513
+ if (existsSync24(join25(root, LECODES_DIR, "manifest.json")))
220974
221514
  note(`Token stored in ${LECODES_DIR}/dev.json (delete it to rotate the URL).`);
220975
221515
  log("");
220976
221516
  log(`Watching ${root} — save a file to hot-reload. Ctrl+C to stop.`);
@@ -220998,8 +221538,8 @@ init_localFiles();
220998
221538
 
220999
221539
  // src/serverDiff.ts
221000
221540
  init_localFiles();
221001
- import { readFileSync as readFileSync20 } from "node:fs";
221002
- import { basename as basename10 } from "node:path";
221541
+ import { readFileSync as readFileSync21 } from "node:fs";
221542
+ import { basename as basename11 } from "node:path";
221003
221543
  var isTextAsset = (t2) => t2 === "text" || t2 === "shader" || t2 === "scene";
221004
221544
  var computeServerDiff = (project, scan2) => {
221005
221545
  const pathById = buildPaths(project.assets);
@@ -221016,8 +221556,8 @@ var computeServerDiff = (project, scan2) => {
221016
221556
  const server2 = serverByPath.get(path2);
221017
221557
  const local = localByPath.get(path2);
221018
221558
  if (local && !server2) {
221019
- const text = isTextKind(classifyKind(basename10(path2)));
221020
- entries.push({ path: path2, status: "added", binary: !text, serverText: "", localText: text ? readFileSync20(local.absPath, "utf8") : undefined });
221559
+ const text = isTextKind(classifyKind(basename11(path2)));
221560
+ entries.push({ path: path2, status: "added", binary: !text, serverText: "", localText: text ? readFileSync21(local.absPath, "utf8") : undefined });
221021
221561
  continue;
221022
221562
  }
221023
221563
  if (server2 && !local) {
@@ -221028,12 +221568,12 @@ var computeServerDiff = (project, scan2) => {
221028
221568
  if (server2 && local) {
221029
221569
  if (isTextAsset(server2.type)) {
221030
221570
  const serverText = server2.text ?? "";
221031
- const localText = readFileSync20(local.absPath, "utf8");
221571
+ const localText = readFileSync21(local.absPath, "utf8");
221032
221572
  if (serverText !== localText)
221033
221573
  entries.push({ path: path2, status: "modified", binary: false, serverText, localText });
221034
221574
  } else {
221035
221575
  const serverSize = server2.file?.size;
221036
- const localSize = Buffer.byteLength(readFileSync20(local.absPath));
221576
+ const localSize = Buffer.byteLength(readFileSync21(local.absPath));
221037
221577
  if (serverSize !== undefined && serverSize !== localSize) {
221038
221578
  entries.push({ path: path2, status: "modified", binary: true });
221039
221579
  }
@@ -221191,8 +221731,8 @@ var statusLabel = (e2) => e2.status === "added" ? c.green(" added ") : e2.st
221191
221731
  init_manifest();
221192
221732
  init_types4();
221193
221733
  init_ignore();
221194
- import { existsSync as existsSync22, mkdirSync as mkdirSync17, writeFileSync as writeFileSync18 } from "node:fs";
221195
- import { basename as basename11, dirname as dirname14, join as join22, resolve as resolve9 } from "node:path";
221734
+ import { existsSync as existsSync25, mkdirSync as mkdirSync19, writeFileSync as writeFileSync20 } from "node:fs";
221735
+ import { basename as basename12, dirname as dirname14, join as join26, resolve as resolve10 } from "node:path";
221196
221736
  init_util();
221197
221737
  var STARTER2 = `// Welcome to LeCodes — the whole SDK is available as globals, no imports needed.
221198
221738
  // Run it live on a device with \`lecodes dev\`, or bundle it with \`lecodes compile -o app.js\`.
@@ -221300,18 +221840,18 @@ Full command list: \`lecodes --help\`.
221300
221840
  `;
221301
221841
  var init2 = async (args) => {
221302
221842
  const dirArg = (args._[0] ?? ".").replace(/[/\\]+$/, "") || ".";
221303
- const root = resolve9(dirArg);
221304
- const name = basename11(root);
221305
- mkdirSync17(root, { recursive: true });
221843
+ const root = resolve10(dirArg);
221844
+ const name = basename12(root);
221845
+ mkdirSync19(root, { recursive: true });
221306
221846
  let created = 0;
221307
221847
  const write = (rel, content) => {
221308
- const abs = join22(root, rel);
221309
- if (existsSync22(abs)) {
221848
+ const abs = join26(root, rel);
221849
+ if (existsSync25(abs)) {
221310
221850
  note(`kept ${rel}`);
221311
221851
  return;
221312
221852
  }
221313
- mkdirSync17(dirname14(abs), { recursive: true });
221314
- writeFileSync18(abs, content);
221853
+ mkdirSync19(dirname14(abs), { recursive: true });
221854
+ writeFileSync20(abs, content);
221315
221855
  log(`${c.green("created")} ${rel}`);
221316
221856
  created++;
221317
221857
  };
@@ -221358,8 +221898,8 @@ init_config();
221358
221898
  init_ignore();
221359
221899
  init_localFiles();
221360
221900
  init_manifest();
221361
- import { existsSync as existsSync23 } from "node:fs";
221362
- import { basename as basename13, join as join23, resolve as resolve10 } from "node:path";
221901
+ import { existsSync as existsSync26 } from "node:fs";
221902
+ import { basename as basename14, join as join27, resolve as resolve11 } from "node:path";
221363
221903
  init_types4();
221364
221904
  init_util();
221365
221905
 
@@ -221368,8 +221908,8 @@ init_api();
221368
221908
  init_config();
221369
221909
  init_manifest();
221370
221910
  init_localFiles();
221371
- import { basename as basename12 } from "node:path";
221372
- import { readFileSync as readFileSync21 } from "node:fs";
221911
+ import { basename as basename13 } from "node:path";
221912
+ import { readFileSync as readFileSync22 } from "node:fs";
221373
221913
  init_util();
221374
221914
  var push = async (args) => {
221375
221915
  const root = findProjectRoot(process.cwd());
@@ -221425,14 +221965,14 @@ var push = async (args) => {
221425
221965
  let uploaded = 0;
221426
221966
  for (const file of scan2.files) {
221427
221967
  const source = sourceFor(file.path);
221428
- const kind = source?.type ?? classifyKind(basename12(file.path));
221968
+ const kind = source?.type ?? classifyKind(basename13(file.path));
221429
221969
  const entry = { path: file.path, type: kind };
221430
221970
  if (source)
221431
221971
  entry.assetId = source.id;
221432
221972
  if (isTextKind(kind)) {
221433
- entry.text = readFileSync21(file.absPath, "utf8");
221973
+ entry.text = readFileSync22(file.absPath, "utf8");
221434
221974
  } else if (!source || source.sha !== file.sha) {
221435
- const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename12(file.path), readFileSync21(file.absPath));
221975
+ const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename13(file.path), readFileSync22(file.absPath));
221436
221976
  entry.blobFileId = blob.id;
221437
221977
  uploaded++;
221438
221978
  }
@@ -221539,10 +222079,10 @@ var link = async (args) => {
221539
222079
  const apiUrl = requireApiUrl(config);
221540
222080
  const token = requireToken(config);
221541
222081
  const dirArg = args._[0];
221542
- const root = dirArg ? resolve10(dirArg) : process.cwd();
221543
- if (!existsSync23(root))
222082
+ const root = dirArg ? resolve11(dirArg) : process.cwd();
222083
+ if (!existsSync26(root))
221544
222084
  throw new CliError(`No such directory: ${root}`);
221545
- if (existsSync23(join23(root, LECODES_DIR, "manifest.json")) && !flagBool(args, "force")) {
222085
+ if (existsSync26(join27(root, LECODES_DIR, "manifest.json")) && !flagBool(args, "force")) {
221546
222086
  throw new CliError("This folder is already linked (.lecodes/manifest.json exists) — use `lecodes push`, or `lecodes link --force` to re-link it to another project.");
221547
222087
  }
221548
222088
  let enclosing = null;
@@ -221569,7 +222109,7 @@ var link = async (args) => {
221569
222109
  }
221570
222110
  name = project.name;
221571
222111
  } else {
221572
- name = flagStr(args, "name") ?? basename13(root);
222112
+ name = flagStr(args, "name") ?? basename14(root);
221573
222113
  info(c.dim(`Creating project ${c.bold(name)}…`));
221574
222114
  uuid = (await createProject(apiUrl, token, name, flagStr(args, "team"))).uuid;
221575
222115
  files = {};
@@ -221619,9 +222159,91 @@ var login = async (args) => {
221619
222159
  success(`Logged in as ${c.bold(account.email)} on ${apiUrl}`);
221620
222160
  };
221621
222161
 
222162
+ // src/commands/shaders.ts
222163
+ init_manifest();
222164
+ import { readFileSync as readFileSync23 } from "node:fs";
222165
+ init_util();
222166
+ var collectShaders = () => {
222167
+ let root;
222168
+ let manifest;
222169
+ try {
222170
+ root = findProjectRoot(process.cwd());
222171
+ manifest = readManifest(root);
222172
+ } catch {
222173
+ root = process.cwd();
222174
+ manifest = null;
222175
+ }
222176
+ const { entries } = collectLocalEntries(root, manifest, "", { localShaders: true });
222177
+ return { root, entries, shaders: entries.filter((e2) => e2.type === "shader" && e2.absPath) };
222178
+ };
222179
+ var status2 = async () => {
222180
+ const { root, shaders } = collectShaders();
222181
+ const installed = installedMatcVersions();
222182
+ const matcExe = resolveMatcExeCached();
222183
+ log(`matc ${matcExe ? process.env.LECODES_MATC ? `LECODES_MATC=${process.env.LECODES_MATC}` : `${installed[0]} ${c.dim("(~/.lecodes/matc)")}` : c.dim("not downloaded — fetched by `lecodes shaders compile` / `lecodes dev`")}`);
222184
+ if (shaders.length === 0) {
222185
+ log(c.dim("No .mat shaders in this project."));
222186
+ } else {
222187
+ const matcId = matcExe ? matcIdentity(matcExe) : null;
222188
+ log(`shaders ${c.dim(`(compiled = up-to-date artifacts in ${shadersCacheDir(root)})`)}`);
222189
+ const width = Math.max(...shaders.map((s2) => s2.path.length));
222190
+ for (const shader of shaders) {
222191
+ const state = matcId === null ? c.dim("unknown (matc not downloaded)") : shaderIsCompiled(root, readFileSync23(shader.absPath, "utf8"), matcId) ? "compiled" : c.yellow("not compiled — run `lecodes shaders compile`");
222192
+ log(` ${shader.path.padEnd(width)} ${state}`);
222193
+ }
222194
+ }
222195
+ try {
222196
+ const { latest } = await fetchMatcManifest();
222197
+ const newest = installed[0];
222198
+ const hint = !newest ? "" : semverNewer(newest, latest) > 0 ? ` ${c.yellow("(newer — run `lecodes shaders update`)")}` : latest === newest ? ` ${c.dim("(up to date)")}` : "";
222199
+ log(`published ${latest}${hint}`);
222200
+ } catch (e2) {
222201
+ log(`published ${c.dim(e2 instanceof CliError ? e2.message : "unknown (couldn't reach the release manifest)")}`);
222202
+ }
222203
+ };
222204
+ var compile3 = async () => {
222205
+ const { root, entries, shaders } = collectShaders();
222206
+ if (shaders.length === 0) {
222207
+ note("No .mat shaders in this project.");
222208
+ return;
222209
+ }
222210
+ const matcExe = await resolveMatcExe();
222211
+ const matcId = matcIdentity(matcExe);
222212
+ const wasCompiled = new Map(shaders.map((s2) => [s2.path, shaderIsCompiled(root, readFileSync23(s2.absPath, "utf8"), matcId)]));
222213
+ const errors2 = [];
222214
+ for (const shader of shaders) {
222215
+ try {
222216
+ const { warnings } = await applyLocalShaders([shader], { root, local: true });
222217
+ for (const w of warnings)
222218
+ warnErr(w);
222219
+ log(` ${shader.path} ${wasCompiled.get(shader.path) ? c.dim("compiled (cached)") : "compiled"}`);
222220
+ } catch (e2) {
222221
+ const message = e2 instanceof CliError ? e2.message : String(e2);
222222
+ log(` ${shader.path} ${c.red("FAILED")}`);
222223
+ errors2.push(message);
222224
+ }
222225
+ }
222226
+ for (const e2 of errors2)
222227
+ log(`
222228
+ ${c.red("✗")} ${e2}`);
222229
+ if (errors2.length > 0)
222230
+ throw new CliError(`${errors2.length} of ${shaders.length} shader${shaders.length === 1 ? "" : "s"} failed to compile.`);
222231
+ note(`${shaders.length} shader${shaders.length === 1 ? "" : "s"} compiled → ${shadersCacheDir(root)}`);
222232
+ };
222233
+ var shaders = async (args) => {
222234
+ const sub = args._[0];
222235
+ if (sub === undefined)
222236
+ return status2();
222237
+ if (sub === "compile")
222238
+ return compile3();
222239
+ if (sub === "update")
222240
+ return updateMatc();
222241
+ throw new CliError(`Unknown subcommand "shaders ${sub}". Use \`lecodes shaders\` (status), \`lecodes shaders compile\` or \`lecodes shaders update\`.`);
222242
+ };
222243
+
221622
222244
  // src/commands/render.ts
221623
- import { mkdirSync as mkdirSync18, readFileSync as readFileSync22, writeFileSync as writeFileSync19 } from "node:fs";
221624
- import { dirname as dirname15, resolve as resolve12 } from "node:path";
222245
+ import { mkdirSync as mkdirSync20, readFileSync as readFileSync24, writeFileSync as writeFileSync21 } from "node:fs";
222246
+ import { dirname as dirname15, resolve as resolve13 } from "node:path";
221625
222247
 
221626
222248
  // src/compile/headlessBundle.ts
221627
222249
  init_api();
@@ -221705,10 +222327,10 @@ init_manifest();
221705
222327
 
221706
222328
  // src/desktopScript.ts
221707
222329
  init_util();
221708
- import { resolve as resolve11 } from "node:path";
222330
+ import { resolve as resolve12 } from "node:path";
221709
222331
  var isPoint = (v) => typeof v === "object" && v !== null && typeof v.x === "number" && typeof v.y === "number";
221710
222332
  var FRAME_MS = 16;
221711
- var ACTIONS = ["tap", "type", "fill", "scroll", "wait", "frames", "drag", "hold", "key", "keyDown", "keyUp", "camera", "view"];
222333
+ var ACTIONS = ["tap", "type", "fill", "scroll", "wait", "frames", "drag", "hold", "key", "keyDown", "keyUp", "look", "mouseTo", "wheel", "stick", "gamepad", "camera", "view"];
221712
222334
  var SEMANTIC_EXPECT = ["text", "notText", "node", "notNode", "screen", "value"];
221713
222335
  var VIEW_NAMES = ["iso", "top", "front", "side", "back"];
221714
222336
  var isVec3 = (v) => Array.isArray(v) && v.length === 3 && v.every((n2) => typeof n2 === "number");
@@ -221821,6 +222443,55 @@ var translateScenario = (rawSteps, opts) => {
221821
222443
  plan.lines.push(emit(`${action.toLowerCase()} ${code}`));
221822
222444
  budgetMs += 2 * FRAME_MS;
221823
222445
  parts.push(`${action} ${code}`);
222446
+ } else if (action === "look") {
222447
+ const v = s2.look;
222448
+ if (!Array.isArray(v) || v.length !== 2 || !v.every((n2) => typeof n2 === "number"))
222449
+ throw new CliError(`${at}: "look" must be [dx, dy] (logical px of mouse motion).`);
222450
+ const ms = num(at, "ms", s2.ms, FRAME_MS);
222451
+ plan.lines.push(emit(`look ${v[0]} ${v[1]} ${Math.round(ms)}`));
222452
+ budgetMs += ms + 2 * FRAME_MS;
222453
+ parts.push(`look ${v[0]},${v[1]} over ${ms}ms`);
222454
+ } else if (action === "mouseTo") {
222455
+ const v = s2.mouseTo;
222456
+ if (!Array.isArray(v) || v.length !== 2 || !v.every((n2) => typeof n2 === "number"))
222457
+ throw new CliError(`${at}: "mouseTo" must be [x, y] (logical px).`);
222458
+ plan.lines.push(emit(`mouseto ${v[0]} ${v[1]}`));
222459
+ budgetMs += FRAME_MS;
222460
+ parts.push(`mouseTo ${v[0]},${v[1]}`);
222461
+ } else if (action === "wheel") {
222462
+ const v = s2.wheel;
222463
+ if (!Array.isArray(v) || v.length !== 2 || !v.every((n2) => typeof n2 === "number"))
222464
+ throw new CliError(`${at}: "wheel" must be [dx, dy] (notches).`);
222465
+ plan.lines.push(emit(`wheel ${v[0]} ${v[1]}`));
222466
+ budgetMs += 2 * FRAME_MS;
222467
+ parts.push(`wheel ${v[0]},${v[1]}`);
222468
+ } else if (action === "stick") {
222469
+ const st = s2.stick;
222470
+ if (typeof st !== "object" || st === null)
222471
+ throw new CliError(`${at}: "stick" must be { left?: [x, y], right?: [x, y], triggers?: [l, r] }.`);
222472
+ const pad = typeof s2.index === "number" ? s2.index : 0;
222473
+ const parts2 = [];
222474
+ for (const k of ["left", "right", "triggers"]) {
222475
+ const v = st[k];
222476
+ if (v === undefined)
222477
+ continue;
222478
+ if (!Array.isArray(v) || v.length !== 2 || !v.every((n2) => typeof n2 === "number"))
222479
+ throw new CliError(`${at}: "stick.${k}" must be a pair of numbers.`);
222480
+ plan.lines.push(emit(`stick ${pad} ${k} ${v[0]} ${v[1]}`));
222481
+ parts2.push(`${k}=${v[0]},${v[1]}`);
222482
+ }
222483
+ for (const k of Object.keys(st))
222484
+ if (!["left", "right", "triggers"].includes(k))
222485
+ throw new CliError(`${at}: "stick" knows left, right, triggers — not "${k}".`);
222486
+ budgetMs += 2 * FRAME_MS;
222487
+ parts.push(`stick #${pad} ${parts2.join(" ")}`);
222488
+ } else if (action === "gamepad") {
222489
+ if (s2.gamepad !== "connect" && s2.gamepad !== "disconnect")
222490
+ throw new CliError(`${at}: "gamepad" must be "connect" or "disconnect".`);
222491
+ const pad = typeof s2.index === "number" ? s2.index : 0;
222492
+ plan.lines.push(emit(`gamepad ${pad} ${s2.gamepad}`));
222493
+ budgetMs += 2 * FRAME_MS;
222494
+ parts.push(`gamepad ${s2.gamepad} #${pad}`);
221824
222495
  } else if (action === "wait") {
221825
222496
  const ms = num(at, "wait", s2.wait);
221826
222497
  plan.lines.push(emit(`wait ${Math.round(ms)}`));
@@ -221857,7 +222528,7 @@ var translateScenario = (rawSteps, opts) => {
221857
222528
  if (s2.screenshot !== undefined) {
221858
222529
  if (typeof s2.screenshot !== "string")
221859
222530
  throw new CliError(`${at}: "screenshot" must be a path.`);
221860
- plan.screenshot = resolve11(opts.outDir, s2.screenshot);
222531
+ plan.screenshot = resolve12(opts.outDir, s2.screenshot);
221861
222532
  plan.lines.push(emit(`frames 2`));
221862
222533
  plan.lines.push(emit(`shot ${plan.screenshot}`));
221863
222534
  budgetMs += 3 * FRAME_MS;
@@ -221966,9 +222637,9 @@ var requireRenderer = async () => {
221966
222637
  };
221967
222638
  var fileScenarioIO = (dir) => ({
221968
222639
  write: (relPath, data) => {
221969
- const abs = resolve12(dir, relPath);
221970
- mkdirSync18(dirname15(abs), { recursive: true });
221971
- writeFileSync19(abs, data);
222640
+ const abs = resolve13(dir, relPath);
222641
+ mkdirSync20(dirname15(abs), { recursive: true });
222642
+ writeFileSync21(abs, data);
221972
222643
  }
221973
222644
  });
221974
222645
  var resolveFixtures = (value, baseDir) => {
@@ -221976,9 +222647,9 @@ var resolveFixtures = (value, baseDir) => {
221976
222647
  return;
221977
222648
  let map = value;
221978
222649
  if (typeof value === "string") {
221979
- const abs = resolve12(baseDir, value);
222650
+ const abs = resolve13(baseDir, value);
221980
222651
  try {
221981
- map = JSON.parse(readFileSync22(abs, "utf8"));
222652
+ map = JSON.parse(readFileSync24(abs, "utf8"));
221982
222653
  } catch (e2) {
221983
222654
  throw new CliError(`Can't read fixtures "${value}": ${e2 instanceof Error ? e2.message : String(e2)}`);
221984
222655
  }
@@ -221991,7 +222662,7 @@ var resolveFixtures = (value, baseDir) => {
221991
222662
  var readScenarioFile = (path2) => {
221992
222663
  let raw;
221993
222664
  try {
221994
- raw = JSON.parse(readFileSync22(resolve12(path2), "utf8"));
222665
+ raw = JSON.parse(readFileSync24(resolve13(path2), "utf8"));
221995
222666
  } catch (e2) {
221996
222667
  throw new CliError(`Can't read scenario "${path2}": ${e2 instanceof Error ? e2.message : String(e2)}`);
221997
222668
  }
@@ -222041,13 +222712,13 @@ var renderDesktop = async (args) => {
222041
222712
  if ((camera || view) && scenario)
222042
222713
  throw new CliError('--camera/--view apply to a plain render; in a --script run use a { "view": "iso" } or { "camera": { "at", "lookAt" } } step before the screenshot.');
222043
222714
  const { js, resources } = await buildBundle(args, undefined, { localAssets: true });
222044
- mkdirSync18(dirname15(resolve12(pngFlag)), { recursive: true });
222715
+ mkdirSync20(dirname15(resolve13(pngFlag)), { recursive: true });
222045
222716
  if (scenario) {
222046
222717
  const outDir = flagStr(args, "out-dir") ?? ".";
222047
222718
  const plan = translateScenario(scenario.steps, { outDir, headroomFrames: frames });
222048
222719
  for (const st of plan.steps)
222049
222720
  if (st.screenshot)
222050
- mkdirSync18(dirname15(st.screenshot), { recursive: true });
222721
+ mkdirSync20(dirname15(st.screenshot), { recursive: true });
222051
222722
  const timeoutMs2 = Number(flagStr(args, "timeout")) || 60000 + plan.steps.length * 5000;
222052
222723
  note(`Running ${plan.steps.length} step${plan.steps.length === 1 ? "" : "s"} (native)…`);
222053
222724
  const { output } = await runDesktopRender(js, resources, {
@@ -222142,7 +222813,7 @@ var render = async (args) => {
222142
222813
  const settleStr = flagStr(args, "settle") ?? (scenario?.settle !== undefined ? String(scenario.settle) : undefined);
222143
222814
  const timeStr = flagStr(args, "time");
222144
222815
  const onConsole = flagBool(args, "logs") ? (level, text2) => logErr(`${c.dim(`[${level}]`)} ${text2}`) : undefined;
222145
- const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname15(resolve12(scriptFlag)) : ".");
222816
+ const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname15(resolve13(scriptFlag)) : ".");
222146
222817
  const flagFixtures = resolveFixtures(flagStr(args, "fixtures"), ".");
222147
222818
  const fixtures = envelopeFixtures || flagFixtures ? { ...envelopeFixtures, ...flagFixtures } : undefined;
222148
222819
  if (fixtures) {
@@ -222181,7 +222852,7 @@ var render = async (args) => {
222181
222852
  ${c.red(st.error ?? "failed")}`);
222182
222853
  }
222183
222854
  if (!result2.ok && result2.failureArtifacts.length > 0) {
222184
- logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p2) => resolve12(outDir, p2)).join(", ")}`));
222855
+ logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p2) => resolve13(outDir, p2)).join(", ")}`));
222185
222856
  }
222186
222857
  logErr(result2.ok ? c.green(`✓ ${result2.steps.length}/${steps.length} steps passed`) : c.red(`✗ failed at step ${result2.steps.length} of ${steps.length}`));
222187
222858
  return done((cb) => cb(), result2.ok ? 0 : 1);
@@ -222205,7 +222876,7 @@ var render = async (args) => {
222205
222876
  if (isNullSink(pngFlag)) {
222206
222877
  note(`Discarded PNG (${pngFlag})`);
222207
222878
  } else {
222208
- writeFileSync19(resolve12(pngFlag), png2);
222879
+ writeFileSync21(resolve13(pngFlag), png2);
222209
222880
  note(`Wrote ${pngFlag} (${png2.length} bytes, ${outW}x${outH}${clip !== undefined ? ", clipped" : ""})`);
222210
222881
  }
222211
222882
  return done((cb) => cb());
@@ -222220,7 +222891,7 @@ var render = async (args) => {
222220
222891
  return done((cb) => cb());
222221
222892
  }
222222
222893
  if (out) {
222223
- writeFileSync19(resolve12(out), result.text);
222894
+ writeFileSync21(resolve13(out), result.text);
222224
222895
  note(`Wrote ${out} (${result.text.length} bytes)`);
222225
222896
  return done((cb) => cb());
222226
222897
  }
@@ -222235,8 +222906,8 @@ init_api();
222235
222906
  init_config();
222236
222907
  init_manifest();
222237
222908
  init_util();
222238
- import { appendFileSync as appendFileSync2, existsSync as existsSync24, readFileSync as readFileSync23, writeFileSync as writeFileSync20 } from "node:fs";
222239
- import { join as join24 } from "node:path";
222909
+ import { appendFileSync as appendFileSync2, existsSync as existsSync27, readFileSync as readFileSync25, writeFileSync as writeFileSync22 } from "node:fs";
222910
+ import { join as join28 } from "node:path";
222240
222911
  var ENV_VAR = "LECODES_PUSH_KEY";
222241
222912
  var context = () => {
222242
222913
  const root = findProjectRoot(process.cwd());
@@ -222380,11 +223051,11 @@ var pnKeysList = async (ctx) => {
222380
223051
  };
222381
223052
  var writeEnv = (root, key) => {
222382
223053
  const touched = [];
222383
- const envPath = join24(root, ".env");
223054
+ const envPath = join28(root, ".env");
222384
223055
  const line = `${ENV_VAR}=${key}`;
222385
- const existing = existsSync24(envPath) ? readFileSync23(envPath, "utf8") : "";
223056
+ const existing = existsSync27(envPath) ? readFileSync25(envPath, "utf8") : "";
222386
223057
  if (new RegExp(`^${ENV_VAR}=`, "m").test(existing)) {
222387
- writeFileSync20(envPath, existing.replace(new RegExp(`^${ENV_VAR}=.*$`, "m"), line));
223058
+ writeFileSync22(envPath, existing.replace(new RegExp(`^${ENV_VAR}=.*$`, "m"), line));
222388
223059
  } else {
222389
223060
  const separator = !existing || existing.endsWith(`
222390
223061
  `) ? "" : `
@@ -222393,8 +223064,8 @@ var writeEnv = (root, key) => {
222393
223064
  `);
222394
223065
  }
222395
223066
  touched.push(".env");
222396
- const ignorePath = join24(root, ".gitignore");
222397
- const ignore = existsSync24(ignorePath) ? readFileSync23(ignorePath, "utf8") : "";
223067
+ const ignorePath = join28(root, ".gitignore");
223068
+ const ignore = existsSync27(ignorePath) ? readFileSync25(ignorePath, "utf8") : "";
222398
223069
  if (!ignore.split(`
222399
223070
  `).some((l2) => l2.trim() === ".env")) {
222400
223071
  const separator = !ignore || ignore.endsWith(`
@@ -222493,8 +223164,8 @@ var pn = async (args) => {
222493
223164
  init_api();
222494
223165
  init_config();
222495
223166
  init_manifest();
222496
- import { existsSync as existsSync25, mkdirSync as mkdirSync19, rmSync as rmSync6, writeFileSync as writeFileSync21 } from "node:fs";
222497
- import { dirname as dirname16, join as join25 } from "node:path";
223167
+ import { existsSync as existsSync28, mkdirSync as mkdirSync21, rmSync as rmSync8, writeFileSync as writeFileSync23 } from "node:fs";
223168
+ import { dirname as dirname16, join as join29 } from "node:path";
222498
223169
  init_types4();
222499
223170
  init_localFiles();
222500
223171
  init_util();
@@ -222517,9 +223188,9 @@ var pull = async (args) => {
222517
223188
  const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a2) => a2.id === id)?.type !== "folder").map(([, path2]) => path2));
222518
223189
  for (const path2 of Object.keys(manifest.files)) {
222519
223190
  if (!remotePaths.has(path2)) {
222520
- const abs = join25(root, ...path2.replace(/^\//, "").split("/"));
222521
- if (existsSync25(abs))
222522
- rmSync6(abs);
223191
+ const abs = join29(root, ...path2.replace(/^\//, "").split("/"));
223192
+ if (existsSync28(abs))
223193
+ rmSync8(abs);
222523
223194
  }
222524
223195
  }
222525
223196
  const files = await materializeAssets(root, project, apiUrl, token, progress);
@@ -222556,18 +223227,18 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
222556
223227
  const files = { ...manifest.files };
222557
223228
  for (const asset of targets) {
222558
223229
  const key = pathById.get(asset.id);
222559
- const abs = join25(root, ...key.replace(/^\//, "").split("/"));
222560
- mkdirSync19(dirname16(abs), { recursive: true });
223230
+ const abs = join29(root, ...key.replace(/^\//, "").split("/"));
223231
+ mkdirSync21(dirname16(abs), { recursive: true });
222561
223232
  let sha;
222562
223233
  if (asset.type === "file") {
222563
223234
  if (!asset.file?.src)
222564
223235
  continue;
222565
223236
  const bytes = await downloadBlob(apiUrl, asset.file.src);
222566
- writeFileSync21(abs, bytes);
223237
+ writeFileSync23(abs, bytes);
222567
223238
  sha = sha256(bytes);
222568
223239
  } else {
222569
223240
  const text = asset.text ?? "";
222570
- writeFileSync21(abs, text);
223241
+ writeFileSync23(abs, text);
222571
223242
  sha = sha256(text);
222572
223243
  }
222573
223244
  files[key] = { id: asset.id, type: kindOf3(asset.type), sha };
@@ -222580,8 +223251,8 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
222580
223251
 
222581
223252
  // src/commands/scene.ts
222582
223253
  init_manifest();
222583
- import { existsSync as existsSync27, readFileSync as readFileSync25, readdirSync as readdirSync12, statSync as statSync8, writeFileSync as writeFileSync22 } from "node:fs";
222584
- import { basename as basename14, dirname as dirname17, join as join27, resolve as resolve13 } from "node:path";
223254
+ import { existsSync as existsSync30, readFileSync as readFileSync27, readdirSync as readdirSync13, statSync as statSync9, writeFileSync as writeFileSync24 } from "node:fs";
223255
+ import { basename as basename15, dirname as dirname17, join as join31, resolve as resolve14 } from "node:path";
222585
223256
 
222586
223257
  // src/compile/sceneCompile.ts
222587
223258
  init_localFiles();
@@ -222589,18 +223260,18 @@ init_ignore();
222589
223260
  init_distRoot();
222590
223261
  init_util();
222591
223262
  init_projectCompile();
222592
- import { existsSync as existsSync26, readFileSync as readFileSync24 } from "node:fs";
222593
- import { join as join26 } from "node:path";
223263
+ import { existsSync as existsSync29, readFileSync as readFileSync26 } from "node:fs";
223264
+ import { join as join30 } from "node:path";
222594
223265
  var TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i;
222595
223266
  var rel = (key) => key.replace(/^\//, "");
222596
223267
  var resolveHarness = (loadSceneHarness2) => {
222597
- const inRepo = join26(distRoot, "..", "projects", "src", "scene-editor");
223268
+ const inRepo = join30(distRoot, "..", "projects", "src", "scene-editor");
222598
223269
  try {
222599
- return loadSceneHarness2(existsSync26(inRepo) ? inRepo : undefined);
223270
+ return loadSceneHarness2(existsSync29(inRepo) ? inRepo : undefined);
222600
223271
  } catch {
222601
- const vendored = join26(distRoot, "runtime", "scene-harness.json");
222602
- if (existsSync26(vendored))
222603
- return JSON.parse(readFileSync24(vendored, "utf8"));
223272
+ const vendored = join30(distRoot, "runtime", "scene-harness.json");
223273
+ if (existsSync29(vendored))
223274
+ return JSON.parse(readFileSync26(vendored, "utf8"));
222604
223275
  throw new CliError("scene-editor harness sources not found — reinstall the CLI or set LECODES_SCENE_HARNESS_DIR.");
222605
223276
  }
222606
223277
  };
@@ -222612,7 +223283,7 @@ var compileSceneEditor = async (opts) => {
222612
223283
  for (const file of scan2.files) {
222613
223284
  const path2 = "./" + rel(file.path);
222614
223285
  if (TEXT_EXT.test(path2)) {
222615
- entries.push({ path: path2, type: "text", text: readFileSync24(file.absPath, "utf8") });
223286
+ entries.push({ path: path2, type: "text", text: readFileSync26(file.absPath, "utf8") });
222616
223287
  } else {
222617
223288
  entries.push({ path: path2, type: "resource", fileSrc: "/project/" + rel(file.path) });
222618
223289
  }
@@ -222643,6 +223314,17 @@ var compileSceneEditor = async (opts) => {
222643
223314
  init_browserAuth();
222644
223315
  init_util();
222645
223316
  var DEFAULT_PORT3 = 4488;
223317
+ var findAppRoot = (start) => {
223318
+ let dir = start;
223319
+ while (true) {
223320
+ if (existsSync30(join31(dir, "app.json")))
223321
+ return dir;
223322
+ const parent = dirname17(dir);
223323
+ if (parent === dir)
223324
+ return null;
223325
+ dir = parent;
223326
+ }
223327
+ };
222646
223328
  var getContext2 = (args) => {
222647
223329
  let root;
222648
223330
  let manifest;
@@ -222654,24 +223336,26 @@ var getContext2 = (args) => {
222654
223336
  manifest = null;
222655
223337
  const pathArg = args._[0];
222656
223338
  if (pathArg) {
222657
- const abs = resolve13(process.cwd(), pathArg);
222658
- if (existsSync27(abs) && statSync8(abs).isDirectory())
223339
+ const abs = resolve14(process.cwd(), pathArg);
223340
+ if (existsSync30(abs) && statSync9(abs).isDirectory())
222659
223341
  root = abs;
222660
- else if (abs.endsWith(".scene.ts") && existsSync27(abs))
222661
- root = dirname17(abs);
223342
+ else if (abs.endsWith(".scene.ts") && existsSync30(abs))
223343
+ root = findAppRoot(dirname17(abs)) ?? dirname17(abs);
222662
223344
  else
222663
223345
  throw new CliError(`No such folder or .scene.ts: ${pathArg}`);
223346
+ } else {
223347
+ root = findAppRoot(root) ?? root;
222664
223348
  }
222665
223349
  }
222666
- return { root, manifest, name: manifest?.name ?? basename14(root) };
223350
+ return { root, manifest, name: manifest?.name ?? basename15(root) };
222667
223351
  };
222668
223352
  var writeMcpConfig2 = (root, port) => {
222669
- const path2 = join27(root, ".mcp.json");
223353
+ const path2 = join31(root, ".mcp.json");
222670
223354
  const entry = { type: "http", url: `http://localhost:${port}/mcp` };
222671
223355
  let config = {};
222672
- if (existsSync27(path2)) {
223356
+ if (existsSync30(path2)) {
222673
223357
  try {
222674
- config = JSON.parse(readFileSync25(path2, "utf8"));
223358
+ config = JSON.parse(readFileSync27(path2, "utf8"));
222675
223359
  } catch {
222676
223360
  warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-scene server by hand.");
222677
223361
  return;
@@ -222684,17 +223368,17 @@ var writeMcpConfig2 = (root, port) => {
222684
223368
  return;
222685
223369
  }
222686
223370
  config.mcpServers["lecodes-scene"] = entry;
222687
- writeFileSync22(path2, JSON.stringify(config, null, 2) + `
223371
+ writeFileSync24(path2, JSON.stringify(config, null, 2) + `
222688
223372
  `);
222689
223373
  note(`${existing ? "updated" : "created"} .mcp.json (lecodes-scene MCP → ${entry.url})`);
222690
223374
  };
222691
223375
  var countScenes = (dir) => {
222692
223376
  let n2 = 0;
222693
- for (const name of readdirSync12(dir)) {
223377
+ for (const name of readdirSync13(dir)) {
222694
223378
  if (name === "node_modules" || name.startsWith("."))
222695
223379
  continue;
222696
- const full = join27(dir, name);
222697
- if (statSync8(full).isDirectory())
223380
+ const full = join31(dir, name);
223381
+ if (statSync9(full).isDirectory())
222698
223382
  n2 += countScenes(full);
222699
223383
  else if (name.endsWith(".scene.ts"))
222700
223384
  n2++;
@@ -222753,7 +223437,7 @@ var assets = async (args) => {
222753
223437
  init_manifest();
222754
223438
  init_localFiles();
222755
223439
  init_util();
222756
- var status2 = async (_args) => {
223440
+ var status3 = async (_args) => {
222757
223441
  const root = findProjectRoot(process.cwd());
222758
223442
  const manifest = readManifest(root);
222759
223443
  const scan2 = scanProject(root, manifest);
@@ -222782,8 +223466,8 @@ var status2 = async (_args) => {
222782
223466
  };
222783
223467
 
222784
223468
  // src/commands/test.ts
222785
- import { existsSync as existsSync28, mkdirSync as mkdirSync20, readFileSync as readFileSync26, readdirSync as readdirSync13 } from "node:fs";
222786
- import { dirname as dirname18, resolve as resolve14 } from "node:path";
223469
+ import { existsSync as existsSync31, mkdirSync as mkdirSync22, readFileSync as readFileSync28, readdirSync as readdirSync14 } from "node:fs";
223470
+ import { dirname as dirname18, resolve as resolve15 } from "node:path";
222787
223471
  init_manifest();
222788
223472
  init_util();
222789
223473
  var deriveDesignFlows = (meta, entryScreen) => {
@@ -222846,7 +223530,7 @@ var slug3 = (s2) => s2.replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "").
222846
223530
  var readFlowFile = (abs, displayName) => {
222847
223531
  let raw;
222848
223532
  try {
222849
- raw = JSON.parse(readFileSync26(abs, "utf8"));
223533
+ raw = JSON.parse(readFileSync28(abs, "utf8"));
222850
223534
  } catch (e2) {
222851
223535
  throw new CliError(`Can't read "${displayName}": ${e2 instanceof Error ? e2.message : String(e2)}`);
222852
223536
  }
@@ -222894,40 +223578,40 @@ var test = async (args) => {
222894
223578
  if (!Number.isFinite(nativeFixedDt) || nativeFixedDt < 0 || nativeFixedDt > 1000)
222895
223579
  throw new CliError(`--fixed-dt wants a number of ms (0 = wall clock, max 1000), got "${fixedDtFlag}".`);
222896
223580
  const projectViewport = desktopDefaultViewport(root);
222897
- const testsDir = resolve14(root, flagStr(args, "dir") ?? "tests");
222898
- const designMetaPath = resolve14(root, flagStr(args, "design-dir") ?? "design", "meta.json");
223581
+ const testsDir = resolve15(root, flagStr(args, "dir") ?? "tests");
223582
+ const designMetaPath = resolve15(root, flagStr(args, "design-dir") ?? "design", "meta.json");
222899
223583
  const asJson = flagBool(args, "json");
222900
223584
  const out = asJson ? logErr : log;
222901
223585
  const cases = [];
222902
223586
  if (args._.length > 0) {
222903
223587
  for (const given of args._) {
222904
- const candidates = [resolve14(given), resolve14(testsDir, given), resolve14(testsDir, `${given}.flow.json`)];
222905
- const abs = candidates.find((p2) => existsSync28(p2));
223588
+ const candidates = [resolve15(given), resolve15(testsDir, given), resolve15(testsDir, `${given}.flow.json`)];
223589
+ const abs = candidates.find((p2) => existsSync31(p2));
222906
223590
  if (!abs)
222907
223591
  throw new CliError(`No such test: "${given}" (looked in ${testsDir}).`);
222908
223592
  cases.push(readFlowFile(abs, given));
222909
223593
  }
222910
- } else if (existsSync28(testsDir)) {
222911
- for (const f3 of readdirSync13(testsDir).filter((f4) => f4.endsWith(".flow.json")).sort()) {
222912
- cases.push(readFlowFile(resolve14(testsDir, f3), f3));
223594
+ } else if (existsSync31(testsDir)) {
223595
+ for (const f3 of readdirSync14(testsDir).filter((f4) => f4.endsWith(".flow.json")).sort()) {
223596
+ cases.push(readFlowFile(resolve15(testsDir, f3), f3));
222913
223597
  }
222914
223598
  }
222915
223599
  const fixturesFlag = flagStr(args, "fixtures");
222916
- const defaultPack = resolve14(testsDir, "fixtures.json");
222917
- const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") : existsSync28(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
223600
+ const defaultPack = resolve15(testsDir, "fixtures.json");
223601
+ const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") : existsSync31(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
222918
223602
  const flowsOnly = flagBool(args, "flows");
222919
223603
  const wantFlows = !flagBool(args, "no-flows") && args._.length === 0;
222920
223604
  if (flowsOnly)
222921
223605
  cases.length = 0;
222922
223606
  let skipped = [];
222923
223607
  let entryScreen = null;
222924
- if (desktop2 && wantFlows && existsSync28(designMetaPath)) {
223608
+ if (desktop2 && wantFlows && existsSync31(designMetaPath)) {
222925
223609
  note("Design-derived flows skipped with --desktop (they tap by name; run `lecodes test` headless for them).");
222926
223610
  }
222927
- if (!desktop2 && (wantFlows || flowsOnly) && existsSync28(designMetaPath)) {
223611
+ if (!desktop2 && (wantFlows || flowsOnly) && existsSync31(designMetaPath)) {
222928
223612
  let meta;
222929
223613
  try {
222930
- meta = JSON.parse(readFileSync26(designMetaPath, "utf8"));
223614
+ meta = JSON.parse(readFileSync28(designMetaPath, "utf8"));
222931
223615
  } catch (e2) {
222932
223616
  throw new CliError(`Can't read ${designMetaPath}: ${e2 instanceof Error ? e2.message : String(e2)}`);
222933
223617
  }
@@ -222959,16 +223643,16 @@ var test = async (args) => {
222959
223643
  }, timeoutMs);
222960
223644
  killer.unref?.();
222961
223645
  const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
222962
- const artifactsRoot = resolve14(root, flagStr(args, "artifacts") ?? resolve14(testsDir, ".artifacts"));
223646
+ const artifactsRoot = resolve15(root, flagStr(args, "artifacts") ?? resolve15(testsDir, ".artifacts"));
222963
223647
  const results = [];
222964
223648
  const runNativeCase = async (kase) => {
222965
- const caseDir = resolve14(artifactsRoot, slug3(kase.name));
223649
+ const caseDir = resolve15(artifactsRoot, slug3(kase.name));
222966
223650
  const plan = translateScenario(kase.steps, { outDir: caseDir, headroomFrames: nativeFrames });
222967
223651
  for (const st of plan.steps)
222968
223652
  if (st.screenshot)
222969
- mkdirSync20(dirname18(st.screenshot), { recursive: true });
222970
- mkdirSync20(caseDir, { recursive: true });
222971
- const finalPng = resolve14(caseDir, "final.png");
223653
+ mkdirSync22(dirname18(st.screenshot), { recursive: true });
223654
+ mkdirSync22(caseDir, { recursive: true });
223655
+ const finalPng = resolve15(caseDir, "final.png");
222972
223656
  const { output } = await runDesktopRender(native.js, native.resources, {
222973
223657
  width: Number(flagStr(args, "width")) || kase.width || projectViewport?.width || 390,
222974
223658
  height: Number(flagStr(args, "height")) || kase.height || projectViewport?.height || 844,
@@ -223019,14 +223703,14 @@ var test = async (args) => {
223019
223703
  onConsole
223020
223704
  });
223021
223705
  try {
223022
- const io = fileScenarioIO(resolve14(artifactsRoot, slug3(kase.name)));
223706
+ const io = fileScenarioIO(resolve15(artifactsRoot, slug3(kase.name)));
223023
223707
  const outcome = await renderer.runScenario(session, steps, io);
223024
223708
  result = {
223025
223709
  name: kase.name,
223026
223710
  kind: kase.kind,
223027
223711
  ok: outcome.ok,
223028
223712
  steps: outcome.steps.map((s2) => ({ desc: s2.desc, ok: s2.ok, error: s2.error })),
223029
- artifacts: outcome.failureArtifacts.map((p2) => resolve14(artifactsRoot, slug3(kase.name), p2))
223713
+ artifacts: outcome.failureArtifacts.map((p2) => resolve15(artifactsRoot, slug3(kase.name), p2))
223030
223714
  };
223031
223715
  } finally {
223032
223716
  session.dispose();
@@ -223094,11 +223778,11 @@ init_config();
223094
223778
  init_manifest();
223095
223779
  init_types4();
223096
223780
  init_util();
223097
- import { resolve as resolve15 } from "node:path";
223781
+ import { resolve as resolve16 } from "node:path";
223098
223782
  var label = (bundle) => `${Object.keys(bundle).length} file(s)`;
223099
223783
  var types4 = async (args) => {
223100
223784
  const dirArg = args._[0];
223101
- const root = dirArg ? resolve15(dirArg) : (() => {
223785
+ const root = dirArg ? resolve16(dirArg) : (() => {
223102
223786
  try {
223103
223787
  return findProjectRoot(process.cwd());
223104
223788
  } catch {
@@ -223157,23 +223841,23 @@ var types4 = async (args) => {
223157
223841
  };
223158
223842
 
223159
223843
  // src/index.ts
223160
- import { existsSync as existsSync30, readFileSync as readFileSync28, realpathSync as realpathSync2 } from "node:fs";
223844
+ import { existsSync as existsSync33, readFileSync as readFileSync30, realpathSync as realpathSync2 } from "node:fs";
223161
223845
  import { createRequire as createRequire4 } from "node:module";
223162
- import { delimiter as delimiter2, dirname as dirname20, join as join29 } from "node:path";
223846
+ import { delimiter as delimiter2, dirname as dirname20, join as join33 } from "node:path";
223163
223847
 
223164
223848
  // src/commands/install.ts
223165
223849
  init_util();
223166
223850
  import { execFileSync as execFileSync5 } from "node:child_process";
223167
- import { appendFileSync as appendFileSync3, chmodSync as chmodSync2, cpSync as cpSync4, existsSync as existsSync29, mkdirSync as mkdirSync21, readFileSync as readFileSync27, realpathSync, rmSync as rmSync7, symlinkSync } from "node:fs";
223168
- import { homedir as homedir7 } from "node:os";
223169
- import { basename as basename15, delimiter, dirname as dirname19, join as join28, resolve as resolve16, sep as sep2 } from "node:path";
223851
+ import { appendFileSync as appendFileSync3, chmodSync as chmodSync2, cpSync as cpSync4, existsSync as existsSync32, mkdirSync as mkdirSync23, readFileSync as readFileSync29, realpathSync, rmSync as rmSync9, symlinkSync } from "node:fs";
223852
+ import { homedir as homedir8 } from "node:os";
223853
+ import { basename as basename16, delimiter, dirname as dirname19, join as join32, resolve as resolve17, sep as sep2 } from "node:path";
223170
223854
  var isWin = process.platform === "win32";
223171
- var HOME = homedir7();
223855
+ var HOME = homedir8();
223172
223856
  var safeRealpath = (p2) => {
223173
223857
  try {
223174
223858
  return realpathSync(p2);
223175
223859
  } catch {
223176
- return resolve16(p2);
223860
+ return resolve17(p2);
223177
223861
  }
223178
223862
  };
223179
223863
  var onPath = (dir) => {
@@ -223190,24 +223874,24 @@ var onPath = (dir) => {
223190
223874
  };
223191
223875
  var install = async (args) => {
223192
223876
  const bundle = dirname19(process.execPath);
223193
- const exeName2 = basename15(process.execPath);
223877
+ const exeName2 = basename16(process.execPath);
223194
223878
  const chiselName = isWin ? "chisel.exe" : "chisel";
223195
- if (!existsSync29(join28(bundle, "runtime")) || !existsSync29(join28(bundle, chiselName))) {
223879
+ if (!existsSync32(join32(bundle, "runtime")) || !existsSync32(join32(bundle, chiselName))) {
223196
223880
  throw new CliError("`lecodes install` only works from the packaged release build (the archive containing lecodes + " + "chisel + runtime/). You appear to be running from source or an npm install — install globally " + "with your package manager there instead.");
223197
223881
  }
223198
- const companions = [exeName2, chiselName, "runtime", "README.txt"].filter((f3) => existsSync29(join28(bundle, f3)));
223882
+ const companions = [exeName2, chiselName, "runtime", "README.txt"].filter((f3) => existsSync32(join32(bundle, f3)));
223199
223883
  if (isWin)
223200
223884
  return installWindows(args, bundle, companions);
223201
223885
  return installUnix(args, bundle, companions);
223202
223886
  };
223203
223887
  var copyBundle = (bundle, target, companions) => {
223204
- mkdirSync21(target, { recursive: true });
223888
+ mkdirSync23(target, { recursive: true });
223205
223889
  if (safeRealpath(target) === safeRealpath(bundle))
223206
223890
  return;
223207
223891
  for (const f3 of companions) {
223208
- const dest = join28(target, f3);
223209
- rmSync7(dest, { recursive: true, force: true });
223210
- cpSync4(join28(bundle, f3), dest, { recursive: true });
223892
+ const dest = join32(target, f3);
223893
+ rmSync9(dest, { recursive: true, force: true });
223894
+ cpSync4(join32(bundle, f3), dest, { recursive: true });
223211
223895
  }
223212
223896
  };
223213
223897
  var permError = (e2, dir, system) => {
@@ -223219,41 +223903,41 @@ var permError = (e2, dir, system) => {
223219
223903
  };
223220
223904
  var installUnix = (args, bundle, companions) => {
223221
223905
  const system = flagBool(args, "system");
223222
- const libDir = resolve16(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join28(HOME, ".local", "lib", "lecodes-cli")));
223223
- const binDir = resolve16(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join28(HOME, ".local", "bin")));
223224
- const link2 = join28(binDir, "lecodes");
223906
+ const libDir = resolve17(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join32(HOME, ".local", "lib", "lecodes-cli")));
223907
+ const binDir = resolve17(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join32(HOME, ".local", "bin")));
223908
+ const link2 = join32(binDir, "lecodes");
223225
223909
  try {
223226
223910
  copyBundle(bundle, libDir, companions);
223227
- chmodSync2(join28(libDir, "lecodes"), 493);
223228
- chmodSync2(join28(libDir, "chisel"), 493);
223911
+ chmodSync2(join32(libDir, "lecodes"), 493);
223912
+ chmodSync2(join32(libDir, "chisel"), 493);
223229
223913
  } catch (e2) {
223230
223914
  throw permError(e2, libDir, system);
223231
223915
  }
223232
223916
  try {
223233
- mkdirSync21(binDir, { recursive: true });
223234
- rmSync7(link2, { force: true });
223235
- symlinkSync(join28(libDir, "lecodes"), link2);
223917
+ mkdirSync23(binDir, { recursive: true });
223918
+ rmSync9(link2, { force: true });
223919
+ symlinkSync(join32(libDir, "lecodes"), link2);
223236
223920
  } catch (e2) {
223237
223921
  throw permError(e2, binDir, system);
223238
223922
  }
223239
223923
  success(`Installed to ${c.bold(libDir)}`);
223240
- log(` ${c.dim(link2 + " → " + join28(libDir, "lecodes"))}`);
223924
+ log(` ${c.dim(link2 + " → " + join32(libDir, "lecodes"))}`);
223241
223925
  finishPathUnix(args, binDir);
223242
223926
  };
223243
223927
  var shellRc = () => {
223244
- const shell = basename15(process.env.SHELL ?? "");
223928
+ const shell = basename16(process.env.SHELL ?? "");
223245
223929
  if (shell === "zsh")
223246
- return { file: join28(HOME, ".zshrc"), name: "~/.zshrc" };
223930
+ return { file: join32(HOME, ".zshrc"), name: "~/.zshrc" };
223247
223931
  if (shell === "bash")
223248
- return { file: join28(HOME, ".bashrc"), name: "~/.bashrc" };
223249
- return { file: join28(HOME, ".profile"), name: "~/.profile" };
223932
+ return { file: join32(HOME, ".bashrc"), name: "~/.bashrc" };
223933
+ return { file: join32(HOME, ".profile"), name: "~/.profile" };
223250
223934
  };
223251
223935
  var PATH_MARKER = "# Added by `lecodes install`";
223252
223936
  var appendPathToShellRc = (binDir) => {
223253
223937
  const { file, name } = shellRc();
223254
223938
  const line = `export PATH="${binDir}:$PATH"`;
223255
223939
  try {
223256
- const existing = existsSync29(file) ? readFileSync27(file, "utf8") : "";
223940
+ const existing = existsSync32(file) ? readFileSync29(file, "utf8") : "";
223257
223941
  if (existing.includes(line))
223258
223942
  return name;
223259
223943
  appendFileSync3(file, `${existing && !existing.endsWith(`
@@ -223291,7 +223975,7 @@ var finishPathUnix = (args, binDir) => {
223291
223975
  log(` • or install system-wide (no PATH edit): ${c.bold("sudo lecodes install --system")}`);
223292
223976
  };
223293
223977
  var installWindows = (args, bundle, companions) => {
223294
- const target = resolve16(flagStr(args, "dir") ?? join28(process.env.LOCALAPPDATA ?? join28(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
223978
+ const target = resolve17(flagStr(args, "dir") ?? join32(process.env.LOCALAPPDATA ?? join32(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
223295
223979
  try {
223296
223980
  copyBundle(bundle, target, companions);
223297
223981
  } catch (e2) {
@@ -223331,7 +224015,7 @@ init_util();
223331
224015
  var STANDALONE = (() => {
223332
224016
  const dir = dirname20(process.execPath);
223333
224017
  const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
223334
- return existsSync30(join29(dir, "runtime")) && existsSync30(join29(dir, chisel));
224018
+ return existsSync33(join33(dir, "runtime")) && existsSync33(join33(dir, chisel));
223335
224019
  })();
223336
224020
  var INSTALLED = STANDALONE && (() => {
223337
224021
  const exe = process.platform === "win32" ? "lecodes.exe" : "lecodes";
@@ -223346,8 +224030,8 @@ var INSTALLED = STANDALONE && (() => {
223346
224030
  if (!d)
223347
224031
  return false;
223348
224032
  try {
223349
- const cand = join29(d, exe);
223350
- return existsSync30(cand) && realpathSync2(cand) === self2;
224033
+ const cand = join33(d, exe);
224034
+ return existsSync33(cand) && realpathSync2(cand) === self2;
223351
224035
  } catch {
223352
224036
  return false;
223353
224037
  }
@@ -223535,6 +224219,12 @@ ${c.bold("Commands:")}
223535
224219
  --public-url <url> uses the server's copies instead)
223536
224220
  update move to the newest published release
223537
224221
  (cached in ~/.lecodes/renderer)
224222
+ shaders [compile|update] The project's custom shaders (.mat) and their local compile:
224223
+ no args lists the shaders + their compile state; compile
224224
+ compiles them all into .lecodes/shaders (the same pass
224225
+ 'lecodes dev' runs on the fly — via Filament's matc,
224226
+ downloaded on first use to ~/.lecodes/matc); update
224227
+ downloads the newest matc (run after an engine upgrade)
223538
224228
  scene [path] LeCodes Scene Editor — visually edit a .scene.ts (tree, inspector,
223539
224229
  gizmo) on local files (needs the optional 'lecodes-3d-editor' package;
223540
224230
  works inside a cloned project or standalone in any folder)
@@ -223633,7 +224323,7 @@ ${c.bold("Commands:")}
223633
224323
  — 'new --env' banks it in .env), revoke <id>
223634
224324
 
223635
224325
  Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKEN).`;
223636
- var commands = { init: init2, login, create, clone: clone3, link, status: status2, diff: diff2, pull, push, compile: compile2, preview: previewLegacy, render, test, types: types4, design, scene, dev, app, pn, desktop, assets };
224326
+ var commands = { init: init2, login, create, clone: clone3, link, status: status3, diff: diff2, pull, push, compile: compile2, preview: previewLegacy, render, test, types: types4, design, scene, dev, app, pn, desktop, shaders, assets };
223637
224327
  if (STANDALONE)
223638
224328
  commands.install = install;
223639
224329
  var readVersion = () => package_default.version ?? "unknown";
@@ -223648,9 +224338,9 @@ var peerVersion = (name, probe) => {
223648
224338
  const require2 = createRequire4(import.meta.url);
223649
224339
  let dir = dirname20(require2.resolve(probe));
223650
224340
  for (let depth = 0;depth < 16; depth++) {
223651
- const manifest = join29(dir, "package.json");
223652
- if (existsSync30(manifest)) {
223653
- const parsed = JSON.parse(readFileSync28(manifest, "utf8"));
224341
+ const manifest = join33(dir, "package.json");
224342
+ if (existsSync33(manifest)) {
224343
+ const parsed = JSON.parse(readFileSync30(manifest, "utf8"));
223654
224344
  if (parsed.name === name)
223655
224345
  return parsed.version ?? "unknown";
223656
224346
  }
@@ -223670,6 +224360,8 @@ var printVersion = () => {
223670
224360
  }
223671
224361
  const desktopVersion = selectedDesktopVersion();
223672
224362
  rows.push(["lecodes-desktop", installedDesktopVersions().includes(desktopVersion) ? desktopVersion : `${desktopVersion} ${c.dim("(not downloaded — fetched on first `render --desktop`)")}`]);
224363
+ const matcVersion = installedMatcVersions()[0];
224364
+ rows.push(["matc", matcVersion ?? c.dim("not downloaded — fetched on the first local shader compile")]);
223673
224365
  const width = Math.max(...rows.map(([label2]) => label2.length));
223674
224366
  for (const [label2, value] of rows)
223675
224367
  log(`${label2.padEnd(width)} ${value}`);