lecodes-cli 0.18.0 → 0.18.2

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 (62) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +2013 -564
  3. package/package.json +4 -4
  4. package/runtime/scene-harness.json +1 -1
  5. package/runtime/sdk/core/Aspect.ts +512 -255
  6. package/runtime/sdk/core/compWrite.ts +42 -0
  7. package/runtime/sdk/core/fields.ts +1 -1
  8. package/runtime/sdk/core/time.ts +81 -0
  9. package/runtime/sdk/g2/Camera2D.ts +8 -1
  10. package/runtime/sdk/g2/CharacterController2D.ts +253 -53
  11. package/runtime/sdk/g2/Node2D.ts +80 -10
  12. package/runtime/sdk/g2/OneWay2D.ts +66 -0
  13. package/runtime/sdk/g2/Physics2D.ts +240 -30
  14. package/runtime/sdk/g2/Scene2D.ts +33 -1
  15. package/runtime/sdk/g2/Shape2D.ts +218 -22
  16. package/runtime/sdk/g2/Trigger2D.ts +42 -12
  17. package/runtime/sdk/g2/groups2d.ts +106 -0
  18. package/runtime/sdk/g2/loop.ts +15 -4
  19. package/runtime/sdk/gl/Camera.ts +40 -1
  20. package/runtime/sdk/gl/CameraPlace.ts +52 -51
  21. package/runtime/sdk/gl/CharacterController.ts +184 -56
  22. package/runtime/sdk/gl/Gearbox.ts +212 -0
  23. package/runtime/sdk/gl/Geometry.ts +70 -9
  24. package/runtime/sdk/gl/Light.ts +64 -2
  25. package/runtime/sdk/gl/Lightmap.ts +179 -0
  26. package/runtime/sdk/gl/Material.ts +25 -0
  27. package/runtime/sdk/gl/Mesh.ts +6 -23
  28. package/runtime/sdk/gl/Model.ts +16 -2
  29. package/runtime/sdk/gl/Node.ts +119 -39
  30. package/runtime/sdk/gl/Physics.ts +75 -24
  31. package/runtime/sdk/gl/Scene.ts +161 -5
  32. package/runtime/sdk/gl/Shape.ts +42 -3
  33. package/runtime/sdk/gl/Trigger.ts +45 -50
  34. package/runtime/sdk/gl/Vehicle.ts +276 -322
  35. package/runtime/sdk/gl/Wheel.ts +240 -0
  36. package/runtime/sdk/gl/scenarios.ts +4 -30
  37. package/runtime/sdk/gl/state.ts +6 -6
  38. package/runtime/sdk/inject.ts +186 -171
  39. package/runtime/sdk/runtime/device.ts +102 -1
  40. package/runtime/sdk/scene/defineScene.ts +108 -53
  41. package/runtime/sdk/scene/gizmos.ts +31 -11
  42. package/runtime/sdk/scene/material.ts +188 -0
  43. package/runtime/sdk-types.json +1 -1
  44. package/runtime/sdk/compile/aspectMacro.ts +0 -42
  45. package/runtime/sdk/compile/assetIconMacro.ts +0 -384
  46. package/runtime/sdk/compile/assetMacro.ts +0 -45
  47. package/runtime/sdk/compile/assetName.ts +0 -50
  48. package/runtime/sdk/compile/bundler.ts +0 -252
  49. package/runtime/sdk/compile/compileProject.ts +0 -129
  50. package/runtime/sdk/compile/detectEntry.ts +0 -128
  51. package/runtime/sdk/compile/fontMacro.ts +0 -459
  52. package/runtime/sdk/compile/fontRegistry.ts +0 -78
  53. package/runtime/sdk/compile/header.ts +0 -67
  54. package/runtime/sdk/compile/index.ts +0 -85
  55. package/runtime/sdk/compile/libraryImports.ts +0 -52
  56. package/runtime/sdk/compile/liteMaterial.ts +0 -247
  57. package/runtime/sdk/compile/sceneEditor.ts +0 -78
  58. package/runtime/sdk/compile/serverSplit.ts +0 -233
  59. package/runtime/sdk/compile/serverTypes.ts +0 -227
  60. package/runtime/sdk/compile/sfnt.ts +0 -98
  61. package/runtime/sdk/compile/shaderTargets.ts +0 -42
  62. package/runtime/sdk/compile/sourcemap.ts +0 -25
package/dist/index.js CHANGED
@@ -418,7 +418,7 @@ node_modules/
418
418
  return "updated";
419
419
  };
420
420
  var init_ignore = __esm(() => {
421
- BUILTIN_PATTERNS = ["node_modules/", ".git/", "_*", "*.log", ".DS_Store", ".artifacts/"];
421
+ BUILTIN_PATTERNS = ["node_modules/", ".git/", "_*", "*.log", ".DS_Store", ".artifacts/", "dist/", ".env", ".env.*"];
422
422
  });
423
423
 
424
424
  // src/localFiles.ts
@@ -564,45 +564,212 @@ var init_distRoot = __esm(() => {
564
564
  });
565
565
 
566
566
  // ../sdk/src/compile/assetMacro.ts
567
- var ASSET_RE, hasAssetMacro = (src) => src.includes("asset("), desugarAssetMacros = (src, filePath) => {
568
- const byPath = new Map;
567
+ var ASSET_RE, ASSET_CALL_RE, hasAssetMacro = (src) => src.includes("asset("), maskNonCode = (src) => {
568
+ const out = src.split("");
569
+ const blank = (from, to) => {
570
+ for (let k = from;k < to && k < out.length; k++)
571
+ if (out[k] !== `
572
+ `)
573
+ out[k] = " ";
574
+ };
575
+ const tpl = [];
576
+ let braces = 0;
569
577
  let i = 0;
570
- const out = src.replace(ASSET_RE, (_match, _quote, path) => {
571
- let name = byPath.get(path);
572
- if (!name) {
573
- name = `__asset_${i++}`;
574
- byPath.set(path, name);
578
+ const scanTemplate = () => {
579
+ const start = i;
580
+ while (i < src.length) {
581
+ const c2 = src[i];
582
+ if (c2 === "\\") {
583
+ i += 2;
584
+ continue;
585
+ }
586
+ if (c2 === "`") {
587
+ blank(start, i);
588
+ i++;
589
+ return;
590
+ }
591
+ if (c2 === "$" && src[i + 1] === "{") {
592
+ blank(start, i);
593
+ tpl.push(braces);
594
+ i += 2;
595
+ return;
596
+ }
597
+ i++;
598
+ }
599
+ blank(start, i);
600
+ };
601
+ while (i < src.length) {
602
+ const c2 = src[i], d = src[i + 1];
603
+ if (c2 === "/" && d === "/") {
604
+ const e = src.indexOf(`
605
+ `, i);
606
+ const stop = e === -1 ? src.length : e;
607
+ blank(i, stop);
608
+ i = stop;
609
+ continue;
610
+ }
611
+ if (c2 === "/" && d === "*") {
612
+ const e = src.indexOf("*/", i + 2);
613
+ const stop = e === -1 ? src.length : e + 2;
614
+ blank(i, stop);
615
+ i = stop;
616
+ continue;
617
+ }
618
+ if (c2 === "'" || c2 === '"') {
619
+ const start = i;
620
+ i++;
621
+ while (i < src.length) {
622
+ const q = src[i];
623
+ if (q === "\\") {
624
+ i += 2;
625
+ continue;
626
+ }
627
+ i++;
628
+ if (q === c2 || q === `
629
+ `)
630
+ break;
631
+ }
632
+ blank(start, i);
633
+ continue;
634
+ }
635
+ if (c2 === "`") {
636
+ i++;
637
+ scanTemplate();
638
+ continue;
639
+ }
640
+ if (c2 === "{") {
641
+ braces++;
642
+ i++;
643
+ continue;
644
+ }
645
+ if (c2 === "}") {
646
+ if (tpl.length > 0 && braces === tpl[tpl.length - 1]) {
647
+ tpl.pop();
648
+ i++;
649
+ scanTemplate();
650
+ continue;
651
+ }
652
+ if (braces > 0)
653
+ braces--;
654
+ i++;
655
+ continue;
656
+ }
657
+ i++;
658
+ }
659
+ return out.join("");
660
+ }, collectAssetMacros = (src, filePath) => {
661
+ const mask = maskNonCode(src);
662
+ const refs = [];
663
+ const literal = new Set;
664
+ let scanned = 0;
665
+ let line = 1;
666
+ for (const m of src.matchAll(ASSET_RE)) {
667
+ const index = m.index ?? 0;
668
+ if (mask[index] !== "a")
669
+ continue;
670
+ for (;scanned < index; scanned++)
671
+ if (src[scanned] === `
672
+ `)
673
+ line++;
674
+ refs.push({ path: m[2], index, length: m[0].length, line });
675
+ literal.add(index);
676
+ }
677
+ for (const m of src.matchAll(ASSET_CALL_RE)) {
678
+ const index = m.index ?? 0;
679
+ if (mask[index] === "a" && !literal.has(index)) {
680
+ throw new Error(`asset() expects a string literal, found a dynamic argument in: ${filePath}`);
575
681
  }
576
- return name;
577
- });
578
- if (/(?<![.\w$])asset\(/.test(out)) {
579
- throw new Error(`asset() expects a string literal, found a dynamic argument in: ${filePath}`);
580
682
  }
581
- if (byPath.size === 0)
683
+ return refs;
684
+ }, desugarAssetMacros = (src, filePath, found) => {
685
+ const refs = found ?? collectAssetMacros(src, filePath);
686
+ if (refs.length === 0)
582
687
  return src;
688
+ const byPath = new Map;
689
+ const parts = [];
690
+ let at = 0;
691
+ for (const ref of refs) {
692
+ let name = byPath.get(ref.path);
693
+ if (!name) {
694
+ name = `__asset_${byPath.size}`;
695
+ byPath.set(ref.path, name);
696
+ }
697
+ parts.push(src.slice(at, ref.index), name);
698
+ at = ref.index + ref.length;
699
+ }
700
+ parts.push(src.slice(at));
583
701
  const imports = [...byPath].map(([path, name]) => `import ${name} from ${JSON.stringify(path)};`).join(`
584
702
  `);
585
- return out + `
703
+ return parts.join("") + `
586
704
  ` + imports;
587
705
  };
588
706
  var init_assetMacro = __esm(() => {
589
707
  ASSET_RE = /(?<![.\w$])asset\(\s*(["'])((?:[^"'\\]|\\.)*)\1\s*\)/g;
708
+ ASSET_CALL_RE = /(?<![.\w$])asset\(/g;
590
709
  });
591
710
 
592
711
  // ../sdk/src/compile/aspectMacro.ts
593
- var ASPECT_RE, hasAspectExtends = (src) => /\bextends\s+Aspect\s*</.test(src), injectAspectNames = (src) => {
712
+ var HEAD_RE, hasAspectExtends = (src) => /\bextends\s+(?:Aspect|System)\s*</.test(src), injectAspectNames = (src) => {
594
713
  let out = "";
595
714
  let last = 0;
596
- ASPECT_RE.lastIndex = 0;
715
+ HEAD_RE.lastIndex = 0;
597
716
  let m;
598
- while ((m = ASPECT_RE.exec(src)) !== null) {
599
- const braceEnd = m.index + m[0].length;
717
+ while ((m = HEAD_RE.exec(src)) !== null) {
718
+ const braceEnd = classBodyStart(src, m.index + m[0].length);
719
+ if (braceEnd < 0)
720
+ continue;
721
+ HEAD_RE.lastIndex = braceEnd;
600
722
  if (!classBodyDeclaresAspect(src, braceEnd)) {
601
723
  out += src.slice(last, braceEnd) + ` static aspect = ${JSON.stringify(m[2])};`;
602
724
  last = braceEnd;
603
725
  }
604
726
  }
605
727
  return last === 0 ? src : out + src.slice(last);
728
+ }, classBodyStart = (src, from) => {
729
+ let angle = 1;
730
+ let i = from;
731
+ for (;i < src.length && angle > 0; i++) {
732
+ const c2 = src[i];
733
+ if (c2 === '"' || c2 === "'" || c2 === "`") {
734
+ i = skipString(src, i) - 1;
735
+ continue;
736
+ }
737
+ if (c2 === "=" && src[i + 1] === ">") {
738
+ i++;
739
+ continue;
740
+ }
741
+ if (c2 === "<")
742
+ angle++;
743
+ else if (c2 === ">")
744
+ angle--;
745
+ }
746
+ if (angle !== 0)
747
+ return -1;
748
+ angle = 0;
749
+ for (;i < src.length; i++) {
750
+ const c2 = src[i];
751
+ if (c2 === "<")
752
+ angle++;
753
+ else if (c2 === ">")
754
+ angle--;
755
+ else if (c2 === "{" && angle === 0)
756
+ return i + 1;
757
+ else if (c2 === ";" || c2 === "}")
758
+ return -1;
759
+ }
760
+ return -1;
761
+ }, skipString = (src, at) => {
762
+ const q = src[at];
763
+ let i = at + 1;
764
+ for (;i < src.length; i++) {
765
+ if (src[i] === "\\") {
766
+ i++;
767
+ continue;
768
+ }
769
+ if (src[i] === q)
770
+ return i + 1;
771
+ }
772
+ return i;
606
773
  }, classBodyDeclaresAspect = (src, from) => {
607
774
  let depth = 1;
608
775
  let i = from;
@@ -616,7 +783,7 @@ var ASPECT_RE, hasAspectExtends = (src) => /\bextends\s+Aspect\s*</.test(src), i
616
783
  return /\bstatic\b[^;{}()=]*\baspect\b/.test(src.slice(from, i));
617
784
  };
618
785
  var init_aspectMacro = __esm(() => {
619
- ASPECT_RE = /\bclass\s+\w+\s+extends\s+Aspect\s*<\s*(["'])((?:[^"'\\]|\\.)*)\1[^{};]*>\s*\{/g;
786
+ HEAD_RE = /\bclass\s+\w+\s+extends\s+(?:Aspect|System)\s*<\s*(["'])((?:[^"'\\]|\\.)*)\1/g;
620
787
  });
621
788
 
622
789
  // ../sdk/src/compile/libraryImports.ts
@@ -723,18 +890,37 @@ var resolveInject = () => {
723
890
  } catch {
724
891
  return null;
725
892
  }
893
+ }, assertAssetsExist = (refs, files) => {
894
+ const missing = [];
895
+ for (const { file, ref } of refs) {
896
+ if (!ref.path.startsWith("./") && !ref.path.startsWith("../") && !ref.path.startsWith("/"))
897
+ continue;
898
+ const dir = file.slice(0, file.lastIndexOf("/"));
899
+ const base = ref.path.startsWith("/") ? normalize(ref.path) : normalize(`${dir}/${ref.path}`);
900
+ const found = [base, `${base}.ts`, `${base}.js`, `${base}/index.ts`].some((c2) => files[c2] !== undefined);
901
+ if (!found)
902
+ missing.push(`asset not found: ${ref.path} (${file.replace(/^\//, "")}:${ref.line})`);
903
+ }
904
+ if (missing.length > 0)
905
+ throw new Error(missing.join(`
906
+ `));
726
907
  }, bundleProjectChisel = async (entrypoint, files, format, sourcemap, minifyOverride, editor, libraries, inject = CLIENT_INJECT) => {
727
908
  const entry = normalize(entrypoint);
728
909
  const slugs = new Set(Object.keys(libraries ?? {}));
729
910
  const projFiles = {};
911
+ const assetRefs = [];
730
912
  for (const [k, v] of Object.entries(files)) {
731
913
  const path = normalize(k);
732
914
  let src = v;
733
915
  if (/\.[tj]sx?$/.test(path)) {
734
916
  if (hasImportStatement(src))
735
917
  src = rewriteLibraryImports(src, slugs);
736
- if (hasAssetMacro(src))
737
- src = desugarAssetMacros(src, path);
918
+ if (hasAssetMacro(src)) {
919
+ const found = collectAssetMacros(src, path);
920
+ for (const ref of found)
921
+ assetRefs.push({ file: path, ref });
922
+ src = desugarAssetMacros(src, path, found);
923
+ }
738
924
  if (hasAspectExtends(src))
739
925
  src = injectAspectNames(src);
740
926
  }
@@ -747,14 +933,19 @@ var resolveInject = () => {
747
933
  const path = normalize(`/__lib/${slug}/${k}`);
748
934
  let src = v;
749
935
  if (/\.[tj]sx?$/.test(path)) {
750
- if (hasAssetMacro(src))
751
- src = desugarAssetMacros(src, path);
936
+ if (hasAssetMacro(src)) {
937
+ const found = collectAssetMacros(src, path);
938
+ for (const ref of found)
939
+ assetRefs.push({ file: path, ref });
940
+ src = desugarAssetMacros(src, path, found);
941
+ }
752
942
  if (hasAspectExtends(src))
753
943
  src = injectAspectNames(src);
754
944
  }
755
945
  projFiles[path] = src;
756
946
  }
757
947
  }
948
+ assertAssetsExist(assetRefs, projFiles);
758
949
  const sdkFiles = collectSdkFiles(resolveSdkSrc());
759
950
  const minify = minifyOverride ?? process.env.LECODES_CHISEL_MINIFY !== "0";
760
951
  const input = JSON.stringify({
@@ -2983,7 +3174,7 @@ var require_source_map_support = __commonJS((exports, module) => {
2983
3174
 
2984
3175
  // ../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/typescript.js
2985
3176
  var require_typescript = __commonJS((exports, module) => {
2986
- var __dirname = "C:\\Users\\Den\\Desktop\\LeCodes\\lecodes\\node_modules\\.bun\\typescript@5.8.3\\node_modules\\typescript\\lib", __filename = "C:\\Users\\Den\\Desktop\\LeCodes\\lecodes\\node_modules\\.bun\\typescript@5.8.3\\node_modules\\typescript\\lib\\typescript.js";
3177
+ var __dirname = "/Users/den/Desktop/Ingenuity/le-codes/node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib", __filename = "/Users/den/Desktop/Ingenuity/le-codes/node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/typescript.js";
2987
3178
  /*! *****************************************************************************
2988
3179
  Copyright (c) Microsoft Corporation. All rights reserved.
2989
3180
  Licensed under the Apache License, Version 2.0 (the "License"); you may not use
@@ -98214,23 +98405,23 @@ ${lanes.join(`
98214
98405
  const call = cast(aliasAssignment ? skipOuterExpressions(aliasAssignment.right) : initializer, isCallExpression);
98215
98406
  const func = cast(skipOuterExpressions(call.expression), isFunctionExpression);
98216
98407
  const funcStatements = func.body.statements;
98217
- let classBodyStart = 0;
98408
+ let classBodyStart2 = 0;
98218
98409
  let classBodyEnd = -1;
98219
98410
  const statements = [];
98220
98411
  if (aliasAssignment) {
98221
- const extendsCall = tryCast(funcStatements[classBodyStart], isExpressionStatement);
98412
+ const extendsCall = tryCast(funcStatements[classBodyStart2], isExpressionStatement);
98222
98413
  if (extendsCall) {
98223
98414
  statements.push(extendsCall);
98224
- classBodyStart++;
98415
+ classBodyStart2++;
98225
98416
  }
98226
- statements.push(funcStatements[classBodyStart]);
98227
- classBodyStart++;
98417
+ statements.push(funcStatements[classBodyStart2]);
98418
+ classBodyStart2++;
98228
98419
  statements.push(factory2.createExpressionStatement(factory2.createAssignment(aliasAssignment.left, cast(variable.name, isIdentifier))));
98229
98420
  }
98230
98421
  while (!isReturnStatement(elementAt(funcStatements, classBodyEnd))) {
98231
98422
  classBodyEnd--;
98232
98423
  }
98233
- addRange(statements, funcStatements, classBodyStart, classBodyEnd);
98424
+ addRange(statements, funcStatements, classBodyStart2, classBodyEnd);
98234
98425
  if (classBodyEnd < -1) {
98235
98426
  addRange(statements, funcStatements, classBodyEnd + 1);
98236
98427
  }
@@ -171460,11 +171651,11 @@ var init_detectEntry = __esm(() => {
171460
171651
  });
171461
171652
 
171462
171653
  // ../sdk/src/compile/shaderTargets.ts
171463
- var externalSamplerReg, PLATFORM_BUILD, matcPlatformsFor = (text) => {
171654
+ var externalSamplerReg, PLATFORM_BUILD, LOCAL_PLATFORM_BUILD, ALL_PLATFORM_BUILD, LOCAL_ONLY_PLATFORMS, TARGET_MATRIX_GENERATION = 1, matcPlatformsFor = (text) => {
171464
171655
  externalSamplerReg.lastIndex = 0;
171465
171656
  return externalSamplerReg.test(text) ? ["opengl", "metal", "webgl", "desktop"] : ["opengl", "metal", "desktop"];
171466
171657
  }, matcArgs = (platform, outPath, inPath) => {
171467
- const build = PLATFORM_BUILD[platform];
171658
+ const build = ALL_PLATFORM_BUILD[platform];
171468
171659
  return ["-p", build.profile, "--no-essl1", "-a", build.api, "-o", outPath, inPath];
171469
171660
  };
171470
171661
  var init_shaderTargets = __esm(() => {
@@ -171475,6 +171666,11 @@ var init_shaderTargets = __esm(() => {
171475
171666
  webgl: { profile: "mobile", api: "opengl", replaceExternal: true },
171476
171667
  desktop: { profile: "desktop", api: "opengl", replaceExternal: true }
171477
171668
  };
171669
+ LOCAL_PLATFORM_BUILD = {
171670
+ "desktop-vulkan": { profile: "desktop", api: "vulkan", replaceExternal: true }
171671
+ };
171672
+ ALL_PLATFORM_BUILD = { ...PLATFORM_BUILD, ...LOCAL_PLATFORM_BUILD };
171673
+ LOCAL_ONLY_PLATFORMS = ["desktop-vulkan"];
171478
171674
  });
171479
171675
 
171480
171676
  // ../sdk/src/compile/liteMaterial.ts
@@ -171717,6 +171913,191 @@ var init_liteMaterial = __esm(() => {
171717
171913
  SAMPLER_TYPES = new Set(["sampler2d", "samplerExternal"]);
171718
171914
  });
171719
171915
 
171916
+ // ../sdk/src/compile/shaderSchema.ts
171917
+ var SAMPLER_TYPES2, COLOR_NAME, zeroFor = (type, editor) => {
171918
+ switch (editor) {
171919
+ case "number":
171920
+ return 0;
171921
+ case "int":
171922
+ return 0;
171923
+ case "switch":
171924
+ return false;
171925
+ case "vec2":
171926
+ return [0, 0];
171927
+ case "vec3":
171928
+ return [0, 0, 0];
171929
+ case "vec4":
171930
+ return [0, 0, 0, 0];
171931
+ case "color":
171932
+ return type === "float4" ? "#ffffffff" : "#ffffff";
171933
+ case "texture":
171934
+ return null;
171935
+ default:
171936
+ return null;
171937
+ }
171938
+ }, parseDefault = (raw, editor) => {
171939
+ const s = raw.trim();
171940
+ if (s === "true")
171941
+ return true;
171942
+ if (s === "false")
171943
+ return false;
171944
+ if (/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(s))
171945
+ return s.toLowerCase();
171946
+ if (/^-?\d+(\.\d+)?$/.test(s))
171947
+ return editor === "switch" ? Number(s) !== 0 : Number(s);
171948
+ const arr = /^\[(.*)\]$/.exec(s);
171949
+ if (arr) {
171950
+ const nums = arr[1].split(",").map((p) => Number(p.trim()));
171951
+ if (nums.length > 0 && nums.every((n) => Number.isFinite(n)))
171952
+ return nums;
171953
+ }
171954
+ return;
171955
+ }, parseAnnotation = (comment) => {
171956
+ const out = { tooltip: "", errors: [] };
171957
+ let rest = comment;
171958
+ rest = rest.replace(/@(color|vec)\b/g, (_m, tag) => {
171959
+ out[tag] = true;
171960
+ return " ";
171961
+ });
171962
+ rest = rest.replace(/@(range|step|default)\s*\(([^)]*)\)/g, (_m, tag, body) => {
171963
+ if (tag === "range") {
171964
+ const parts = body.split(",").map((p) => Number(p.trim()));
171965
+ if (parts.length === 2 && parts.every((n) => Number.isFinite(n)))
171966
+ out.range = [parts[0], parts[1]];
171967
+ else
171968
+ out.errors.push(`@range expects two numbers, got "${body.trim()}"`);
171969
+ } else if (tag === "step") {
171970
+ const n = Number(body.trim());
171971
+ if (Number.isFinite(n) && n > 0)
171972
+ out.step = n;
171973
+ else
171974
+ out.errors.push(`@step expects a positive number, got "${body.trim()}"`);
171975
+ } else {
171976
+ out.default = body;
171977
+ }
171978
+ return " ";
171979
+ });
171980
+ rest = rest.replace(/@\w+(\([^)]*\))?/g, " ");
171981
+ out.tooltip = rest.replace(/\s+/g, " ").trim();
171982
+ return out;
171983
+ }, collectComments = (materialBlock) => {
171984
+ const out = new Map;
171985
+ const start = materialBlock.search(/\bparameters\s*[:=]/);
171986
+ if (start < 0)
171987
+ return out;
171988
+ const lines = materialBlock.slice(start).split(`
171989
+ `);
171990
+ let above = [];
171991
+ for (const line of lines) {
171992
+ const trimmed = line.trim();
171993
+ const name = /\bname\s*[:=]\s*"?([A-Za-z_]\w*)"?/.exec(line)?.[1];
171994
+ if (name) {
171995
+ const trailing = /\}[^/\n]*\/\/(.*)$/.exec(line)?.[1] ?? /\/\/(.*)$/.exec(line.slice(line.indexOf("}") + 1))?.[1];
171996
+ const text = trailing !== undefined ? trailing : above.join(" ");
171997
+ if (text.trim() !== "")
171998
+ out.set(name, text.trim());
171999
+ above = [];
172000
+ } else if (trimmed.startsWith("//")) {
172001
+ above.push(trimmed.slice(2).trim());
172002
+ } else if (trimmed !== "" && trimmed !== "[" && trimmed !== "],") {
172003
+ above = [];
172004
+ }
172005
+ }
172006
+ return out;
172007
+ }, parseShaderSchema = (text) => {
172008
+ const schema = { name: "", shadingModel: "lit", blending: "opaque", params: [], errors: [] };
172009
+ let block;
172010
+ let def;
172011
+ try {
172012
+ block = extractMatBlocks(text).material;
172013
+ if (block === undefined) {
172014
+ schema.errors.push("no material block");
172015
+ return schema;
172016
+ }
172017
+ def = parseJsonish("{" + block + "}");
172018
+ } catch (e) {
172019
+ schema.errors.push(e instanceof Error ? e.message : String(e));
172020
+ return schema;
172021
+ }
172022
+ schema.name = typeof def.name === "string" ? def.name : "";
172023
+ schema.shadingModel = typeof def.shadingModel === "string" ? def.shadingModel : "lit";
172024
+ schema.blending = typeof def.blending === "string" ? def.blending : "opaque";
172025
+ const comments = collectComments(block);
172026
+ const rawParams = def.parameters === undefined ? [] : Array.isArray(def.parameters) ? def.parameters : [def.parameters];
172027
+ for (const raw of rawParams) {
172028
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
172029
+ schema.errors.push("malformed parameter entry");
172030
+ continue;
172031
+ }
172032
+ const entry = raw;
172033
+ const name = String(entry.name ?? "");
172034
+ const typeRaw = String(entry.type ?? "");
172035
+ const typeMatch = /^(\w+)(?:\[(\d+)\])?$/.exec(typeRaw);
172036
+ if (!name || !typeMatch) {
172037
+ schema.errors.push(`malformed parameter "${name || typeRaw}"`);
172038
+ continue;
172039
+ }
172040
+ const type = typeMatch[1];
172041
+ const size = typeMatch[2] ? Number(typeMatch[2]) : undefined;
172042
+ const ann = parseAnnotation(comments.get(name) ?? "");
172043
+ for (const e of ann.errors)
172044
+ schema.errors.push(`${name}: ${e}`);
172045
+ let editor;
172046
+ if (size !== undefined)
172047
+ editor = "array";
172048
+ else if (SAMPLER_TYPES2.has(type))
172049
+ editor = "texture";
172050
+ else if (type === "float")
172051
+ editor = "number";
172052
+ else if (type === "int" || type === "uint")
172053
+ editor = "int";
172054
+ else if (type === "bool")
172055
+ editor = "switch";
172056
+ else if (type === "float2")
172057
+ editor = "vec2";
172058
+ else if (type === "float3" || type === "float4") {
172059
+ const isColor = ann.color === true || ann.vec !== true && COLOR_NAME.test(name);
172060
+ editor = isColor ? "color" : type === "float3" ? "vec3" : "vec4";
172061
+ } else if (/^(int|uint|bool)[234]$/.test(type))
172062
+ editor = `vec${type.slice(-1)}`;
172063
+ else if (/^float[234]x[234]$/.test(type))
172064
+ editor = "matrix";
172065
+ else
172066
+ editor = "array";
172067
+ const param = { name, type, editor, default: zeroFor(type, editor) };
172068
+ if (size !== undefined)
172069
+ param.size = size;
172070
+ if (ann.range) {
172071
+ param.min = ann.range[0];
172072
+ param.max = ann.range[1];
172073
+ }
172074
+ if (ann.step !== undefined)
172075
+ param.step = ann.step;
172076
+ if (ann.default !== undefined) {
172077
+ const v = parseDefault(ann.default, editor);
172078
+ if (v === undefined)
172079
+ schema.errors.push(`${name}: unreadable @default(${ann.default.trim()})`);
172080
+ else
172081
+ param.default = v;
172082
+ }
172083
+ if (ann.tooltip)
172084
+ param.tooltip = ann.tooltip;
172085
+ schema.params.push(param);
172086
+ }
172087
+ return schema;
172088
+ }, shaderDefaults = (schema) => {
172089
+ const out = {};
172090
+ for (const p of schema.params)
172091
+ if (p.editor !== "texture" && p.default !== null)
172092
+ out[p.name] = p.default;
172093
+ return out;
172094
+ };
172095
+ var init_shaderSchema = __esm(() => {
172096
+ init_liteMaterial();
172097
+ SAMPLER_TYPES2 = new Set(["sampler2d", "samplerExternal", "sampler2dArray", "samplerCubemap", "sampler3d"]);
172098
+ COLOR_NAME = /colou?r|tint|albedo|emissive|glare|glow/i;
172099
+ });
172100
+
171720
172101
  // ../sdk/src/compile/sourcemap.ts
171721
172102
  var offsetSourceMap = (mapJson, lines) => {
171722
172103
  if (lines <= 0)
@@ -171865,13 +172246,18 @@ export {}
171865
172246
  });
171866
172247
  const editorImports = projectPaths.filter((p) => p.endsWith(".editor.ts")).map((p) => `import "${p}"
171867
172248
  `).join("");
172249
+ const materialPaths = projectPaths.filter((p) => p.endsWith(".material.ts"));
172250
+ const materialImports = materialPaths.map((p, i) => `import __mat${i} from "${p}"
172251
+ `).join("");
172252
+ const materialRegister = materialPaths.length === 0 ? "" : `;(globalThis as any).__lecodesMaterials = { ${materialPaths.map((p, i) => `${JSON.stringify(p)}: __mat${i}`).join(", ")} }
172253
+ `;
171868
172254
  entries.push({
171869
172255
  path: SCENE_ENTRY_PATH,
171870
172256
  type: "text",
171871
172257
  text: `import "${SCENE_HARNESS_DIR}/.edit-mode.ts"
171872
- ` + `import "${scenePath}"
172258
+ ` + materialImports + `import "${scenePath}"
171873
172259
  ` + editorImports + `import "${SCENE_HARNESS_DIR}/main.ts"
171874
- `
172260
+ ` + materialRegister
171875
172261
  });
171876
172262
  return entries;
171877
172263
  };
@@ -172002,7 +172388,7 @@ var TAG_TTF = 65536, TAG_OTTO = 1330926671, TAG_TTCF = 1953784678, TAG_WOFF = 20
172002
172388
  };
172003
172389
 
172004
172390
  // ../sdk/src/compile/fontMacro.ts
172005
- var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c2) => c2 >= "A" && c2 <= "Z" || c2 >= "a" && c2 <= "z" || c2 === "_" || c2 === "$", isIdentPart = (c2) => isIdentStart(c2) || c2 >= "0" && c2 <= "9", skipString = (src, start) => {
172391
+ var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c2) => c2 >= "A" && c2 <= "Z" || c2 >= "a" && c2 <= "z" || c2 === "_" || c2 === "$", isIdentPart = (c2) => isIdentStart(c2) || c2 >= "0" && c2 <= "9", skipString2 = (src, start) => {
172006
172392
  const quote = src[start];
172007
172393
  const n = src.length;
172008
172394
  let i = start + 1;
@@ -172028,7 +172414,7 @@ var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c2) => c2 >= "A" && c2 <=
172028
172414
  while (i < n) {
172029
172415
  const c2 = src[i];
172030
172416
  if (c2 === '"' || c2 === "'" || c2 === "`") {
172031
- i = skipString(src, i);
172417
+ i = skipString2(src, i);
172032
172418
  continue;
172033
172419
  }
172034
172420
  if (c2 === "{")
@@ -172048,7 +172434,7 @@ var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c2) => c2 >= "A" && c2 <=
172048
172434
  while (i < n) {
172049
172435
  const c2 = src[i];
172050
172436
  if (c2 === '"' || c2 === "'" || c2 === "`") {
172051
- i = skipString(src, i);
172437
+ i = skipString2(src, i);
172052
172438
  continue;
172053
172439
  }
172054
172440
  if (c2 === "/" && src[i + 1] === "/") {
@@ -172084,7 +172470,7 @@ var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c2) => c2 >= "A" && c2 <=
172084
172470
  while (i < n) {
172085
172471
  const c2 = text[i];
172086
172472
  if (c2 === '"' || c2 === "'" || c2 === "`") {
172087
- i = skipString(text, i);
172473
+ i = skipString2(text, i);
172088
172474
  continue;
172089
172475
  }
172090
172476
  if (c2 === "(" || c2 === "[" || c2 === "{")
@@ -172124,7 +172510,7 @@ var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c2) => c2 >= "A" && c2 <=
172124
172510
  while (i < n) {
172125
172511
  const c2 = source[i];
172126
172512
  if (c2 === '"' || c2 === "'" || c2 === "`") {
172127
- i = skipString(source, i);
172513
+ i = skipString2(source, i);
172128
172514
  continue;
172129
172515
  }
172130
172516
  if (c2 === "/" && source[i + 1] === "/") {
@@ -172412,7 +172798,7 @@ var init_fontMacro = __esm(() => {
172412
172798
  });
172413
172799
 
172414
172800
  // ../sdk/src/compile/assetIconMacro.ts
172415
- var ICON_ID, isIdentStart2 = (c2) => c2 >= "A" && c2 <= "Z" || c2 >= "a" && c2 <= "z" || c2 === "_" || c2 === "$", isIdentPart2 = (c2) => isIdentStart2(c2) || c2 >= "0" && c2 <= "9", skipString2 = (src, start) => {
172801
+ var ICON_ID, isIdentStart2 = (c2) => c2 >= "A" && c2 <= "Z" || c2 >= "a" && c2 <= "z" || c2 === "_" || c2 === "$", isIdentPart2 = (c2) => isIdentStart2(c2) || c2 >= "0" && c2 <= "9", skipString3 = (src, start) => {
172416
172802
  const quote = src[start];
172417
172803
  const n = src.length;
172418
172804
  let i = start + 1;
@@ -172438,7 +172824,7 @@ var ICON_ID, isIdentStart2 = (c2) => c2 >= "A" && c2 <= "Z" || c2 >= "a" && c2 <
172438
172824
  while (i < n) {
172439
172825
  const c2 = src[i];
172440
172826
  if (c2 === '"' || c2 === "'" || c2 === "`") {
172441
- i = skipString2(src, i);
172827
+ i = skipString3(src, i);
172442
172828
  continue;
172443
172829
  }
172444
172830
  if (c2 === "{")
@@ -172458,7 +172844,7 @@ var ICON_ID, isIdentStart2 = (c2) => c2 >= "A" && c2 <= "Z" || c2 >= "a" && c2 <
172458
172844
  while (i < n) {
172459
172845
  const c2 = src[i];
172460
172846
  if (c2 === '"' || c2 === "'" || c2 === "`") {
172461
- i = skipString2(src, i);
172847
+ i = skipString3(src, i);
172462
172848
  continue;
172463
172849
  }
172464
172850
  if (c2 === "/" && src[i + 1] === "/") {
@@ -172494,7 +172880,7 @@ var ICON_ID, isIdentStart2 = (c2) => c2 >= "A" && c2 <= "Z" || c2 >= "a" && c2 <
172494
172880
  while (i < n) {
172495
172881
  const c2 = text[i];
172496
172882
  if (c2 === '"' || c2 === "'" || c2 === "`") {
172497
- i = skipString2(text, i);
172883
+ i = skipString3(text, i);
172498
172884
  continue;
172499
172885
  }
172500
172886
  if (c2 === "(" || c2 === "[" || c2 === "{")
@@ -172529,7 +172915,7 @@ var ICON_ID, isIdentStart2 = (c2) => c2 >= "A" && c2 <= "Z" || c2 >= "a" && c2 <
172529
172915
  while (i < n) {
172530
172916
  const c2 = source[i];
172531
172917
  if (c2 === '"' || c2 === "'" || c2 === "`") {
172532
- i = skipString2(source, i);
172918
+ i = skipString3(source, i);
172533
172919
  continue;
172534
172920
  }
172535
172921
  if (c2 === "/" && source[i + 1] === "/") {
@@ -172731,6 +173117,7 @@ var exports_compile = {};
172731
173117
  __export(exports_compile, {
172732
173118
  suggestIconNames: () => suggestIconNames,
172733
173119
  stubServerEntries: () => stubServerEntries,
173120
+ shaderDefaults: () => shaderDefaults,
172734
173121
  serverStubSource: () => serverStubSource,
172735
173122
  serverModuleId: () => serverModuleId,
172736
173123
  serverEntrySource: () => serverEntrySource,
@@ -172743,6 +173130,7 @@ __export(exports_compile, {
172743
173130
  scanAssetIconCalls: () => scanAssetIconCalls,
172744
173131
  sanitizeAssetName: () => sanitizeAssetName,
172745
173132
  rewriteAssetIcons: () => rewriteAssetIcons,
173133
+ parseShaderSchema: () => parseShaderSchema,
172746
173134
  parseSfnt: () => parseSfnt,
172747
173135
  parseJsonish: () => parseJsonish,
172748
173136
  offsetSourceMap: () => offsetSourceMap,
@@ -172771,17 +173159,21 @@ __export(exports_compile, {
172771
173159
  buildFontsHeaderValue: () => buildFontsHeaderValue,
172772
173160
  applyFontMacro: () => applyFontMacro,
172773
173161
  applyAssetIconMacro: () => applyAssetIconMacro,
173162
+ TARGET_MATRIX_GENERATION: () => TARGET_MATRIX_GENERATION,
172774
173163
  SERVER_INJECT: () => SERVER_INJECT,
172775
173164
  SERVER_ENTRY_PATH: () => SERVER_ENTRY_PATH,
172776
173165
  SCENE_HARNESS_DIR: () => SCENE_HARNESS_DIR,
172777
173166
  SCENE_ENTRY_PATH: () => SCENE_ENTRY_PATH,
172778
173167
  PLATFORM_BUILD: () => PLATFORM_BUILD,
173168
+ LOCAL_PLATFORM_BUILD: () => LOCAL_PLATFORM_BUILD,
173169
+ LOCAL_ONLY_PLATFORMS: () => LOCAL_ONLY_PLATFORMS,
172779
173170
  ICON_ID: () => ICON_ID,
172780
173171
  FONT_REGISTRY: () => FONT_REGISTRY,
172781
173172
  EndpointTypeError: () => EndpointTypeError,
172782
173173
  ENTRY_CONVENTION: () => ENTRY_CONVENTION,
172783
173174
  DEFAULT_FONT_REGISTRY_BASE: () => DEFAULT_FONT_REGISTRY_BASE,
172784
- CLIENT_INJECT: () => CLIENT_INJECT
173175
+ CLIENT_INJECT: () => CLIENT_INJECT,
173176
+ ALL_PLATFORM_BUILD: () => ALL_PLATFORM_BUILD
172785
173177
  });
172786
173178
  var init_compile = __esm(() => {
172787
173179
  init_bundler();
@@ -172791,6 +173183,7 @@ var init_compile = __esm(() => {
172791
173183
  init_assetName();
172792
173184
  init_shaderTargets();
172793
173185
  init_liteMaterial();
173186
+ init_shaderSchema();
172794
173187
  init_compileProject();
172795
173188
  init_sceneEditor();
172796
173189
  init_fontMacro();
@@ -172957,7 +173350,7 @@ var init_fonts = __esm(() => {
172957
173350
 
172958
173351
  // src/compile/assetIcons.ts
172959
173352
  import { createHash as createHash5 } from "node:crypto";
172960
- import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "node:fs";
173353
+ import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "node:fs";
172961
173354
  import { homedir as homedir3 } from "node:os";
172962
173355
  import { dirname as dirname7, join as join14 } from "node:path";
172963
173356
  var DEFAULT_REGISTRY, isHttp2 = (s) => /^https?:/i.test(s), sha122 = (s) => createHash5("sha256").update(s).digest("hex").slice(0, 12), readText = async (registry, rel) => {
@@ -172970,13 +173363,13 @@ var DEFAULT_REGISTRY, isHttp2 = (s) => /^https?:/i.test(s), sha122 = (s) => crea
172970
173363
  }
172971
173364
  }
172972
173365
  try {
172973
- return readFileSync11(join14(registry, rel), "utf8");
173366
+ return readFileSync12(join14(registry, rel), "utf8");
172974
173367
  } catch {
172975
173368
  return null;
172976
173369
  }
172977
173370
  }, safeRead = (path) => {
172978
173371
  try {
172979
- return readFileSync11(path, "utf8");
173372
+ return readFileSync12(path, "utf8");
172980
173373
  } catch {
172981
173374
  return null;
172982
173375
  }
@@ -172998,7 +173391,7 @@ var DEFAULT_REGISTRY, isHttp2 = (s) => /^https?:/i.test(s), sha122 = (s) => crea
172998
173391
  return JSON.parse(raw);
172999
173392
  }, readLock2 = (path) => {
173000
173393
  try {
173001
- const lock = JSON.parse(readFileSync11(path, "utf8"));
173394
+ const lock = JSON.parse(readFileSync12(path, "utf8"));
173002
173395
  if (lock && typeof lock === "object" && lock.icons && lock.packs)
173003
173396
  return lock;
173004
173397
  } catch {}
@@ -213727,65 +214120,65 @@ var init_esm30 = __esm(() => {
213727
214120
  });
213728
214121
 
213729
214122
  // src/types.ts
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";
214123
+ import { dirname as dirname13, join as join22, relative as relative3, sep } from "node:path";
214124
+ import { existsSync as existsSync21, mkdirSync as mkdirSync15, readdirSync as readdirSync10, readFileSync as readFileSync18, rmSync as rmSync8, statSync as statSync8, writeFileSync as writeFileSync17 } from "node:fs";
213732
214125
  var TSCONFIG, listBundleFiles = (dir, base = dir) => {
213733
- if (!existsSync20(dir))
214126
+ if (!existsSync21(dir))
213734
214127
  return [];
213735
214128
  const out = [];
213736
- for (const name of readdirSync9(dir)) {
213737
- const path2 = join21(dir, name);
213738
- if (statSync7(path2).isDirectory())
214129
+ for (const name of readdirSync10(dir)) {
214130
+ const path2 = join22(dir, name);
214131
+ if (statSync8(path2).isDirectory())
213739
214132
  out.push(...listBundleFiles(path2, base));
213740
214133
  else
213741
214134
  out.push(relative3(base, path2).split(sep).join("/"));
213742
214135
  }
213743
214136
  return out;
213744
214137
  }, pruneEmptyDirs = (dir, root) => {
213745
- if (!existsSync20(dir) || !statSync7(dir).isDirectory())
214138
+ if (!existsSync21(dir) || !statSync8(dir).isDirectory())
213746
214139
  return;
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 });
214140
+ for (const name of readdirSync10(dir))
214141
+ pruneEmptyDirs(join22(dir, name), root);
214142
+ if (dir !== root && readdirSync10(dir).length === 0)
214143
+ rmSync8(dir, { recursive: true });
213751
214144
  }, writeTypeBundle = (root, bundle) => {
213752
- const typesDir = join21(root, LECODES_DIR, "types");
214145
+ const typesDir = join22(root, LECODES_DIR, "types");
213753
214146
  const before = new Set(listBundleFiles(typesDir));
213754
214147
  const stats = { added: 0, updated: 0, removed: 0, total: Object.keys(bundle).length };
213755
214148
  for (const [path2, text] of Object.entries(bundle)) {
213756
- const dest = join21(typesDir, path2);
214149
+ const dest = join22(typesDir, path2);
213757
214150
  if (!before.has(path2))
213758
214151
  stats.added++;
213759
- else if (readFileSync17(dest, "utf8") !== text)
214152
+ else if (readFileSync18(dest, "utf8") !== text)
213760
214153
  stats.updated++;
213761
214154
  else
213762
214155
  continue;
213763
- mkdirSync14(dirname13(dest), { recursive: true });
213764
- writeFileSync16(dest, text);
214156
+ mkdirSync15(dirname13(dest), { recursive: true });
214157
+ writeFileSync17(dest, text);
213765
214158
  }
213766
214159
  for (const path2 of before) {
213767
214160
  if (path2 in bundle)
213768
214161
  continue;
213769
- rmSync7(join21(typesDir, path2));
214162
+ rmSync8(join22(typesDir, path2));
213770
214163
  stats.removed++;
213771
214164
  }
213772
214165
  pruneEmptyDirs(typesDir, typesDir);
213773
214166
  return stats;
213774
214167
  }, bundleHash = (bundle) => sha256(Object.keys(bundle).sort().map((k) => `${k}\x00${bundle[k]}`).join("\x00")), readProjectTypeBundle = (root) => {
213775
- const typesDir = join21(root, LECODES_DIR, "types");
214168
+ const typesDir = join22(root, LECODES_DIR, "types");
213776
214169
  const bundle = {};
213777
214170
  for (const path2 of listBundleFiles(typesDir))
213778
- bundle[path2] = readFileSync17(join21(typesDir, path2), "utf8");
214171
+ bundle[path2] = readFileSync18(join22(typesDir, path2), "utf8");
213779
214172
  return bundle;
213780
214173
  }, materializeTypes = async (root, apiUrl) => writeTypeBundle(root, await getTypes(apiUrl)), readLocalTypeBundle = () => {
213781
214174
  const candidates = [
213782
- join21(distRoot, "runtime", "sdk-types.json"),
213783
- join21(distRoot, "..", "sdk", "dist", "types.json")
214175
+ join22(distRoot, "runtime", "sdk-types.json"),
214176
+ join22(distRoot, "..", "sdk", "dist", "types.json")
213784
214177
  ];
213785
214178
  for (const path2 of candidates) {
213786
- if (existsSync20(path2)) {
214179
+ if (existsSync21(path2)) {
213787
214180
  try {
213788
- return JSON.parse(readFileSync17(path2, "utf8"));
214181
+ return JSON.parse(readFileSync18(path2, "utf8"));
213789
214182
  } catch {}
213790
214183
  }
213791
214184
  }
@@ -213797,7 +214190,7 @@ var TSCONFIG, listBundleFiles = (dir, base = dir) => {
213797
214190
  writeTypeBundle(root, bundle);
213798
214191
  return true;
213799
214192
  }, writeTsconfig = (root) => {
213800
- writeFileSync16(join21(root, "tsconfig.json"), JSON.stringify(TSCONFIG, null, 2));
214193
+ writeFileSync17(join22(root, "tsconfig.json"), JSON.stringify(TSCONFIG, null, 2));
213801
214194
  };
213802
214195
  var init_types4 = __esm(() => {
213803
214196
  init_api();
@@ -213823,14 +214216,14 @@ var init_types4 = __esm(() => {
213823
214216
  });
213824
214217
 
213825
214218
  // src/compile/designCompile.ts
213826
- import { readFileSync as readFileSync18 } from "node:fs";
213827
- import { basename as basename8 } from "node:path";
214219
+ import { readFileSync as readFileSync19 } from "node:fs";
214220
+ import { basename as basename9 } from "node:path";
213828
214221
  var LOCAL_ASSET_SCHEME = "lecodesfile://", compileDesignScreen = async (opts) => {
213829
214222
  const { root, manifest, designDir, designKey, screenId, state } = opts;
213830
214223
  const entries = [];
213831
214224
  const scan2 = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest?.files ?? {})) });
213832
214225
  for (const file of scan2.files) {
213833
- const type = manifest?.files[file.path]?.type ?? classifyKind(basename8(file.path));
214226
+ const type = manifest?.files[file.path]?.type ?? classifyKind(basename9(file.path));
213834
214227
  if (type === "resource") {
213835
214228
  entries.push({
213836
214229
  path: file.path,
@@ -213841,7 +214234,7 @@ var LOCAL_ASSET_SCHEME = "lecodesfile://", compileDesignScreen = async (opts) =>
213841
214234
  } else if (type === "shader") {
213842
214235
  entries.push({ path: file.path, type, shaders: [] });
213843
214236
  } else {
213844
- entries.push({ path: file.path, type, text: readFileSync18(file.absPath, "utf8") });
214237
+ entries.push({ path: file.path, type, text: readFileSync19(file.absPath, "utf8") });
213845
214238
  }
213846
214239
  }
213847
214240
  const invoke = state !== undefined ? `screen(${JSON.stringify(state)})` : "screen()";
@@ -213886,7 +214279,7 @@ var init_designCompile = __esm(() => {
213886
214279
 
213887
214280
  // src/browserAuth.ts
213888
214281
  import { createServer } from "node:http";
213889
- import { spawn as spawn4 } from "node:child_process";
214282
+ import { spawn as spawn5 } from "node:child_process";
213890
214283
  import { randomBytes as randomBytes2 } from "node:crypto";
213891
214284
  import { hostname } from "node:os";
213892
214285
  var SUCCESS_HTML = `<!doctype html><meta charset="utf-8"><title>lecodes</title>
@@ -213911,16 +214304,16 @@ var SUCCESS_HTML = `<!doctype html><meta charset="utf-8"><title>lecodes</title>
213911
214304
  args = [url];
213912
214305
  }
213913
214306
  try {
213914
- const child = spawn4(cmd, args, { stdio: "ignore", detached: true });
214307
+ const child = spawn5(cmd, args, { stdio: "ignore", detached: true });
213915
214308
  child.on("error", () => {});
213916
214309
  child.unref();
213917
214310
  } catch {}
213918
214311
  }, browserLogin = async (webBase, timeoutMs = 5 * 60 * 1000, open = openBrowser) => {
213919
214312
  const state = randomBytes2(16).toString("hex");
213920
- let resolve10;
214313
+ let resolve11;
213921
214314
  let reject;
213922
214315
  const tokenPromise = new Promise((res, rej) => {
213923
- resolve10 = res;
214316
+ resolve11 = res;
213924
214317
  reject = rej;
213925
214318
  });
213926
214319
  const server2 = createServer((req, res) => {
@@ -213938,7 +214331,7 @@ var SUCCESS_HTML = `<!doctype html><meta charset="utf-8"><title>lecodes</title>
213938
214331
  }
213939
214332
  res.writeHead(200, { "content-type": "text/html" });
213940
214333
  res.end(SUCCESS_HTML);
213941
- resolve10(token);
214334
+ resolve11(token);
213942
214335
  });
213943
214336
  await new Promise((res, rej) => {
213944
214337
  server2.once("error", rej);
@@ -214358,6 +214751,10 @@ lecodes design snapshot <id> --state empty # one state → design/.snapshots/<i
214358
214751
  lecodes design snapshot <id> --all-states # every state → design/.snapshots/<id>@<state>.png|.json
214359
214752
  lecodes design snapshot # all screens → design/.snapshots/
214360
214753
  lecodes design check # concept + flow coherence (spec.md links, states, untagged)
214754
+
214755
+ lecodes render <id> [--state <s>] # the same screen through the full render CLI, when you
214756
+ lecodes render <id> --png card.png --clip <name> # need one element cropped out, a device's
214757
+ lecodes render <id> --device iphone --png x.png # real safe-area insets, or a --script run
214361
214758
  \`\`\`
214362
214759
 
214363
214760
  Either way: after every screen you create or change, look at it and fix what's wrong (overflow,
@@ -214433,14 +214830,37 @@ var init_designTemplates = __esm(() => {
214433
214830
  init_templates();
214434
214831
  });
214435
214832
 
214833
+ // src/designMeta.ts
214834
+ import { readFileSync as readFileSync20 } from "node:fs";
214835
+ import { join as join24 } from "node:path";
214836
+ var PHONE_SIZE, readMetaFrames = (designDir) => {
214837
+ try {
214838
+ const meta = JSON.parse(readFileSync20(join24(designDir, "meta.json"), "utf8"));
214839
+ const d = meta?.defaultSize;
214840
+ const valid = Array.isArray(d) && d.length === 2 && d.every((n2) => typeof n2 === "number" && Number.isFinite(n2) && n2 >= 64 && n2 <= 8192);
214841
+ return {
214842
+ screens: meta?.screens ?? {},
214843
+ defaultSize: valid ? [Math.round(d[0]), Math.round(d[1])] : PHONE_SIZE
214844
+ };
214845
+ } catch {
214846
+ return { screens: {}, defaultSize: PHONE_SIZE };
214847
+ }
214848
+ }, screenFrame = (designDir, screenId) => {
214849
+ const frames = readMetaFrames(designDir);
214850
+ return frames.screens[screenId]?.size ?? frames.defaultSize;
214851
+ };
214852
+ var init_designMeta = __esm(() => {
214853
+ PHONE_SIZE = [390, 844];
214854
+ });
214855
+
214436
214856
  // src/commands/design.ts
214437
214857
  var exports_design = {};
214438
214858
  __export(exports_design, {
214439
214859
  flushDesignComments: () => flushDesignComments,
214440
214860
  design: () => design
214441
214861
  });
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";
214862
+ import { existsSync as existsSync24, mkdirSync as mkdirSync18, readFileSync as readFileSync21, readdirSync as readdirSync13, writeFileSync as writeFileSync19, appendFileSync } from "node:fs";
214863
+ import { basename as basename10, join as join25 } from "node:path";
214444
214864
  var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214445
214865
  let root;
214446
214866
  let manifest;
@@ -214455,13 +214875,13 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214455
214875
  return {
214456
214876
  root,
214457
214877
  manifest,
214458
- name: manifest?.name ?? basename9(root),
214878
+ name: manifest?.name ?? basename10(root),
214459
214879
  dirName,
214460
- designDir: join23(root, dirName),
214880
+ designDir: join25(root, dirName),
214461
214881
  designKey: "/" + dirName.replace(/\\/g, "/")
214462
214882
  };
214463
214883
  }, loadDesignServer = async (ctx, needs = []) => {
214464
- if (!existsSync23(ctx.designDir)) {
214884
+ if (!existsSync24(ctx.designDir)) {
214465
214885
  throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
214466
214886
  }
214467
214887
  let mod;
@@ -214493,29 +214913,17 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214493
214913
  `).find((l2) => l2.trim())?.trim() ?? "";
214494
214914
  return line.length > max ? line.slice(0, max - 1) + "…" : line;
214495
214915
  }, listScreens = (designDir) => {
214496
- const dir = join23(designDir, "screens");
214497
- if (!existsSync23(dir))
214916
+ const dir = join25(designDir, "screens");
214917
+ if (!existsSync24(dir))
214498
214918
  return [];
214499
- return readdirSync12(dir).filter((f3) => f3.endsWith(".ts") && !f3.startsWith(".")).map((f3) => f3.slice(0, -3)).sort();
214500
- }, PHONE_SIZE, readMetaFrames = (designDir) => {
214501
- try {
214502
- const meta = JSON.parse(readFileSync19(join23(designDir, "meta.json"), "utf8"));
214503
- const d = meta?.defaultSize;
214504
- const valid = Array.isArray(d) && d.length === 2 && d.every((n2) => typeof n2 === "number" && Number.isFinite(n2) && n2 >= 64 && n2 <= 8192);
214505
- return {
214506
- screens: meta?.screens ?? {},
214507
- defaultSize: valid ? [Math.round(d[0]), Math.round(d[1])] : PHONE_SIZE
214508
- };
214509
- } catch {
214510
- return { screens: {}, defaultSize: PHONE_SIZE };
214511
- }
214919
+ return readdirSync13(dir).filter((f3) => f3.endsWith(".ts") && !f3.startsWith(".")).map((f3) => f3.slice(0, -3)).sort();
214512
214920
  }, writeMcpConfig = (ctx, port) => {
214513
- const path2 = join23(ctx.root, ".mcp.json");
214921
+ const path2 = join25(ctx.root, ".mcp.json");
214514
214922
  const entry = { type: "http", url: `http://localhost:${port}/mcp` };
214515
214923
  let config = {};
214516
- if (existsSync23(path2)) {
214924
+ if (existsSync24(path2)) {
214517
214925
  try {
214518
- config = JSON.parse(readFileSync19(path2, "utf8"));
214926
+ config = JSON.parse(readFileSync21(path2, "utf8"));
214519
214927
  } catch {
214520
214928
  warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-design server by hand.");
214521
214929
  return;
@@ -214528,7 +214936,7 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214528
214936
  return;
214529
214937
  }
214530
214938
  config.mcpServers["lecodes-design"] = entry;
214531
- writeFileSync18(path2, JSON.stringify(config, null, 2) + `
214939
+ writeFileSync19(path2, JSON.stringify(config, null, 2) + `
214532
214940
  `);
214533
214941
  log(`${c.green(existing ? "updated" : "created")} .mcp.json (lecodes-design MCP → ${entry.url})`);
214534
214942
  }, init = async (ctx, args) => {
@@ -214537,26 +214945,26 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214537
214945
  ["spec.md", SPEC_MD],
214538
214946
  ["README.md", README_HINT],
214539
214947
  ["CLAUDE.md", CLAUDE_MD],
214540
- [join23("shared", "tokens.ts"), TOKENS_TS],
214541
- [join23("shared", "tabs.ts"), TABS_TS],
214542
- [join23("screens", "home.ts"), HOME_SCREEN_TS]
214948
+ [join25("shared", "tokens.ts"), TOKENS_TS],
214949
+ [join25("shared", "tabs.ts"), TABS_TS],
214950
+ [join25("screens", "home.ts"), HOME_SCREEN_TS]
214543
214951
  ];
214544
- mkdirSync17(join23(ctx.designDir, "screens"), { recursive: true });
214545
- mkdirSync17(join23(ctx.designDir, "shared"), { recursive: true });
214952
+ mkdirSync18(join25(ctx.designDir, "screens"), { recursive: true });
214953
+ mkdirSync18(join25(ctx.designDir, "shared"), { recursive: true });
214546
214954
  let created = 0;
214547
214955
  for (const [rel, content] of files) {
214548
- const abs = join23(ctx.designDir, rel);
214549
- if (existsSync23(abs)) {
214956
+ const abs = join25(ctx.designDir, rel);
214957
+ if (existsSync24(abs)) {
214550
214958
  note(`kept ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
214551
214959
  continue;
214552
214960
  }
214553
- writeFileSync18(abs, content);
214961
+ writeFileSync19(abs, content);
214554
214962
  log(`${c.green("created")} ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
214555
214963
  created++;
214556
214964
  }
214557
- const ignorePath = join23(ctx.root, ".lecodesignore");
214965
+ const ignorePath = join25(ctx.root, ".lecodesignore");
214558
214966
  const ignoreLines = [`${ctx.dirName}/.snapshots/`, `${ctx.dirName}/comments/`];
214559
- const current = existsSync23(ignorePath) ? readFileSync19(ignorePath, "utf8") : "";
214967
+ const current = existsSync24(ignorePath) ? readFileSync21(ignorePath, "utf8") : "";
214560
214968
  const present = new Set(current.split(/\r?\n/));
214561
214969
  const missing = ignoreLines.filter((line) => !present.has(line));
214562
214970
  if (missing.length) {
@@ -214571,8 +214979,8 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214571
214979
  writeMcpConfig(ctx, Number(flagStr(args, "port")) || DEFAULT_PORT);
214572
214980
  const wroteTypes = materializeTypesLocal(ctx.designDir);
214573
214981
  writeTsconfig(ctx.designDir);
214574
- mkdirSync17(join23(ctx.designDir, ".lecodes", "types"), { recursive: true });
214575
- writeFileSync18(join23(ctx.designDir, ".lecodes", "types", "design-macros.d.ts"), DESIGN_MACROS_DTS);
214982
+ mkdirSync18(join25(ctx.designDir, ".lecodes", "types"), { recursive: true });
214983
+ writeFileSync19(join25(ctx.designDir, ".lecodes", "types", "design-macros.d.ts"), DESIGN_MACROS_DTS);
214576
214984
  log(`${c.green("created")} ${ctx.dirName}/tsconfig.json${wroteTypes ? ` + ${ctx.dirName}/.lecodes/types/` : ""} (IDE types)`);
214577
214985
  if (!wroteTypes)
214578
214986
  warnErr("Bundled SDK types not found — tsconfig written, but globals may show unresolved until the CLI is rebuilt with vendored types.");
@@ -214620,16 +215028,16 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214620
215028
  killer.unref?.();
214621
215029
  const png2 = flagBool(args, "png") || flagStr(args, "png") !== undefined;
214622
215030
  const frames = readMetaFrames(ctx.designDir);
214623
- const outDir = flagStr(args, "out-dir") ?? join23(ctx.designDir, ".snapshots");
215031
+ const outDir = flagStr(args, "out-dir") ?? join25(ctx.designDir, ".snapshots");
214624
215032
  const single = targets.length === 1 && requested.length === 1 && !png2 && !allStates && !flagStr(args, "out-dir");
214625
215033
  const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
214626
215034
  if (!single)
214627
- mkdirSync17(outDir, { recursive: true });
215035
+ mkdirSync18(outDir, { recursive: true });
214628
215036
  for (const id of targets) {
214629
215037
  const [width, height] = frames.screens[id]?.size ?? frames.defaultSize;
214630
215038
  let plan = [{ state: undefined, bases: [id] }];
214631
215039
  if (discover) {
214632
- const { states, canonical } = discover(readFileSync19(join23(ctx.designDir, "screens", `${id}.ts`), "utf8"));
215040
+ const { states, canonical } = discover(readFileSync21(join25(ctx.designDir, "screens", `${id}.ts`), "utf8"));
214633
215041
  if (stateFlag !== undefined) {
214634
215042
  if (!states.includes(stateFlag)) {
214635
215043
  if (requested.length === 1)
@@ -214670,11 +215078,11 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214670
215078
  for (const w of pngResult.warnings)
214671
215079
  warnErr(`${tag}: ${w}`);
214672
215080
  for (const base of bases) {
214673
- writeFileSync18(join23(outDir, `${base}.json`), result.text);
214674
- log(`${c.green("✓")} ${tag} → ${join23(outDir, `${base}.json`)}`);
215081
+ writeFileSync19(join25(outDir, `${base}.json`), result.text);
215082
+ log(`${c.green("✓")} ${tag} → ${join25(outDir, `${base}.json`)}`);
214675
215083
  if (pngResult) {
214676
- writeFileSync18(join23(outDir, `${base}.png`), pngResult.png);
214677
- log(`${c.green("✓")} ${tag} → ${join23(outDir, `${base}.png`)}`);
215084
+ writeFileSync19(join25(outDir, `${base}.png`), pngResult.png);
215085
+ log(`${c.green("✓")} ${tag} → ${join25(outDir, `${base}.png`)}`);
214678
215086
  }
214679
215087
  }
214680
215088
  }
@@ -214976,7 +215384,7 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214976
215384
  note("Read them with: lecodes design comments");
214977
215385
  }, flushDesignComments = async (args) => {
214978
215386
  const ctx = getContext(args);
214979
- if (!existsSync23(ctx.designDir))
215387
+ if (!existsSync24(ctx.designDir))
214980
215388
  return null;
214981
215389
  const platform = optionalPlatform(ctx);
214982
215390
  if (!platform)
@@ -215096,7 +215504,7 @@ var init_design = __esm(() => {
215096
215504
  init_config();
215097
215505
  init_api();
215098
215506
  init_types4();
215099
- PHONE_SIZE = [390, 844];
215507
+ init_designMeta();
215100
215508
  });
215101
215509
 
215102
215510
  // ../../node_modules/.bun/qrcode-terminal@0.12.0/node_modules/qrcode-terminal/vendor/QRCode/QRMode.js
@@ -216118,9 +216526,9 @@ var require_main = __commonJS((exports, module) => {
216118
216526
  // package.json
216119
216527
  var package_default = {
216120
216528
  name: "lecodes-cli",
216121
- version: "0.18.0",
216529
+ version: "0.18.2",
216122
216530
  dependencies: {
216123
- "@letary/chisel": "^0.8.0",
216531
+ "@letary/chisel": "^0.10.0",
216124
216532
  jimp: "^1.6.1"
216125
216533
  },
216126
216534
  devDependencies: {
@@ -216134,8 +216542,8 @@ var package_default = {
216134
216542
  },
216135
216543
  peerDependencies: {
216136
216544
  "lecodes-design": "^0.8.1",
216137
- "lecodes-renderer": "^0.8.8",
216138
- "lecodes-3d-editor": "^0.4.0",
216545
+ "lecodes-renderer": "^0.8.9",
216546
+ "lecodes-3d-editor": "^0.5.0",
216139
216547
  "lecodes-assets": "^0.1.0"
216140
216548
  },
216141
216549
  bin: {
@@ -216185,10 +216593,10 @@ var package_default = {
216185
216593
  // src/commands/app.ts
216186
216594
  init_manifest();
216187
216595
  init_config();
216188
- import { execFileSync as execFileSync4 } from "node:child_process";
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";
216596
+ import { execFileSync as execFileSync6 } from "node:child_process";
216597
+ import { cpSync as cpSync4, existsSync as existsSync20, mkdirSync as mkdirSync14, readFileSync as readFileSync17, readdirSync as readdirSync9, rmSync as rmSync7, statSync as statSync7, writeFileSync as writeFileSync16 } from "node:fs";
216190
216598
  import { homedir as homedir7 } from "node:os";
216191
- import { basename as basename7, dirname as dirname12, join as join20, resolve as resolve7 } from "node:path";
216599
+ import { basename as basename8, dirname as dirname12, join as join21, resolve as resolve8 } from "node:path";
216192
216600
 
216193
216601
  // src/commands/compile.ts
216194
216602
  init_api();
@@ -216313,7 +216721,7 @@ init_fonts();
216313
216721
  init_compile();
216314
216722
  import { execFile } from "node:child_process";
216315
216723
  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";
216724
+ import { copyFileSync, existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync11, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync7 } from "node:fs";
216317
216725
  import { tmpdir } from "node:os";
216318
216726
  import { basename as basename3, join as join13 } from "node:path";
216319
216727
 
@@ -216325,10 +216733,18 @@ import { join as join12, resolve } from "node:path";
216325
216733
  // src/releases.ts
216326
216734
  init_util();
216327
216735
  import { createHash as createHash3 } from "node:crypto";
216328
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, mkdtempSync, renameSync, rmSync, writeFileSync as writeFileSync6 } from "node:fs";
216736
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, mkdtempSync, readFileSync as readFileSync10, renameSync, rmSync, writeFileSync as writeFileSync6 } from "node:fs";
216329
216737
  import { join as join11 } from "node:path";
216330
216738
  import { spawnSync as spawnSync2 } from "node:child_process";
216331
216739
  var MANIFEST_URL = "https://storage.yandexcloud.net/le-codes/releases/versions.json";
216740
+ var ARTIFACT_STAMP = ".lecodes-artifact.json";
216741
+ var cachedArtifactSha = (dir) => {
216742
+ try {
216743
+ return JSON.parse(readFileSync10(join11(dir, ARTIFACT_STAMP), "utf8")).sha256 ?? null;
216744
+ } catch {
216745
+ return null;
216746
+ }
216747
+ };
216332
216748
  var semverNewer = (a, b) => {
216333
216749
  const pa = a.split(/[.-]/).map(Number), pb = b.split(/[.-]/).map(Number);
216334
216750
  for (let i = 0;i < 3; i++)
@@ -216370,15 +216786,20 @@ var downloadReleaseArchive = async (file, opts) => {
216370
216786
  const archiveName = file.url.split("/").pop() ?? "archive";
216371
216787
  const archive = join11(stage, archiveName);
216372
216788
  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"}`);
216789
+ if (opts.extract) {
216790
+ opts.extract(archive, stage);
216791
+ } else {
216792
+ const winTar = join11(process.env.SystemRoot ?? "C:\\Windows", "System32", "tar.exe");
216793
+ const tarExe = process.platform === "win32" && existsSync9(winTar) ? winTar : "tar";
216794
+ const tar = spawnSync2(tarExe, ["-xf", archiveName], { cwd: stage, stdio: ["ignore", "ignore", "pipe"] });
216795
+ if (tar.status !== 0)
216796
+ throw new CliError(`Couldn't extract the archive: ${tar.stderr?.toString() ?? "tar failed"}`);
216797
+ }
216378
216798
  rmSync(archive);
216379
216799
  const bad = opts.validate(stage);
216380
216800
  if (bad)
216381
216801
  throw new CliError(bad);
216802
+ writeFileSync6(join11(stage, ARTIFACT_STAMP), JSON.stringify({ sha256: file.sha256, url: file.url }, null, 2));
216382
216803
  try {
216383
216804
  renameSync(stage, opts.dest);
216384
216805
  } catch {
@@ -216404,7 +216825,13 @@ var platformKey = () => {
216404
216825
  return "windows-x64";
216405
216826
  if (process.platform === "linux")
216406
216827
  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}.`);
216828
+ if (process.platform === "darwin") {
216829
+ if (process.arch !== "arm64") {
216830
+ throw new CliError("matc isn't published for Intel macOS — set LECODES_MATC to a locally built matc " + "(creator-gl/out/cmake-release/filament/tools/matc), or push the project once so shaders " + "compile on the server.");
216831
+ }
216832
+ return "macos-arm64";
216833
+ }
216834
+ throw new CliError(`matc isn't published for ${process.platform}.`);
216408
216835
  };
216409
216836
  var cacheRoot = () => join12(homedir2(), ".lecodes", "matc");
216410
216837
  var findMatc = (dir) => {
@@ -216486,15 +216913,27 @@ var matcIdentity = (exe) => {
216486
216913
  const stat = statSync2(exe);
216487
216914
  return `${stat.size}:${Math.trunc(stat.mtimeMs)}`;
216488
216915
  };
216489
- var shaderHash = (text, matcId) => createHash4("sha256").update(matcId + "\x00" + text).digest("hex").slice(0, 16);
216916
+ var shaderHash = (text, matcId) => createHash4("sha256").update(`${TARGET_MATRIX_GENERATION}\x00${matcId}\x00${text}`).digest("hex").slice(0, 16);
216490
216917
  var artifactFamily = (text) => {
216491
- const matcPlatforms = matcPlatformsFor(text);
216918
+ const matcPlatforms = [...matcPlatformsFor(text), ...LOCAL_ONLY_PLATFORMS];
216492
216919
  return [...matcPlatforms, ...matcPlatforms.includes("webgl") ? [] : ["webgl"], "webgl-lite"];
216493
216920
  };
216494
216921
  var shaderIsCompiled = (root, text, matcId) => {
216495
216922
  const hash = shaderHash(text, matcId);
216496
216923
  return artifactFamily(text).every((p) => existsSync11(join13(shadersCacheDir(root), `${hash}_${p}.filamat`)));
216497
216924
  };
216925
+ var shaderCompiledPlatforms = (root, text, matcId) => {
216926
+ const hash = shaderHash(text, matcId);
216927
+ return artifactFamily(text).filter((p) => existsSync11(join13(shadersCacheDir(root), `${hash}_${p}.filamat`)));
216928
+ };
216929
+ var localShaderArtifact = (root, text, platform) => {
216930
+ const matcExe = resolveMatcExeCached();
216931
+ if (!matcExe)
216932
+ return null;
216933
+ const file = join13(shadersCacheDir(root), `${shaderHash(text, matcIdentity(matcExe))}_${platform}.filamat`);
216934
+ return existsSync11(file) ? file : null;
216935
+ };
216936
+ var allShaderPlatforms = () => [...Object.keys(ALL_PLATFORM_BUILD), "webgl-lite"];
216498
216937
  var runMatc = (exe, args) => new Promise((resolve2) => {
216499
216938
  execFile(exe, args, { windowsHide: true }, (error, _stdout, stderr) => {
216500
216939
  const text = String(stderr ?? "") || (error ? error.message : "");
@@ -216509,6 +216948,8 @@ var cleanMatcOutput = (err, tempName, realName) => {
216509
216948
  return (cut >= 0 ? output.slice(0, cut) : output).trim();
216510
216949
  };
216511
216950
  var NATIVE_PLATFORMS = ["opengl", "metal", "desktop"];
216951
+ var ALL_NATIVE_PLATFORMS = ["opengl", "metal", "desktop", "desktop-vulkan"];
216952
+ var desktopShaderPlatform = (platform, renderer) => platform === "win32" ? renderer === "vulkan" ? "desktop-vulkan" : "desktop" : platform === "darwin" ? "metal" : "opengl";
216512
216953
  var applyLocalShaders = async (entries, opts) => {
216513
216954
  const shaderEntries = entries.filter((e) => e.type === "shader" && e.absPath && (opts.local || !e.shaders?.length));
216514
216955
  if (shaderEntries.length === 0)
@@ -216526,16 +216967,23 @@ var applyLocalShaders = async (entries, opts) => {
216526
216967
  mkdirSync6(cacheDir, { recursive: true });
216527
216968
  const artifacts = [];
216528
216969
  for (const entry of shaderEntries) {
216529
- const text = readFileSync10(entry.absPath, "utf8");
216970
+ const text = readFileSync11(entry.absPath, "utf8");
216530
216971
  const hash = shaderHash(text, matcId);
216531
- const matcPlatforms = matcPlatformsFor(text);
216972
+ const family = artifactFamily(text);
216532
216973
  const artifact = (platform) => `${hash}_${platform}.filamat`;
216533
- if (!artifactFamily(text).every((p) => existsSync11(join13(cacheDir, artifact(p))))) {
216974
+ const has = (platform) => existsSync11(join13(cacheDir, artifact(platform)));
216975
+ const wanted = opts.platforms ? family.filter((p) => opts.platforms.includes(p)) : family;
216976
+ if (wanted.includes("webgl") && !matcPlatformsFor(text).includes("webgl") && !wanted.includes("opengl")) {
216977
+ wanted.push("opengl");
216978
+ }
216979
+ const missing = wanted.filter((p) => !has(p));
216980
+ const matcMissing = missing.filter((p) => p !== "webgl-lite" && !(p === "webgl" && !matcPlatformsFor(text).includes("webgl")));
216981
+ if (matcMissing.length > 0) {
216534
216982
  const tempName = `lecodes-matc-${randomBytes(6).toString("hex")}.mat`;
216535
216983
  const tempFile = join13(tmpdir(), tempName);
216536
216984
  try {
216537
- for (const platform of matcPlatforms) {
216538
- const build = PLATFORM_BUILD[platform];
216985
+ for (const platform of matcMissing) {
216986
+ const build = ALL_PLATFORM_BUILD[platform];
216539
216987
  writeFileSync7(tempFile, build.replaceExternal ? text.replace(externalSamplerReg, "type:sampler2d") : text);
216540
216988
  const out = join13(cacheDir, artifact(platform));
216541
216989
  const { ok, stderr } = await runMatc(matcExe, matcArgs(platform, out, tempFile));
@@ -216548,15 +216996,21 @@ ${cleanMatcOutput(stderr, tempName, basename3(entry.path))}`);
216548
216996
  } finally {
216549
216997
  rmSync2(tempFile, { force: true });
216550
216998
  }
216551
- if (!matcPlatforms.includes("webgl")) {
216552
- copyFileSync(join13(cacheDir, artifact("opengl")), join13(cacheDir, artifact("webgl")));
216553
- }
216999
+ }
217000
+ if (missing.includes("webgl") && !matcPlatformsFor(text).includes("webgl")) {
217001
+ copyFileSync(join13(cacheDir, artifact("opengl")), join13(cacheDir, artifact("webgl")));
217002
+ }
217003
+ if (missing.includes("webgl-lite")) {
216554
217004
  writeFileSync7(join13(cacheDir, artifact("webgl-lite")), JSON.stringify(buildLiteMaterialArtifact(text)));
216555
217005
  }
216556
217006
  if (opts.local) {
216557
- entry.shaders = NATIVE_PLATFORMS.map((platform) => ({ platform, src: artifact(platform) }));
217007
+ const staged = opts.platforms ? ALL_NATIVE_PLATFORMS.filter((p) => opts.platforms.includes(p)) : NATIVE_PLATFORMS;
217008
+ entry.shaders = [
217009
+ { platform: "opengl", src: artifact("opengl") },
217010
+ ...staged.filter((p) => p !== "opengl").map((platform) => ({ platform, src: artifact(platform) }))
217011
+ ];
216558
217012
  entry.shadersLocal = true;
216559
- for (const platform of NATIVE_PLATFORMS) {
217013
+ for (const platform of staged) {
216560
217014
  artifacts.push({ name: artifact(platform), absPath: join13(cacheDir, artifact(platform)) });
216561
217015
  }
216562
217016
  } else {
@@ -216576,7 +217030,7 @@ init_manifest();
216576
217030
  init_ignore();
216577
217031
  init_util();
216578
217032
  import { execFileSync } from "node:child_process";
216579
- import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "node:fs";
217033
+ import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "node:fs";
216580
217034
  import { homedir as homedir4 } from "node:os";
216581
217035
  import { dirname as dirname8, join as join15, resolve as resolve2 } from "node:path";
216582
217036
  var FIRST_PARTY_PLUGINS = {
@@ -216587,6 +217041,22 @@ var FIRST_PARTY_PLUGINS = {
216587
217041
  };
216588
217042
  var readAppConfigAt = (root) => readJson(join15(root, "app.json"));
216589
217043
  var DESKTOP_DEFAULT_WINDOW = { width: 1280, height: 720 };
217044
+ var resolveDesktopRenderer = (root, args) => {
217045
+ const raw = (args ? flagStr(args, "renderer") : undefined) ?? process.env.LECODES_DESKTOP_RENDERER ?? readAppConfigAt(root)?.desktop?.renderer;
217046
+ if (raw === undefined)
217047
+ return "gl";
217048
+ const value = String(raw).toLowerCase();
217049
+ if (value === "gl" || value === "opengl")
217050
+ return "gl";
217051
+ if (value === "vulkan" || value === "vk") {
217052
+ if (process.platform === "darwin") {
217053
+ note('desktop.renderer "vulkan" is Windows-only — the macOS host renders with Metal.');
217054
+ return "gl";
217055
+ }
217056
+ return "vulkan";
217057
+ }
217058
+ throw new CliError(`Unknown desktop renderer "${raw}". Use "gl" or "vulkan".`);
217059
+ };
216590
217060
  var desktopDefaultViewport = (root) => {
216591
217061
  const cfg = readAppConfigAt(root);
216592
217062
  if (!cfg?.desktop || cfg.ios || cfg.android)
@@ -216600,7 +217070,7 @@ var readJson = (path) => {
216600
217070
  if (!existsSync13(path))
216601
217071
  return null;
216602
217072
  try {
216603
- return JSON.parse(readFileSync12(path, "utf8").replace(/^\uFEFF/, ""));
217073
+ return JSON.parse(readFileSync13(path, "utf8").replace(/^\uFEFF/, ""));
216604
217074
  } catch {
216605
217075
  throw new CliError(`Invalid JSON in ${path}`);
216606
217076
  }
@@ -216631,7 +217101,7 @@ ${tail}`);
216631
217101
  };
216632
217102
  var ensureShellIgnored = (root, dir) => {
216633
217103
  const ignorePath = join15(root, ".lecodesignore");
216634
- const ignore = existsSync13(ignorePath) ? readFileSync12(ignorePath, "utf8") : DEFAULT_IGNORE_FILE;
217104
+ const ignore = existsSync13(ignorePath) ? readFileSync13(ignorePath, "utf8") : DEFAULT_IGNORE_FILE;
216635
217105
  if (new RegExp(`^${dir}\\/$`, "m").test(ignore))
216636
217106
  return;
216637
217107
  const comment = ignore.includes("# Native app shells") ? "" : "# Native app shells — generated by `lecodes app init`, never pushed to LeCodes.\n";
@@ -216654,6 +217124,14 @@ var detectEngines = (bundleJs) => {
216654
217124
  `);
216655
217125
  return { gl: !/^\/\/ gl: disable$/m.test(header), g2: /^\/\/ 2d: enable$/m.test(header) };
216656
217126
  };
217127
+ var assertRendererSupportsBundle = (renderer, bundleJs) => {
217128
+ if (renderer !== "vulkan")
217129
+ return;
217130
+ if (!detectEngines(bundleJs).g2)
217131
+ return;
217132
+ throw new CliError("This project uses the 2D engine, which the Vulkan desktop host doesn't include " + `(creator-2d renders through OpenGL and has no Vulkan path yet).
217133
+ ` + " Build with the default renderer: drop `desktop.renderer` from app.json, or pass `--renderer gl`.");
217134
+ };
216657
217135
  var resolveVariant = (requested, bundleJs, configKey = "ios.variant") => {
216658
217136
  const uses = detectEngines(bundleJs);
216659
217137
  const detected = uses.gl && uses.g2 ? "full" : uses.gl ? "3d" : uses.g2 ? "2d" : "core";
@@ -216866,7 +217344,8 @@ var buildBundle = async (args, shell, opts = {}) => {
216866
217344
  }
216867
217345
  let stagedShaders = [];
216868
217346
  if (localAssets && (opts.localShaders ?? !shell)) {
216869
- const local = await applyLocalShaders(entries, { root, local: true });
217347
+ const platforms = opts.desktopRenderer ? [desktopShaderPlatform(process.platform, opts.desktopRenderer)] : undefined;
217348
+ const local = await applyLocalShaders(entries, { root, local: true, platforms });
216870
217349
  warnings.push(...local.warnings);
216871
217350
  stagedShaders = local.artifacts;
216872
217351
  }
@@ -216878,9 +217357,11 @@ var buildBundle = async (args, shell, opts = {}) => {
216878
217357
  publicUrl: shell ? "" : publicUrl,
216879
217358
  makeResourceEntry: (path, absPath) => ({ path, type: "resource", fileSrc: encodeURI(path), absPath })
216880
217359
  });
217360
+ if (opts.extraEntries)
217361
+ entries.push(...opts.extraEntries);
216881
217362
  const iconsHeader = await applyAppAssetIcons(entries);
216882
217363
  note("Compiling…");
216883
- const entryFlag = flagStr(args, "entry");
217364
+ const entryFlag = opts.entryOverride ?? flagStr(args, "entry");
216884
217365
  if (!entryFlag) {
216885
217366
  const ambiguous = ambiguousEntryWarning(entries);
216886
217367
  if (ambiguous)
@@ -217760,14 +218241,14 @@ buildNumber) — edit it there, then re-run \`lecodes app sync\`. Plugins are ma
217760
218241
  init_manifest();
217761
218242
  import { execFileSync as execFileSync2, spawn, spawnSync as spawnSync3 } from "node:child_process";
217762
218243
  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";
218244
+ import { chmodSync, cpSync, existsSync as existsSync16, mkdirSync as mkdirSync10, readFileSync as readFileSync15, readdirSync as readdirSync5, renameSync as renameSync2, rmSync as rmSync3, statSync as statSync3, writeFileSync as writeFileSync12 } from "node:fs";
217764
218245
  import { homedir as homedir5 } from "node:os";
217765
218246
  import { basename as basename5, dirname as dirname10, join as join17 } from "node:path";
217766
218247
  init_util();
217767
218248
 
217768
218249
  // src/commands/appIcon.ts
217769
218250
  init_util();
217770
- import { existsSync as existsSync15, mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync11 } from "node:fs";
218251
+ import { existsSync as existsSync15, mkdirSync as mkdirSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync11 } from "node:fs";
217771
218252
  import { dirname as dirname9, join as join16, resolve as resolve4 } from "node:path";
217772
218253
  var MASTER_PX = 1024;
217773
218254
  var DENSITIES = [
@@ -217897,7 +218378,7 @@ var restampManifestIcon = (androidDir) => {
217897
218378
  const path2 = join16(androidDir, "app", "src", "main", "AndroidManifest.xml");
217898
218379
  if (!existsSync15(path2))
217899
218380
  return;
217900
- const content = readFileSync13(path2, "utf8");
218381
+ const content = readFileSync14(path2, "utf8");
217901
218382
  if (content.includes(`android:icon="@mipmap/ic_launcher"`))
217902
218383
  return;
217903
218384
  if (!/android:icon="[^"]*"/.test(content)) {
@@ -218817,7 +219298,7 @@ var shellSdkVersionAndroid = (androidDir) => {
218817
219298
  const path2 = join17(androidDir, "lecodes-runtime", "build.gradle.kts");
218818
219299
  if (!existsSync16(path2))
218819
219300
  return null;
218820
- const m = /io\.letary:lecodes-sdk(?:-(?:core|2d|3d))?:(\d+\.\d+\.\d+)@aar/.exec(readFileSync14(path2, "utf8"));
219301
+ const m = /io\.letary:lecodes-sdk(?:-(?:core|2d|3d))?:(\d+\.\d+\.\d+)@aar/.exec(readFileSync15(path2, "utf8"));
218821
219302
  return m ? m[1] : null;
218822
219303
  };
218823
219304
  var GRADLE_WRAPPER_JAR_URL = "https://raw.githubusercontent.com/gradle/gradle/v8.13.0/gradle/wrapper/gradle-wrapper.jar";
@@ -218825,7 +219306,7 @@ var GRADLE_WRAPPER_JAR_SHA256 = "81a82aaea5abcc8ff68b3dfcb58b3c3c429378efd98e743
218825
219306
  var ensureGradleWrapperJar = async () => {
218826
219307
  const cached = join17(homedir5(), ".lecodes", "gradle-wrapper", GRADLE_VERSION, "gradle-wrapper.jar");
218827
219308
  if (existsSync16(cached))
218828
- return new Uint8Array(readFileSync14(cached));
219309
+ return new Uint8Array(readFileSync15(cached));
218829
219310
  try {
218830
219311
  const res = await fetch(GRADLE_WRAPPER_JAR_URL);
218831
219312
  if (!res.ok)
@@ -218952,7 +219433,7 @@ var syncAndroid = async (root, config, prior) => {
218952
219433
  resources = prior.resources;
218953
219434
  } else {
218954
219435
  resources = await compileShell({ _: [], flags: { o: join17(assetsDir, "app.js") } }, { remotePatterns });
218955
- bundleJs = readFileSync14(join17(assetsDir, "app.js"), "utf8");
219436
+ bundleJs = readFileSync15(join17(assetsDir, "app.js"), "utf8");
218956
219437
  }
218957
219438
  const requestedVariant = config.android?.variant ?? "auto";
218958
219439
  if (requestedVariant !== "auto" && !SDK_VARIANTS.includes(requestedVariant)) {
@@ -219089,7 +219570,7 @@ var restampAppBuildGradle = (androidDir, config, gradlePlugins) => {
219089
219570
  warn("android/app/build.gradle.kts is missing — run `lecodes app init android` on a clean tree to regenerate the shell.");
219090
219571
  return;
219091
219572
  }
219092
- let content = readFileSync14(path2, "utf8");
219573
+ let content = readFileSync15(path2, "utf8");
219093
219574
  const before = content;
219094
219575
  const versionName = config.version ?? "1.0.0";
219095
219576
  const versionCode = config.android?.versionCode ?? 1;
@@ -219262,13 +219743,13 @@ var doctorAndroidChecks = async (root, config, check) => {
219262
219743
  };
219263
219744
 
219264
219745
  // src/commands/appDesktop.ts
219265
- import { execFileSync as execFileSync3, spawn as spawn3 } from "node:child_process";
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";
219746
+ import { execFileSync as execFileSync5, spawn as spawn4 } from "node:child_process";
219747
+ import { cpSync as cpSync3, existsSync as existsSync19, mkdirSync as mkdirSync13, readdirSync as readdirSync8, rmSync as rmSync6, statSync as statSync6, writeFileSync as writeFileSync15 } from "node:fs";
219748
+ import { basename as basename7, dirname as dirname11, join as join20, resolve as resolve7 } from "node:path";
219268
219749
 
219269
219750
  // 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";
219751
+ import { execFileSync as execFileSync3, spawn as spawn2, spawnSync as spawnSync4 } from "node:child_process";
219752
+ import { copyFileSync as copyFileSync2, existsSync as existsSync17, mkdirSync as mkdirSync11, mkdtempSync as mkdtempSync2, readdirSync as readdirSync6, readFileSync as readFileSync16, rmSync as rmSync4, statSync as statSync4, writeFileSync as writeFileSync13 } from "node:fs";
219272
219753
  import { homedir as homedir6, tmpdir as tmpdir2 } from "node:os";
219273
219754
  import { join as join18, resolve as resolve5 } from "node:path";
219274
219755
  init_util();
@@ -219373,14 +219854,21 @@ var makeStackDemangler = (bundleJs) => {
219373
219854
 
219374
219855
  // src/desktopRenderer.ts
219375
219856
  var DESKTOP_RENDERER_VERSION = "1.1.0";
219376
- var platformKey2 = () => {
219857
+ var DESKTOP_RENDERER_MIN_VERSION = { gl: "0.0.0", vulkan: "1.4.0" };
219858
+ var platformKey2 = (renderer = "gl") => {
219377
219859
  if (process.platform === "win32")
219378
- return "windows-x64";
219860
+ return renderer === "vulkan" ? "windows-x64-vulkan" : "windows-x64";
219861
+ if (renderer === "vulkan") {
219862
+ throw new CliError(`The Vulkan desktop host is Windows-only (this is ${process.platform}). Use the default renderer.`);
219863
+ }
219379
219864
  if (process.platform === "linux")
219380
219865
  return "ubuntu-linux-x64";
219381
- throw new CliError(process.platform === "darwin" ? "`lecodes render --desktop` isn't available on macOS yet (the macOS host ships separately)." : `\`lecodes render --desktop\` isn't available on ${process.platform}.`);
219866
+ if (process.platform === "darwin")
219867
+ return process.arch === "arm64" ? "macos-arm64" : "macos-x64";
219868
+ throw new CliError(`The LeCodes Desktop host isn't available on ${process.platform}.`);
219382
219869
  };
219383
- var exeName = () => process.platform === "win32" ? "lecodes-desktop.exe" : "lecodes-desktop";
219870
+ var isMac = () => process.platform === "darwin";
219871
+ var exeName = () => process.platform === "win32" ? "lecodes-desktop.exe" : isMac() ? "lecodes-app" : "lecodes-desktop";
219384
219872
  var findExe = (dir) => {
219385
219873
  const name = exeName();
219386
219874
  for (const entry of readdirSync6(dir, { withFileTypes: true })) {
@@ -219406,40 +219894,110 @@ var installedDesktopVersions = () => {
219406
219894
  };
219407
219895
  var selectedDesktopVersion = () => {
219408
219896
  try {
219409
- const saved = JSON.parse(readFileSync15(currentFile(), "utf8")).version;
219897
+ const saved = JSON.parse(readFileSync16(currentFile(), "utf8")).version;
219410
219898
  if (saved && /^\d+\.\d+\.\d+/.test(saved) && semverNewer(DESKTOP_RENDERER_VERSION, saved) > 0)
219411
219899
  return saved;
219412
219900
  } catch {}
219413
219901
  return DESKTOP_RENDERER_VERSION;
219414
219902
  };
219415
219903
  var fetchDesktopManifest = () => fetchReleaseProduct("desktop", "desktop");
219416
- var downloadDesktop = async (version, dest) => {
219417
- const key = platformKey2();
219904
+ var pickMacVersion = (versions, wanted, key = "macos-arm64") => {
219905
+ if (versions.find((v) => v.version === wanted)?.files[key])
219906
+ return wanted;
219907
+ const eligible = versions.filter((v) => v.files[key] && semverNewer(DESKTOP_RENDERER_VERSION, v.version) >= 0).map((v) => v.version).sort(semverNewer);
219908
+ return eligible[0] ?? null;
219909
+ };
219910
+ var extractDmg = (archive, stage) => {
219911
+ const mount = join18(stage, "mnt");
219912
+ mkdirSync11(mount, { recursive: true });
219913
+ execFileSync3("hdiutil", ["attach", "-nobrowse", "-readonly", "-quiet", "-mountpoint", mount, archive]);
219914
+ try {
219915
+ const app = readdirSync6(mount).find((n2) => n2.endsWith(".app"));
219916
+ if (!app)
219917
+ throw new CliError("The downloaded DMG holds no .app bundle.");
219918
+ execFileSync3("cp", ["-R", join18(mount, app), join18(stage, app)]);
219919
+ } finally {
219920
+ spawnSync4("hdiutil", ["detach", "-quiet", "-force", mount]);
219921
+ rmSync4(mount, { recursive: true, force: true });
219922
+ }
219923
+ };
219924
+ var downloadDesktop = async (version, dest, renderer = "gl") => {
219925
+ const key = platformKey2(renderer);
219418
219926
  const entry = (await fetchDesktopManifest()).versions.find((v) => v.version === version);
219419
219927
  const file = entry?.files[key];
219420
219928
  if (!file) {
219929
+ if (renderer === "vulkan") {
219930
+ throw new CliError(`The Vulkan desktop host isn't published for LeCodes Desktop ${version} [${key}].
219931
+ ` + ` Vulkan needs desktop >= ${DESKTOP_RENDERER_MIN_VERSION.vulkan}; run \`lecodes desktop update\`, ` + `set LECODES_DESKTOP_EXE to a locally built host, or drop desktop.renderer from app.json.`);
219932
+ }
219421
219933
  throw new CliError(`LeCodes Desktop ${version} [${key}] isn't published yet — \`--desktop\` needs it for the ` + `headless render contract.
219422
219934
  Set LECODES_DESKTOP_EXE to a locally built host, or update lecodes-cli.`);
219423
219935
  }
219424
- note(`Downloading the native renderer (LeCodes Desktop ${version}, ${(file.size / 1024 / 1024).toFixed(1)} MB)…`);
219936
+ note(`Downloading LeCodes Desktop ${version} [${key}] (${(file.size / 1024 / 1024).toFixed(1)} MB)…`);
219425
219937
  await downloadReleaseArchive(file, {
219426
219938
  cacheRoot: cacheRoot2(),
219427
219939
  dest,
219428
- validate: (dir) => findExe(dir) ? null : "The downloaded archive doesn't contain the desktop host binary."
219940
+ validate: (dir) => findExe(dir) ? null : "The downloaded archive doesn't contain the desktop host binary.",
219941
+ extract: file.url.toLowerCase().endsWith(".dmg") ? extractDmg : undefined
219429
219942
  });
219430
219943
  };
219431
- var resolveDesktopExe = async () => {
219944
+ var findMacHostExe = () => {
219945
+ const candidates = [
219946
+ ...["/Applications", join18(homedir6(), "Applications")].flatMap((root) => ["LeCodes Desktop.app", "LeCodes.app"].map((app) => join18(root, app)))
219947
+ ].map((app) => join18(app, "Contents", "MacOS", "lecodes-app"));
219948
+ return candidates.find((exe) => existsSync17(exe)) ?? null;
219949
+ };
219950
+ var resolveMacExe = async (selected) => {
219951
+ const cached = findExe(join18(cacheRoot2(), `desktop-${selected}`));
219952
+ if (cached)
219953
+ return cached;
219954
+ try {
219955
+ const { versions } = await fetchDesktopManifest();
219956
+ const version = pickMacVersion(versions, selected, platformKey2());
219957
+ if (!version) {
219958
+ throw new CliError(`No LeCodes Desktop release for ${platformKey2()} is published at or above ${DESKTOP_RENDERER_VERSION}.`);
219959
+ }
219960
+ const dir = join18(cacheRoot2(), `desktop-${version}`);
219961
+ let exe = findExe(dir);
219962
+ if (!exe) {
219963
+ await downloadDesktop(version, dir);
219964
+ exe = findExe(dir);
219965
+ if (!exe)
219966
+ throw new CliError(`Renderer cache at ${dir} is broken — delete the folder and retry.`);
219967
+ }
219968
+ if (version !== selected) {
219969
+ note(`LeCodes Desktop ${selected} has no macOS build — using ${version}.`);
219970
+ mkdirSync11(cacheRoot2(), { recursive: true });
219971
+ writeFileSync13(currentFile(), JSON.stringify({ version }, null, 2));
219972
+ }
219973
+ return exe;
219974
+ } catch (e2) {
219975
+ const installed = findMacHostExe();
219976
+ if (installed) {
219977
+ warnErr(`${e2 instanceof Error ? e2.message : String(e2)}
219978
+ Falling back to the installed host: ${installed}`);
219979
+ return installed;
219980
+ }
219981
+ throw new CliError(`${e2 instanceof Error ? e2.message : String(e2)}
219982
+ ` + " No LeCodes Desktop.app is installed either — retry online (`lecodes desktop update`), install the DMG into\n" + " /Applications, or point LECODES_DESKTOP_EXE at a build (…/LeCodes Desktop.app/Contents/MacOS/lecodes-app).");
219983
+ }
219984
+ };
219985
+ var resolveDesktopExe = async (renderer = "gl") => {
219432
219986
  const override = process.env.LECODES_DESKTOP_EXE;
219433
219987
  if (override) {
219434
219988
  if (!existsSync17(override))
219435
219989
  throw new CliError(`LECODES_DESKTOP_EXE points at "${override}" which doesn't exist.`);
219436
219990
  return resolve5(override);
219437
219991
  }
219992
+ if (renderer === "vulkan")
219993
+ platformKey2(renderer);
219438
219994
  const version = selectedDesktopVersion();
219439
- const dir = join18(cacheRoot2(), `desktop-${version}`);
219995
+ if (isMac())
219996
+ return resolveMacExe(version);
219997
+ const dir = join18(cacheRoot2(), `desktop-${version}${renderer === "vulkan" ? "-vulkan" : ""}`);
219440
219998
  let exe = existsSync17(dir) ? findExe(dir) : null;
219441
219999
  if (!exe) {
219442
- await downloadDesktop(version, dir);
220000
+ await downloadDesktop(version, dir, renderer);
219443
220001
  exe = findExe(dir);
219444
220002
  if (!exe)
219445
220003
  throw new CliError(`Renderer cache at ${dir} is broken — delete the folder and retry.`);
@@ -219447,13 +220005,23 @@ var resolveDesktopExe = async () => {
219447
220005
  return exe;
219448
220006
  };
219449
220007
  var updateDesktop = async () => {
219450
- const { latest } = await fetchDesktopManifest();
220008
+ const manifest = await fetchDesktopManifest();
220009
+ const latest = isMac() ? pickMacVersion(manifest.versions, manifest.latest, platformKey2()) : manifest.latest;
220010
+ if (!latest)
220011
+ throw new CliError(`No LeCodes Desktop release for ${platformKey2()} is published at or above ${DESKTOP_RENDERER_VERSION}.`);
219451
220012
  if (semverNewer(latest, DESKTOP_RENDERER_VERSION) > 0) {
219452
220013
  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.`);
219453
220014
  }
219454
220015
  const before = selectedDesktopVersion();
219455
220016
  const dir = join18(cacheRoot2(), `desktop-${latest}`);
219456
- const hadIt = existsSync17(dir) && findExe(dir) !== null;
220017
+ const published = (await fetchDesktopManifest()).versions.find((v) => v.version === latest)?.files[platformKey2()];
220018
+ const cached = existsSync17(dir) && findExe(dir) !== null;
220019
+ const republished = cached && published !== undefined && cachedArtifactSha(dir) !== published.sha256;
220020
+ if (republished) {
220021
+ note(`LeCodes Desktop ${latest} was republished since this machine cached it — downloading the current build.`);
220022
+ rmSync4(dir, { recursive: true, force: true });
220023
+ }
220024
+ const hadIt = cached && !republished;
219457
220025
  if (!hadIt)
219458
220026
  await downloadDesktop(latest, dir);
219459
220027
  mkdirSync11(cacheRoot2(), { recursive: true });
@@ -219488,6 +220056,8 @@ var windowEnv = (opts) => {
219488
220056
  if (opts.maxRenderSize && opts.maxRenderSize[0] > 0 && opts.maxRenderSize[1] > 0) {
219489
220057
  env.CREATOR_RENDER_MAX = `${Math.round(opts.maxRenderSize[0])}x${Math.round(opts.maxRenderSize[1])}`;
219490
220058
  }
220059
+ if (opts.fps)
220060
+ env.CREATOR_FPS = "1";
219491
220061
  return env;
219492
220062
  };
219493
220063
  var stageBundle = (js, resources) => {
@@ -219517,7 +220087,7 @@ var pipeThroughDemangler = (src, dst, map) => {
219517
220087
  });
219518
220088
  };
219519
220089
  var runDesktopApp = async (js, resources, opts = {}) => {
219520
- const exe = await resolveDesktopExe();
220090
+ const exe = await resolveDesktopExe(opts.renderer);
219521
220091
  const runDir = stageBundle(js, resources);
219522
220092
  try {
219523
220093
  const env = { ...process.env, ...windowEnv(opts) };
@@ -219541,7 +220111,7 @@ var runDesktopApp = async (js, resources, opts = {}) => {
219541
220111
  }
219542
220112
  };
219543
220113
  var runDesktopDev = async (url, opts = {}) => {
219544
- const exe = await resolveDesktopExe();
220114
+ const exe = await resolveDesktopExe(opts.renderer);
219545
220115
  const env = { ...process.env, ...windowEnv(opts) };
219546
220116
  await new Promise((done, fail) => {
219547
220117
  const child = spawn2(exe, [url], { env, stdio: ["ignore", "ignore", "inherit"] });
@@ -219550,7 +220120,10 @@ var runDesktopDev = async (url, opts = {}) => {
219550
220120
  });
219551
220121
  };
219552
220122
  var runDesktopRender = async (js, resources, opts) => {
219553
- const exe = await resolveDesktopExe();
220123
+ if (isMac()) {
220124
+ throw new CliError("`lecodes render --desktop` isn't available on macOS yet — the macOS host has no headless mode " + "(CREATOR_HIDDEN / CREATOR_DUMP_PNG). `lecodes desktop run` and `lecodes desktop build` do work there.");
220125
+ }
220126
+ const exe = await resolveDesktopExe(opts.renderer);
219554
220127
  const runDir = stageBundle(js, resources);
219555
220128
  try {
219556
220129
  const appJs = join18(runDir, "app.js");
@@ -219577,11 +220150,16 @@ var runDesktopRender = async (js, resources, opts) => {
219577
220150
  return await new Promise((done, fail) => {
219578
220151
  const child = spawn2(exe, [appJs], { cwd: runDir, env, stdio: ["ignore", "pipe", "pipe"] });
219579
220152
  const chunks = [];
220153
+ let stopped = false;
219580
220154
  const sink = (d) => {
219581
220155
  const text = String(d);
219582
220156
  chunks.push(text);
219583
220157
  if (opts.logs)
219584
220158
  process.stderr.write(demangle(text));
220159
+ if (!stopped && opts.stopOnLine?.(text)) {
220160
+ stopped = true;
220161
+ child.kill();
220162
+ }
219585
220163
  };
219586
220164
  child.stdout.on("data", sink);
219587
220165
  child.stderr.on("data", sink);
@@ -219596,6 +220174,8 @@ var runDesktopRender = async (js, resources, opts) => {
219596
220174
  child.on("exit", (code) => {
219597
220175
  clearTimeout(killer);
219598
220176
  const output = demangle(chunks.join(""));
220177
+ if (stopped)
220178
+ return done({ output, exitCode: 0 });
219599
220179
  if (code === 0 && existsSync17(outAbs))
219600
220180
  return done({ output, exitCode: code });
219601
220181
  const tail = demangle(chunks.slice(-80).join("")).trimEnd();
@@ -219611,15 +220191,333 @@ ${tail}` : "";
219611
220191
 
219612
220192
  // src/commands/appDesktop.ts
219613
220193
  init_util();
220194
+
220195
+ // src/commands/appDesktopMac.ts
220196
+ init_util();
220197
+ import { execFileSync as execFileSync4, spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
220198
+ import { cpSync as cpSync2, existsSync as existsSync18, mkdirSync as mkdirSync12, mkdtempSync as mkdtempSync3, readdirSync as readdirSync7, rmSync as rmSync5, statSync as statSync5, writeFileSync as writeFileSync14 } from "node:fs";
220199
+ import { tmpdir as tmpdir3 } from "node:os";
220200
+ import { basename as basename6, join as join19, resolve as resolve6 } from "node:path";
219614
220201
  var mb = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)} MB`;
219615
220202
  var dirSize = (dir) => readdirSync7(dir, { withFileTypes: true }).reduce((sum, e2) => {
219616
220203
  const p2 = join19(dir, e2.name);
219617
- return sum + (e2.isDirectory() ? dirSize(p2) : statSync5(p2).size);
220204
+ return sum + (e2.isDirectory() ? dirSize(p2) : e2.isSymbolicLink() ? 0 : statSync5(p2).size);
220205
+ }, 0);
220206
+ var cleanBundleName = (name) => name.replace(/[/:]/g, "").trim() || "App";
220207
+ var macHostAppOf = (exe) => {
220208
+ const app = resolve6(exe, "..", "..", "..");
220209
+ if (!app.endsWith(".app") || !existsSync18(join19(app, "Contents", "Info.plist"))) {
220210
+ throw new CliError(`The macOS host must be an .app bundle (got ${exe}). Install LeCodes Desktop.app, or point ` + `LECODES_DESKTOP_EXE at <Build>.app/Contents/MacOS/lecodes-app — a bare SwiftPM binary has no ` + `resource bundles and can't be re-signed into an app.`);
220211
+ }
220212
+ return app;
220213
+ };
220214
+ var macHostProvidedNames = (exe) => {
220215
+ const names3 = new Set;
220216
+ const walk = (dir) => {
220217
+ for (const e2 of readdirSync7(dir, { withFileTypes: true })) {
220218
+ if (e2.isDirectory())
220219
+ walk(join19(dir, e2.name));
220220
+ else
220221
+ names3.add(e2.name);
220222
+ }
220223
+ };
220224
+ const res = join19(macHostAppOf(exe), "Contents", "Resources");
220225
+ if (existsSync18(res))
220226
+ walk(res);
220227
+ return names3;
220228
+ };
220229
+ var plistRead = (plist, key) => {
220230
+ const r2 = spawnSync5("/usr/libexec/PlistBuddy", ["-c", `Print :${key}`, plist], { encoding: "utf8" });
220231
+ return r2.status === 0 ? r2.stdout.trim() : null;
220232
+ };
220233
+ var plistSet = (plist, key, value) => {
220234
+ const quoted = `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
220235
+ const set = spawnSync5("/usr/libexec/PlistBuddy", ["-c", `Set :${key} ${quoted}`, plist], { stdio: "ignore" });
220236
+ if (set.status !== 0)
220237
+ execFileSync4("/usr/libexec/PlistBuddy", ["-c", `Add :${key} string ${quoted}`, plist], { stdio: "ignore" });
220238
+ };
220239
+ var macHostVersion = (exe) => {
220240
+ const plist = join19(macHostAppOf(exe), "Contents", "Info.plist");
220241
+ return plistRead(plist, "LeCodesHostVersion") ?? plistRead(plist, "CFBundleShortVersionString") ?? "unknown";
220242
+ };
220243
+ var macHostFeatures = (exe) => {
220244
+ const raw = plistRead(join19(macHostAppOf(exe), "Contents", "Info.plist"), "LeCodesHostFeatures") ?? "";
220245
+ return new Set(raw.split(`
220246
+ `).map((l2) => l2.trim()).filter((l2) => l2 && l2 !== "Array {" && l2 !== "}"));
220247
+ };
220248
+ var assertMacHostCanBundle = (exe) => {
220249
+ if (macHostFeatures(exe).has("project-bundle"))
220250
+ return;
220251
+ throw new CliError(`This LeCodes Desktop host (${macHostAppOf(exe)}) predates project bundles — an app built from it would ` + `open the host's home screen instead of the project.
220252
+ ` + " Run `lecodes desktop update` (needs a published macOS host with project-bundle support), or point " + "LECODES_DESKTOP_EXE at a current build (…/LeCodes Desktop.app/Contents/MacOS/lecodes-app).");
220253
+ };
220254
+ var developerIdInKeychain = () => {
220255
+ const r2 = spawnSync5("security", ["find-identity", "-v", "-p", "codesigning"], { encoding: "utf8" });
220256
+ if (r2.status !== 0)
220257
+ return null;
220258
+ return /"(Developer ID Application: [^"]+)"/.exec(r2.stdout)?.[1] ?? null;
220259
+ };
220260
+ var resolveMacSigning = (cfg, overrides, keychainDeveloperId) => {
220261
+ let identity = overrides.sign ?? overrides.env.LECODES_MACOS_SIGN_ID ?? cfg?.signing?.identity;
220262
+ if (identity === undefined || identity === "" || identity === "auto")
220263
+ identity = keychainDeveloperId() ?? "-";
220264
+ if (identity === "adhoc" || identity === "ad-hoc")
220265
+ identity = "-";
220266
+ const notaryProfile = overrides.noNotarize ? null : overrides.notarize ?? overrides.env.LECODES_NOTARY_PROFILE ?? cfg?.notarize?.profile ?? null;
220267
+ if (notaryProfile && identity === "-") {
220268
+ throw new CliError(`Notarization needs a Developer ID signature — an ad-hoc-signed app can't be notarized.
220269
+ ` + ' Set desktop.macos.signing.identity (or --sign) to a "Developer ID Application: …" certificate, ' + "or drop the notarize profile (--no-notarize).");
220270
+ }
220271
+ return { identity, notaryProfile: notaryProfile || null };
220272
+ };
220273
+ var FALLBACK_ENTITLEMENTS = `<?xml version="1.0" encoding="UTF-8"?>
220274
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
220275
+ <plist version="1.0">
220276
+ <dict>
220277
+ <key>com.apple.security.cs.allow-jit</key>
220278
+ <true/>
220279
+ </dict>
220280
+ </plist>
220281
+ `;
220282
+ var entitlementsFor = (hostApp) => {
220283
+ const r2 = spawnSync5("codesign", ["-d", "--entitlements", "-", "--xml", hostApp], { encoding: "utf8" });
220284
+ const xml = r2.status === 0 ? r2.stdout.trim() : "";
220285
+ if (xml.startsWith("<?xml") && xml.includes("com.apple.security.cs.allow-jit"))
220286
+ return xml + `
220287
+ `;
220288
+ return FALLBACK_ENTITLEMENTS;
220289
+ };
220290
+ var codesignRetrying = async (args) => {
220291
+ for (let attempt = 1;; attempt++) {
220292
+ const r2 = spawnSync5("codesign", args, { encoding: "utf8" });
220293
+ if (r2.status === 0)
220294
+ return;
220295
+ const out = (r2.stdout + `
220296
+ ` + r2.stderr).trim();
220297
+ if (!/timestamp/i.test(out) || attempt >= 5)
220298
+ throw new CliError(`codesign failed:
220299
+ ${out}`);
220300
+ const delay = attempt * 15;
220301
+ warn(`timestamp service unavailable (attempt ${attempt}/5) — retrying in ${delay}s…`);
220302
+ await new Promise((r3) => setTimeout(r3, delay * 1000));
220303
+ }
220304
+ };
220305
+ var signApp = async (app, signing, hostApp, tmp) => {
220306
+ if (signing.identity === "-") {
220307
+ note("Signing ad-hoc (runs on this Mac only — pass a Developer ID identity to distribute)…");
220308
+ await codesignRetrying(["--force", "--sign", "-", app]);
220309
+ return;
220310
+ }
220311
+ note(`Signing with "${signing.identity}" (Hardened Runtime + JIT entitlement)…`);
220312
+ const ent = join19(tmp, "entitlements.plist");
220313
+ writeFileSync14(ent, entitlementsFor(hostApp));
220314
+ await codesignRetrying(["--force", "--options", "runtime", "--timestamp", "--entitlements", ent, "--sign", signing.identity, app]);
220315
+ const verify = spawnSync5("codesign", ["--verify", "--strict", "--verbose=2", app], { encoding: "utf8" });
220316
+ if (verify.status !== 0)
220317
+ throw new CliError(`The signature doesn't verify:
220318
+ ${(verify.stdout + verify.stderr).trim()}`);
220319
+ };
220320
+ var notarizeApp = (app, profile, tmp) => {
220321
+ const zip = join19(tmp, `${basename6(app, ".app")}.zip`);
220322
+ execFileSync4("ditto", ["-c", "-k", "--keepParent", app, zip]);
220323
+ note(`Notarizing with profile "${profile}" — this waits on Apple (usually 1–5 minutes)…`);
220324
+ const r2 = spawnSync5("xcrun", ["notarytool", "submit", zip, "--keychain-profile", profile, "--wait"], { encoding: "utf8" });
220325
+ const out = (r2.stdout + `
220326
+ ` + r2.stderr).trim();
220327
+ const id = /id: ([0-9a-f-]{36})/.exec(out)?.[1];
220328
+ if (r2.status !== 0 || !/status: Accepted/.test(out)) {
220329
+ throw new CliError(`Notarization failed:
220330
+ ${out}
220331
+ ` + (id ? ` Details: xcrun notarytool log ${id} --keychain-profile ${profile}` : ""));
220332
+ }
220333
+ const staple = spawnSync5("xcrun", ["stapler", "staple", app], { encoding: "utf8" });
220334
+ if (staple.status !== 0)
220335
+ throw new CliError(`Stapling failed:
220336
+ ${(staple.stdout + staple.stderr).trim()}`);
220337
+ success("Notarized + stapled.");
220338
+ };
220339
+ var gatekeeperVerdict = (app) => {
220340
+ const r2 = spawnSync5("spctl", ["-a", "-t", "exec", "-vv", app], { encoding: "utf8" });
220341
+ const line = (r2.stdout + r2.stderr).trim().split(`
220342
+ `).filter((l2) => l2.includes("source=") || l2.includes("rejected") || l2.includes("accepted")).join("; ");
220343
+ return { accepted: r2.status === 0, line: line || (r2.stdout + r2.stderr).trim() };
220344
+ };
220345
+ var writeIcns = async (root, config, dest, tmp) => {
220346
+ const master = await loadIconMaster(root, config);
220347
+ const { Jimp: Jimp2 } = await Promise.resolve().then(() => (init_esm30(), exports_esm2));
220348
+ const SIZE = 1024, INSET = 100, RADIUS = 185;
220349
+ const tileSize = SIZE - 2 * INSET;
220350
+ const tile = new Jimp2({ width: tileSize, height: tileSize, color: master.background });
220351
+ const art = master.image.clone();
220352
+ art.resize({ w: tileSize, h: tileSize });
220353
+ tile.composite(art, 0, 0);
220354
+ const { data } = tile.bitmap;
220355
+ for (let y2 = 0;y2 < tileSize; y2++) {
220356
+ for (let x2 = 0;x2 < tileSize; x2++) {
220357
+ const cx = x2 < RADIUS ? RADIUS : x2 >= tileSize - RADIUS ? tileSize - RADIUS - 1 : x2;
220358
+ const cy = y2 < RADIUS ? RADIUS : y2 >= tileSize - RADIUS ? tileSize - RADIUS - 1 : y2;
220359
+ const dx = x2 - cx, dy = y2 - cy;
220360
+ if (dx * dx + dy * dy > RADIUS * RADIUS)
220361
+ data[(y2 * tileSize + x2) * 4 + 3] = 0;
220362
+ }
220363
+ }
220364
+ const canvas = new Jimp2({ width: SIZE, height: SIZE, color: 0 });
220365
+ canvas.composite(tile, INSET, INSET);
220366
+ const iconset = join19(tmp, "AppIcon.iconset");
220367
+ mkdirSync12(iconset, { recursive: true });
220368
+ for (const pt of [16, 32, 128, 256, 512]) {
220369
+ for (const scale of [1, 2]) {
220370
+ const px = pt * scale;
220371
+ const frame = canvas.clone();
220372
+ frame.resize({ w: px, h: px });
220373
+ writeFileSync14(join19(iconset, `icon_${pt}x${pt}${scale === 2 ? "@2x" : ""}.png`), await frame.getBuffer("image/png"));
220374
+ }
220375
+ }
220376
+ execFileSync4("iconutil", ["-c", "icns", "-o", dest, iconset]);
220377
+ };
220378
+ var probeMacApp = async (app, out, args) => {
220379
+ const after = Number(flagStr(args, "after")) || 8;
220380
+ const shot = join19(out, `${basename6(app, ".app")}-probe.png`);
220381
+ rmSync5(shot, { force: true });
220382
+ note(`Probe: launching the built app for ${after + 2}s (the window will show)…`);
220383
+ await new Promise((done, fail) => {
220384
+ const child = spawn3(join19(app, "Contents", "MacOS", "lecodes-app"), [], {
220385
+ cwd: "/",
220386
+ env: { ...process.env, LECODES_SNAPSHOT: shot, LECODES_SNAPSHOT_AFTER: String(after), LECODES_EXIT_AFTER: String(after + 2) },
220387
+ stdio: ["ignore", "pipe", "pipe"]
220388
+ });
220389
+ const tail = [];
220390
+ const sink = (d) => {
220391
+ tail.push(String(d));
220392
+ while (tail.length > 60)
220393
+ tail.shift();
220394
+ };
220395
+ child.stdout.on("data", sink);
220396
+ child.stderr.on("data", sink);
220397
+ const killer = setTimeout(() => {
220398
+ child.kill();
220399
+ fail(new CliError("The probe timed out after 120s."));
220400
+ }, 120000);
220401
+ child.on("error", (e2) => {
220402
+ clearTimeout(killer);
220403
+ fail(new CliError(`Probe couldn't run: ${e2.message}`));
220404
+ });
220405
+ child.on("exit", (code) => {
220406
+ clearTimeout(killer);
220407
+ const detail = tail.length > 0 ? `
220408
+ ${tail.join("").trimEnd()}` : "";
220409
+ if (code === 0 && existsSync18(shot)) {
220410
+ success(`Probe: ${shot}`);
220411
+ done();
220412
+ } else
220413
+ fail(new CliError(`Probe failed (exit ${code}).${detail}`));
220414
+ });
220415
+ });
220416
+ };
220417
+ var buildMacApp = async (input) => {
220418
+ const { root, config, name, slug, out, js, shipped, hostExe, args } = input;
220419
+ const hostApp = macHostAppOf(hostExe);
220420
+ assertMacHostCanBundle(hostExe);
220421
+ const mac = config.desktop?.macos ?? {};
220422
+ const appName = cleanBundleName(name);
220423
+ const app = join19(out, `${appName}.app`);
220424
+ const tmp = mkdtempSync3(join19(tmpdir3(), "lecodes-macapp-"));
220425
+ try {
220426
+ note(` host: LeCodes Desktop ${macHostVersion(hostExe)} (${hostApp})`);
220427
+ rmSync5(out, { recursive: true, force: true });
220428
+ mkdirSync12(out, { recursive: true });
220429
+ execFileSync4("cp", ["-R", hostApp, app]);
220430
+ execFileSync4("xattr", ["-cr", app]);
220431
+ rmSync5(join19(app, "Contents", "_CodeSignature"), { recursive: true, force: true });
220432
+ rmSync5(join19(app, "Contents", "CodeResources"), { force: true });
220433
+ const proj = join19(app, "Contents", "Resources", "project");
220434
+ mkdirSync12(join19(proj, "assets"), { recursive: true });
220435
+ writeFileSync14(join19(proj, "app.js"), js);
220436
+ let assetBytes = 0, assetCount = 0;
220437
+ for (const r2 of shipped) {
220438
+ if (!r2.absPath || !existsSync18(r2.absPath) || !statSync5(r2.absPath).isFile()) {
220439
+ warn(`asset "${r2.name}" (${r2.path}) has no local file — skipped`);
220440
+ continue;
220441
+ }
220442
+ cpSync2(r2.absPath, join19(proj, "assets", r2.name));
220443
+ assetBytes += statSync5(r2.absPath).size;
220444
+ assetCount++;
220445
+ }
220446
+ note(` assets: ${assetCount} files, ${mb(assetBytes)} → Contents/Resources/project/assets/`);
220447
+ writeJson(join19(proj, "app.json"), { name, ...config.desktop ? { desktop: { window: config.desktop.window } } : {} });
220448
+ const plist = join19(app, "Contents", "Info.plist");
220449
+ const bundleId = mac.bundleId ?? `codes.le.app.${slug.toLowerCase().replace(/[^a-z0-9.-]+/g, "-")}`;
220450
+ plistSet(plist, "CFBundleName", appName);
220451
+ plistSet(plist, "CFBundleDisplayName", appName);
220452
+ plistSet(plist, "CFBundleIdentifier", bundleId);
220453
+ plistSet(plist, "CFBundleShortVersionString", config.version ?? "1.0");
220454
+ plistSet(plist, "CFBundleVersion", String(mac.buildNumber ?? 1));
220455
+ if (mac.category)
220456
+ plistSet(plist, "LSApplicationCategoryType", mac.category);
220457
+ if (config.icon) {
220458
+ await writeIcns(root, config, join19(app, "Contents", "Resources", "AppIcon.icns"), tmp);
220459
+ plistSet(plist, "CFBundleIconFile", "AppIcon");
220460
+ note(" icon: AppIcon.icns rendered from " + config.icon);
220461
+ }
220462
+ note(` bundle: ${bundleId} ${config.version ?? "1.0"} (${mac.buildNumber ?? 1})`);
220463
+ const signing = resolveMacSigning(mac, {
220464
+ sign: flagStr(args, "sign"),
220465
+ notarize: flagStr(args, "notarize"),
220466
+ noNotarize: flagBool(args, "no-notarize"),
220467
+ env: process.env
220468
+ }, developerIdInKeychain);
220469
+ await signApp(app, signing, hostApp, tmp);
220470
+ if (signing.notaryProfile) {
220471
+ notarizeApp(app, signing.notaryProfile, tmp);
220472
+ } else if (signing.identity !== "-") {
220473
+ warn("Signed with Developer ID but NOT notarized — Gatekeeper refuses a downloaded copy on every other Mac.");
220474
+ warn(" Set desktop.macos.notarize.profile in app.json (a `xcrun notarytool store-credentials` profile) or pass --notarize <profile>.");
220475
+ }
220476
+ const verdict = gatekeeperVerdict(app);
220477
+ log(`${verdict.accepted ? c.green("gatekeeper") : c.yellow("gatekeeper")} ${verdict.line}`);
220478
+ if (signing.notaryProfile && !verdict.accepted) {
220479
+ throw new CliError("The app was notarized but Gatekeeper still rejects it — see the spctl line above.");
220480
+ }
220481
+ writeFileSync14(join19(out, "README.txt"), readmeText(appName, !!signing.notaryProfile, signing.identity !== "-"));
220482
+ success(`App: ${app} (${mb(dirSize(app))})`);
220483
+ if (flagBool(args, "probe"))
220484
+ await probeMacApp(app, out, args);
220485
+ if (flagBool(args, "zip")) {
220486
+ const zip = join19(out, `${appName}.zip`);
220487
+ rmSync5(zip, { force: true });
220488
+ execFileSync4("ditto", ["-c", "-k", "--keepParent", app, zip]);
220489
+ success(`Zip: ${zip} (${mb(statSync5(zip).size)})`);
220490
+ } else {
220491
+ note("Tip: --zip packs the .app for sending (ditto, structure-preserving).");
220492
+ }
220493
+ } finally {
220494
+ rmSync5(tmp, { recursive: true, force: true });
220495
+ }
220496
+ };
220497
+ var readmeText = (name, notarized, developerId) => `${name}
220498
+ ${"=".repeat(Math.max(name.length, 4))}
220499
+
220500
+ A LeCodes desktop app for macOS (Apple Silicon). Nothing to install: unzip and double-click
220501
+ ${name}.app — or drag it to Applications first.
220502
+
220503
+ ${notarized ? `The app is signed and notarized by Apple: macOS asks once whether to open a downloaded app,
220504
+ then it runs.` : developerId ? `The app is signed but NOT notarized: macOS will refuse to open the downloaded copy. To run it
220505
+ anyway: System Settings › Privacy & Security › "Open Anyway" (after the first refused launch),
220506
+ or remove the quarantine flag in Terminal: xattr -dr com.apple.quarantine "` + name + '.app"' : `The app is not signed for distribution: macOS will refuse to open the downloaded copy. To run it
220507
+ anyway: System Settings › Privacy & Security › "Open Anyway" (after the first refused launch),
220508
+ or remove the quarantine flag in Terminal: xattr -dr com.apple.quarantine "` + name + '.app"'}
220509
+ `;
220510
+
220511
+ // src/commands/appDesktop.ts
220512
+ var mb2 = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)} MB`;
220513
+ var dirSize2 = (dir) => readdirSync8(dir, { withFileTypes: true }).reduce((sum, e2) => {
220514
+ const p2 = join20(dir, e2.name);
220515
+ return sum + (e2.isDirectory() ? dirSize2(p2) : statSync6(p2).size);
219618
220516
  }, 0);
219619
220517
  var initDesktop = async (args) => {
219620
220518
  const root = projectRoot();
219621
- const existing = readJson(join19(root, "app.json")) ?? {};
219622
- const name = flagStr(args, "name") ?? existing.name ?? basename6(root);
220519
+ const existing = readJson(join20(root, "app.json")) ?? {};
220520
+ const name = flagStr(args, "name") ?? existing.name ?? basename7(root);
219623
220521
  const window2 = {
219624
220522
  width: 1280,
219625
220523
  height: 800,
@@ -219629,28 +220527,33 @@ var initDesktop = async (args) => {
219629
220527
  ...flagBool(args, "fullscreen") ? { fullscreen: true } : {}
219630
220528
  };
219631
220529
  const config = { ...existing, name, desktop: { ...existing.desktop, window: window2 } };
219632
- writeJson(join19(root, "app.json"), config);
220530
+ writeJson(join20(root, "app.json"), config);
219633
220531
  log(`${c.green("written")} app.json (desktop.window ${window2.width}×${window2.height}${window2.fullscreen ? ", fullscreen" : ""})`);
219634
220532
  log(`Next: ${c.bold("lecodes dev --desktop")} to develop, ${c.bold("lecodes app build desktop")} to package.`);
219635
220533
  };
219636
220534
  var cleanFileName = (name) => name.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "").replace(/[. ]+$/, "").trim() || "app";
220535
+ var ASSETS_DIR = "assets";
220536
+ var RUNTIME_DIR = "runtime";
219637
220537
  var buildDesktop = async (args) => {
219638
220538
  const root = projectRoot();
219639
- const config = readJson(join19(root, "app.json")) ?? {};
219640
- const name = (config.name ?? basename6(root)).trim() || "app";
220539
+ const config = readJson(join20(root, "app.json")) ?? {};
220540
+ const name = (config.name ?? basename7(root)).trim() || "app";
219641
220541
  const slug = flagStr(args, "slug") ?? cleanFileName(name);
219642
- const out = resolve6(root, flagStr(args, "o", "out") ?? join19(root, "dist", slug));
219643
- if (existsSync18(out)) {
219644
- const entries = readdirSync7(out);
219645
- const looksLikeBuild = entries.length === 0 || entries.includes("app.js");
220542
+ const out = resolve7(root, flagStr(args, "o", "out") ?? join20(root, "dist", slug));
220543
+ if (existsSync19(out)) {
220544
+ const entries = readdirSync8(out);
220545
+ const looksLikeBuild = entries.length === 0 || entries.includes("app.js") || entries.some((e2) => e2.endsWith(".app"));
219646
220546
  if (!looksLikeBuild) {
219647
220547
  throw new CliError(`${out} exists and doesn't look like a previous desktop build — pick another -o or clear it yourself.`);
219648
220548
  }
219649
220549
  }
219650
220550
  if (config.desktop?.entry && flagStr(args, "entry") === undefined)
219651
220551
  args.flags.entry = config.desktop.entry;
219652
- note(`Building "${name}" for desktop…`);
219653
- const { js, resources } = await buildBundle(args, { remotePatterns: config.resources?.remote ?? [] }, { localShaders: true });
220552
+ const renderer = resolveDesktopRenderer(root, args);
220553
+ const mac = process.platform === "darwin";
220554
+ note(`Building "${name}" for ${mac ? "macOS" : "desktop"}${renderer === "vulkan" ? " (Vulkan)" : ""}…`);
220555
+ const { js, resources } = await buildBundle(args, { remotePatterns: config.resources?.remote ?? [] }, { localShaders: true, desktopRenderer: renderer });
220556
+ assertRendererSupportsBundle(renderer, js);
219654
220557
  const wanted = new Set([...js.matchAll(/fetchLocal\("([^"]+)"\)/g)].map((m) => m[1]));
219655
220558
  const shipped = resources.filter((r2) => !r2.remote && (r2.shader || wanted.has(r2.name)));
219656
220559
  const byName = new Map;
@@ -219669,52 +220572,62 @@ var buildDesktop = async (args) => {
219669
220572
  `)).join(`
219670
220573
  `));
219671
220574
  }
219672
- const exe = await resolveDesktopExe();
220575
+ const exe = await resolveDesktopExe(renderer);
219673
220576
  const hostDir = dirname11(exe);
219674
220577
  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 {}
220578
+ const hostProvided = mac ? macHostProvidedNames(exe) : new Set;
220579
+ if (!mac) {
220580
+ try {
220581
+ for (const f3 of readdirSync8(join20(hostDir, "assets")))
220582
+ hostProvided.add(f3);
220583
+ } catch {}
220584
+ }
219680
220585
  const shippedNames = new Set(shipped.map((r2) => r2.name));
219681
220586
  const missing = [...wanted].filter((n2) => !shippedNames.has(n2) && !hostProvided.has(n2) && !IMPLICIT_PRELOADS.has(n2));
219682
220587
  if (missing.length > 0) {
219683
220588
  throw new CliError(`The bundle references assets with no local file: ${missing.join(", ")}`);
219684
220589
  }
220590
+ if (mac)
220591
+ return buildMacApp({ root, config, name, slug, out, js, shipped, hostExe: exe, args });
219685
220592
  const version = process.env.LECODES_DESKTOP_EXE ? "local build" : selectedDesktopVersion();
219686
- rmSync5(out, { recursive: true, force: true });
219687
- mkdirSync12(out, { recursive: true });
219688
- for (const entry of readdirSync7(hostDir, { withFileTypes: true })) {
219689
- const keep = entry.isFile() && entry.name.toLowerCase().endsWith(".dll") || entry.isDirectory() && (entry.name === "assets" || entry.name === "fonts");
219690
- if (keep)
219691
- cpSync2(join19(hostDir, entry.name), join19(out, entry.name), { recursive: true });
219692
- }
219693
- if (!existsSync18(join19(out, "assets", "initial.js"))) {
220593
+ rmSync6(out, { recursive: true, force: true });
220594
+ mkdirSync13(out, { recursive: true });
220595
+ for (const entry of readdirSync8(hostDir, { withFileTypes: true })) {
220596
+ if (entry.isFile() && entry.name.toLowerCase().endsWith(".dll")) {
220597
+ cpSync3(join20(hostDir, entry.name), join20(out, entry.name));
220598
+ } else if (entry.isDirectory() && entry.name === "assets") {
220599
+ cpSync3(join20(hostDir, entry.name), join20(out, RUNTIME_DIR), { recursive: true });
220600
+ } else if (entry.isDirectory() && entry.name === "fonts") {
220601
+ cpSync3(join20(hostDir, entry.name), join20(out, entry.name), { recursive: true });
220602
+ }
220603
+ }
220604
+ if (!existsSync19(join20(out, RUNTIME_DIR, "initial.js"))) {
219694
220605
  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.)`);
219695
220606
  }
219696
220607
  const exeName2 = process.platform === "win32" ? `${cleanFileName(name)}.exe` : cleanFileName(name);
219697
- cpSync2(exe, join19(out, exeName2));
220608
+ cpSync3(exe, join20(out, exeName2));
219698
220609
  note(` host: LeCodes Desktop ${version}`);
219699
- writeFileSync14(join19(out, "app.js"), js);
220610
+ writeFileSync15(join20(out, "app.js"), js);
219700
220611
  let assetBytes = 0, assetCount = 0;
220612
+ if (shipped.length > 0)
220613
+ mkdirSync13(join20(out, ASSETS_DIR), { recursive: true });
219701
220614
  for (const r2 of shipped) {
219702
- if (!r2.absPath || !existsSync18(r2.absPath) || !statSync5(r2.absPath).isFile()) {
220615
+ if (!r2.absPath || !existsSync19(r2.absPath) || !statSync6(r2.absPath).isFile()) {
219703
220616
  warn(`asset "${r2.name}" (${r2.path}) has no local file — skipped`);
219704
220617
  continue;
219705
220618
  }
219706
- cpSync2(r2.absPath, join19(out, r2.name));
219707
- assetBytes += statSync5(r2.absPath).size;
220619
+ cpSync3(r2.absPath, join20(out, ASSETS_DIR, r2.name));
220620
+ assetBytes += statSync6(r2.absPath).size;
219708
220621
  assetCount++;
219709
220622
  }
219710
- note(` assets: ${assetCount} files, ${mb(assetBytes)}`);
219711
- writeJson(join19(out, "app.json"), { name, ...config.desktop ? { desktop: { window: config.desktop.window } } : {} });
220623
+ note(` assets: ${assetCount} files, ${mb2(assetBytes)} → ${ASSETS_DIR}/`);
220624
+ writeJson(join20(out, "app.json"), { name, ...config.desktop ? { desktop: { window: config.desktop.window } } : {} });
219712
220625
  if (config.icon) {
219713
- await writeIco(root, config, join19(out, "app.ico"));
220626
+ await writeIco(root, config, join20(out, "app.ico"));
219714
220627
  note(" app.ico rendered from " + config.icon);
219715
220628
  }
219716
- writeFileSync14(join19(out, "README.txt"), "\uFEFF" + readmeText(name, exeName2, version));
219717
- success(`Portable build: ${out} (${mb(dirSize(out))})`);
220629
+ writeFileSync15(join20(out, "README.txt"), "\uFEFF" + readmeText2(name, exeName2, version));
220630
+ success(`Portable build: ${out} (${mb2(dirSize2(out))})`);
219718
220631
  if (flagBool(args, "probe"))
219719
220632
  await probeBuild(out, exeName2, config, args);
219720
220633
  if (flagBool(args, "zip"))
@@ -219746,15 +220659,15 @@ var writeIco = async (root, config, dest) => {
219746
220659
  header.writeUInt32LE(offset, e2 + 12);
219747
220660
  offset += pngs[i2].length;
219748
220661
  });
219749
- writeFileSync14(dest, Buffer.concat([header, ...pngs]));
220662
+ writeFileSync15(dest, Buffer.concat([header, ...pngs]));
219750
220663
  };
219751
220664
  var probeBuild = async (out, exeName2, config, args) => {
219752
220665
  const win = config.desktop?.window;
219753
220666
  const frames = Number(flagStr(args, "frames")) || 90;
219754
- const shot = join19(dirname11(out), `${basename6(out)}-probe.png`);
220667
+ const shot = join20(dirname11(out), `${basename7(out)}-probe.png`);
219755
220668
  note(`Probe: hidden render of the staged folder (${frames} frames)…`);
219756
220669
  await new Promise((done, fail) => {
219757
- const child = spawn3(join19(out, exeName2), [], {
220670
+ const child = spawn4(join20(out, exeName2), [], {
219758
220671
  cwd: out,
219759
220672
  env: {
219760
220673
  ...process.env,
@@ -219783,7 +220696,7 @@ var probeBuild = async (out, exeName2, config, args) => {
219783
220696
  });
219784
220697
  child.on("exit", (code) => {
219785
220698
  clearTimeout(killer);
219786
- if (code === 0 && existsSync18(shot)) {
220699
+ if (code === 0 && existsSync19(shot)) {
219787
220700
  success(`Probe: ${shot}`);
219788
220701
  done();
219789
220702
  } else {
@@ -219796,11 +220709,11 @@ ${tail.join("").trimEnd()}` : "";
219796
220709
  };
219797
220710
  var zipBuild = (out) => {
219798
220711
  const zip = `${out}.zip`;
219799
- rmSync5(zip, { force: true });
220712
+ rmSync6(zip, { force: true });
219800
220713
  note("Zipping…");
219801
220714
  if (process.platform === "win32") {
219802
220715
  const q = (s2) => s2.replaceAll("'", "''");
219803
- execFileSync3("powershell", ["-NoProfile", "-Command", [
220716
+ execFileSync5("powershell", ["-NoProfile", "-Command", [
219804
220717
  "Add-Type -AssemblyName System.IO.Compression.FileSystem;",
219805
220718
  `$src='${q(out)}'; $base=Split-Path $src -Parent;`,
219806
220719
  `$zip=[System.IO.Compression.ZipFile]::Open('${q(zip)}','Create');`,
@@ -219810,15 +220723,15 @@ var zipBuild = (out) => {
219810
220723
  "$zip.Dispose()"
219811
220724
  ].join(" ")], { stdio: ["ignore", "ignore", "pipe"] });
219812
220725
  } else {
219813
- execFileSync3("zip", ["-rq", zip, basename6(out)], { cwd: dirname11(out) });
220726
+ execFileSync5("zip", ["-rq", zip, basename7(out)], { cwd: dirname11(out) });
219814
220727
  }
219815
- success(`Zip: ${zip} (${mb(statSync5(zip).size)})`);
220728
+ success(`Zip: ${zip} (${mb2(statSync6(zip).size)})`);
219816
220729
  };
219817
- var readmeText = (name, exeName2, version) => `${name}
220730
+ var readmeText2 = (name, exeName2, version) => `${name}
219818
220731
  ${"=".repeat(Math.max(name.length, 4))}
219819
220732
 
219820
220733
  A LeCodes desktop app. Nothing to install: unpack the WHOLE folder anywhere and run ${exeName2}.
219821
- Keep the files together — the app reads its assets from this folder.
220734
+ Keep the files together: assets/ holds the app's own content, runtime/ the engine it needs.
219822
220735
 
219823
220736
  On first run Windows may show a SmartScreen prompt (any unsigned app gets one):
219824
220737
  choose "More info", then "Run anyway".
@@ -219833,10 +220746,10 @@ var SDK_REPO = "letary/lecodes-ios-sdk";
219833
220746
  var SDK_MIN_VERSION = "1.2.0";
219834
220747
  var SDK_IDENTITY_VERSION = "1.6.0";
219835
220748
  var shellSdkVersion = (iosDir) => {
219836
- const path2 = join20(iosDir, "LeCodesRuntime", "Package.swift");
219837
- if (!existsSync19(path2))
220749
+ const path2 = join21(iosDir, "LeCodesRuntime", "Package.swift");
220750
+ if (!existsSync20(path2))
219838
220751
  return null;
219839
- const m = /lecodes-ios-sdk\.git",\s*(?:exact|from):\s*"([^"]+)"/.exec(readFileSync16(path2, "utf8"));
220752
+ const m = /lecodes-ios-sdk\.git",\s*(?:exact|from):\s*"([^"]+)"/.exec(readFileSync17(path2, "utf8"));
219840
220753
  return m && VERSION_RE.test(m[1]) ? m[1] : null;
219841
220754
  };
219842
220755
  var IOS_SDK_OPTS = { repo: SDK_REPO, minVersion: SDK_MIN_VERSION, configKey: "ios.sdk" };
@@ -219850,11 +220763,11 @@ var appInit = async (args) => {
219850
220763
  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>]");
219851
220764
  }
219852
220765
  const root = projectRoot();
219853
- const iosDir = join20(root, "ios");
219854
- if (existsSync19(iosDir)) {
220766
+ const iosDir = join21(root, "ios");
220767
+ if (existsSync20(iosDir)) {
219855
220768
  throw new CliError("ios/ already exists — run `lecodes app sync` to update it.");
219856
220769
  }
219857
- const existing = readJson(join20(root, "app.json")) ?? {};
220770
+ const existing = readJson(join21(root, "app.json")) ?? {};
219858
220771
  const variant = flagStr(args, "variant") ?? existing.ios?.variant ?? "auto";
219859
220772
  if (variant !== "auto" && !SDK_VARIANTS.includes(variant)) {
219860
220773
  throw new CliError(`--variant must be auto, ${SDK_VARIANTS.join(", ")} (got "${variant}")`);
@@ -219862,7 +220775,7 @@ var appInit = async (args) => {
219862
220775
  const sdkFlag = flagStr(args, "sdk") ?? existing.ios?.sdk;
219863
220776
  const sdkVersion = await resolveSdkVersion(process.env.LECODES_IOS_SDK ?? sdkFlag ?? "latest", null, IOS_SDK_OPTS);
219864
220777
  const sdk = sdkFlag === "latest" ? "latest" : sdkVersion;
219865
- const name = flagStr(args, "name") ?? existing.name ?? basename7(root);
220778
+ const name = flagStr(args, "name") ?? existing.name ?? basename8(root);
219866
220779
  const config = {
219867
220780
  ...existing,
219868
220781
  name,
@@ -219876,7 +220789,7 @@ var appInit = async (args) => {
219876
220789
  buildNumber: existing.ios?.buildNumber ?? 1
219877
220790
  }
219878
220791
  };
219879
- writeJson(join20(root, "app.json"), config);
220792
+ writeJson(join21(root, "app.json"), config);
219880
220793
  log(`${c.green("written")} app.json`);
219881
220794
  ensureShellIgnored(root, "ios");
219882
220795
  const identity = {
@@ -219887,9 +220800,9 @@ var appInit = async (args) => {
219887
220800
  buildNumber: config.ios.buildNumber
219888
220801
  };
219889
220802
  const write = (rel, content) => {
219890
- const abs = join20(iosDir, rel);
219891
- mkdirSync13(dirname12(abs), { recursive: true });
219892
- writeFileSync15(abs, content);
220803
+ const abs = join21(iosDir, rel);
220804
+ mkdirSync14(dirname12(abs), { recursive: true });
220805
+ writeFileSync16(abs, content);
219893
220806
  log(`${c.green("created")} ios/${rel}`);
219894
220807
  };
219895
220808
  write("README.md", IOS_README);
@@ -219917,23 +220830,23 @@ var ensureSdkResourcesBundle = async (sdkVersion) => {
219917
220830
  const override = process.env.LECODES_SDK_RESOURCES;
219918
220831
  if (override) {
219919
220832
  note(`Using local resources bundle ${override} (LECODES_SDK_RESOURCES)`);
219920
- return resolve7(override);
220833
+ return resolve8(override);
219921
220834
  }
219922
- const dir = join20(homedir7(), ".lecodes", "sdk-resources", sdkVersion);
219923
- const bundle = join20(dir, "LeCodesSDKResources.bundle");
219924
- if (existsSync19(bundle))
220835
+ const dir = join21(homedir7(), ".lecodes", "sdk-resources", sdkVersion);
220836
+ const bundle = join21(dir, "LeCodesSDKResources.bundle");
220837
+ if (existsSync20(bundle))
219925
220838
  return bundle;
219926
220839
  const url = `https://github.com/${SDK_REPO}/releases/download/${sdkVersion}/LeCodesSDKResources.bundle.zip`;
219927
220840
  note(`Downloading LeCodesSDKResources.bundle (SDK ${sdkVersion})…`);
219928
220841
  const res = await fetch(url);
219929
220842
  if (!res.ok)
219930
220843
  throw new CliError(`Resources bundle download failed (HTTP ${res.status}) — ${url}`);
219931
- mkdirSync13(dir, { recursive: true });
219932
- const zip = join20(dir, "LeCodesSDKResources.bundle.zip");
219933
- writeFileSync15(zip, new Uint8Array(await res.arrayBuffer()));
219934
- execFileSync4("ditto", ["-x", "-k", zip, dir]);
219935
- rmSync6(zip);
219936
- if (!existsSync19(bundle))
220844
+ mkdirSync14(dir, { recursive: true });
220845
+ const zip = join21(dir, "LeCodesSDKResources.bundle.zip");
220846
+ writeFileSync16(zip, new Uint8Array(await res.arrayBuffer()));
220847
+ execFileSync6("ditto", ["-x", "-k", zip, dir]);
220848
+ rmSync7(zip);
220849
+ if (!existsSync20(bundle))
219937
220850
  throw new CliError(`No LeCodesSDKResources.bundle inside the release asset ${url}`);
219938
220851
  return bundle;
219939
220852
  };
@@ -219958,13 +220871,13 @@ var resolveUpdateUrl = (update, root) => {
219958
220871
  };
219959
220872
  var appSync = async (args) => {
219960
220873
  const root = projectRoot();
219961
- const config = readJson(join20(root, "app.json"));
220874
+ const config = readJson(join21(root, "app.json"));
219962
220875
  const platform = args._[1];
219963
220876
  if (platform !== undefined && platform !== "ios" && platform !== "android") {
219964
220877
  throw new CliError(`Unknown platform "${platform}" — use ios or android.`);
219965
220878
  }
219966
- const hasIos = !!config?.ios && existsSync19(join20(root, "ios"));
219967
- const hasAndroid = !!config?.android && existsSync19(join20(root, "android"));
220879
+ const hasIos = !!config?.ios && existsSync20(join21(root, "ios"));
220880
+ const hasAndroid = !!config?.android && existsSync20(join21(root, "android"));
219968
220881
  if (platform === "ios" && !hasIos)
219969
220882
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
219970
220883
  if (platform === "android" && !hasAndroid)
@@ -219979,7 +220892,7 @@ var appSync = async (args) => {
219979
220892
  await syncAndroid(root, config, iosArtifacts);
219980
220893
  };
219981
220894
  var syncIos = async (root, config) => {
219982
- const iosDir = join20(root, "ios");
220895
+ const iosDir = join21(root, "ios");
219983
220896
  if (!config.ios)
219984
220897
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
219985
220898
  const requestedVariant = config.ios.variant ?? "auto";
@@ -219989,14 +220902,14 @@ var syncIos = async (root, config) => {
219989
220902
  const linkedSdk = shellSdkVersion(iosDir);
219990
220903
  const sdkPin = process.env.LECODES_IOS_SDK ?? config.ios.sdk ?? linkedSdk ?? "latest";
219991
220904
  const sdkVersion = await resolveSdkVersion(sdkPin, linkedSdk, IOS_SDK_OPTS);
219992
- const lockPath = join20(root, "plugins.lock.json");
220905
+ const lockPath = join21(root, "plugins.lock.json");
219993
220906
  const oldLock = readJson(lockPath)?.plugins ?? {};
219994
220907
  const pluginSpecs = Object.entries(config.plugins ?? {});
219995
220908
  const runtimePlugins = [];
219996
220909
  const infoValues = {};
219997
220910
  const entitlementValues = {};
219998
220911
  const newLock = {};
219999
- const sourcesDir = join20(iosDir, "LeCodesRuntime", "Sources");
220912
+ const sourcesDir = join21(iosDir, "LeCodesRuntime", "Sources");
220000
220913
  for (const [id, spec] of pluginSpecs) {
220001
220914
  const plugin = resolvePlugin(id, spec, oldLock[id], "ios");
220002
220915
  const ios = plugin.manifest.ios;
@@ -220004,12 +220917,12 @@ var syncIos = async (root, config) => {
220004
220917
  const register = ios.register;
220005
220918
  if (!module || !register)
220006
220919
  throw new CliError(`Plugin "${id}": manifest ios.module / ios.register missing`);
220007
- const src = join20(plugin.dir, ios.sources ?? "ios");
220008
- if (!existsSync19(src))
220920
+ const src = join21(plugin.dir, ios.sources ?? "ios");
220921
+ if (!existsSync20(src))
220009
220922
  throw new CliError(`Plugin "${id}": sources dir ${src} missing`);
220010
- const dest = join20(sourcesDir, module);
220011
- rmSync6(dest, { recursive: true, force: true });
220012
- cpSync3(src, dest, { recursive: true });
220923
+ const dest = join21(sourcesDir, module);
220924
+ rmSync7(dest, { recursive: true, force: true });
220925
+ cpSync4(src, dest, { recursive: true });
220013
220926
  if (ios.minSdk && VERSION_RE.test(ios.minSdk) && cmpVersion(sdkVersion, ios.minSdk) < 0) {
220014
220927
  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).`);
220015
220928
  }
@@ -220024,9 +220937,9 @@ var syncIos = async (root, config) => {
220024
220937
  log(`${c.green("plugin")} ${id}@${plugin.lock.version} (${plugin.lock.tag} → Sources/${module})`);
220025
220938
  }
220026
220939
  const keep = new Set(["LeCodesRuntime", ...runtimePlugins.map((p2) => p2.module)]);
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 });
220940
+ for (const entry of existsSync20(sourcesDir) ? readdirSync9(sourcesDir) : []) {
220941
+ if (!keep.has(entry) && statSync7(join21(sourcesDir, entry)).isDirectory()) {
220942
+ rmSync7(join21(sourcesDir, entry), { recursive: true });
220030
220943
  log(`${c.yellow("removed")} ios/LeCodesRuntime/Sources/${entry} (plugin uninstalled)`);
220031
220944
  }
220032
220945
  }
@@ -220034,11 +220947,11 @@ var syncIos = async (root, config) => {
220034
220947
  if (!Array.isArray(remotePatterns) || remotePatterns.some((p2) => typeof p2 !== "string")) {
220035
220948
  throw new CliError('app.json "resources.remote" must be an array of glob strings (e.g. ["videos/**"]).');
220036
220949
  }
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");
220950
+ const resourcesDir = join21(iosDir, "App", "Resources");
220951
+ rmSync7(resourcesDir, { recursive: true, force: true });
220952
+ mkdirSync14(resourcesDir, { recursive: true });
220953
+ const resources = await compileShell({ _: [], flags: { o: join21(resourcesDir, "app.js") } }, { remotePatterns });
220954
+ const bundleJs = readFileSync17(join21(resourcesDir, "app.js"), "utf8");
220042
220955
  const { variant, detected } = resolveVariant(requestedVariant, bundleJs);
220043
220956
  const WHY = {
220044
220957
  core: "UI only",
@@ -220052,8 +220965,8 @@ var syncIos = async (root, config) => {
220052
220965
  const sdkProvided = new Set;
220053
220966
  if (variant === "3d" || variant === "full") {
220054
220967
  const sdkResources = await ensureSdkResourcesBundle(sdkVersion);
220055
- cpSync3(sdkResources, join20(resourcesDir, "LeCodesSDKResources.bundle"), { recursive: true });
220056
- for (const f3 of readdirSync8(sdkResources))
220968
+ cpSync4(sdkResources, join21(resourcesDir, "LeCodesSDKResources.bundle"), { recursive: true });
220969
+ for (const f3 of readdirSync9(sdkResources))
220057
220970
  if (f3 !== "Info.plist")
220058
220971
  sdkProvided.add(f3);
220059
220972
  log(`${c.green("resources")} LeCodesSDKResources.bundle (filament) → ios/App/Resources/`);
@@ -220080,7 +220993,7 @@ var syncIos = async (root, config) => {
220080
220993
  }
220081
220994
  if (candidates.length > 1)
220082
220995
  warn(`asset name collision: ${name} (${candidates.map((r2) => r2.path).join(", ")}) — embedding ${candidates[0].path}`);
220083
- cpSync3(candidates[0].absPath, join20(resourcesDir, name));
220996
+ cpSync4(candidates[0].absPath, join21(resourcesDir, name));
220084
220997
  embedded++;
220085
220998
  }
220086
220999
  if (embedded > 0)
@@ -220100,14 +221013,14 @@ var syncIos = async (root, config) => {
220100
221013
  }
220101
221014
  if (shellUuid)
220102
221015
  log(`${c.green("identity")} project ${shellUuid} baked into the shell (world identity)`);
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));
221016
+ writeFileSync16(join21(iosDir, "README.md"), IOS_README);
221017
+ writeFileSync16(join21(iosDir, "LeCodesRuntime", "Package.swift"), runtimePackageTemplate(sdkVersion, variant, runtimePlugins));
221018
+ writeFileSync16(join21(sourcesDir, "LeCodesRuntime", "LeCodesRuntime.swift"), runtimeSwiftTemplate(runtimePlugins, shellUuid));
220106
221019
  const mergedKeys = {};
220107
221020
  for (const [key, values] of Object.entries(infoValues))
220108
221021
  mergedKeys[key] = [...new Set(values)].join(" ");
220109
221022
  const updateUrl = resolveUpdateUrl(config.update, root);
220110
- writeFileSync15(join20(iosDir, "App", "Info.plist"), infoPlistTemplate(mergedKeys, updateUrl));
221023
+ writeFileSync16(join21(iosDir, "App", "Info.plist"), infoPlistTemplate(mergedKeys, updateUrl));
220111
221024
  if (updateUrl)
220112
221025
  log(`${c.green("update")} OTA source ${updateUrl} — checked natively at launch, new code applies next launch`);
220113
221026
  else if (!config.update) {
@@ -220118,10 +221031,10 @@ var syncIos = async (root, config) => {
220118
221031
  if (manifest)
220119
221032
  note(`tip: add "update": true to app.json for over-the-air JS updates — ship with \`lecodes push --publish\`, no store release`);
220120
221033
  }
220121
- if (pluginSpecs.length > 0 || existsSync19(lockPath))
221034
+ if (pluginSpecs.length > 0 || existsSync20(lockPath))
220122
221035
  writeJson(lockPath, { plugins: newLock });
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};`);
221036
+ const pbxPath = join21(iosDir, "App.xcodeproj", "project.pbxproj");
221037
+ let pbx = readFileSync17(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};`);
220125
221038
  const mergedEntitlements = {};
220126
221039
  for (const [key, values] of Object.entries(entitlementValues)) {
220127
221040
  if (values.every((v) => Array.isArray(v))) {
@@ -220142,7 +221055,7 @@ var syncIos = async (root, config) => {
220142
221055
  }
220143
221056
  const hasEntitlements = Object.keys(mergedEntitlements).length > 0;
220144
221057
  if (pbx.includes("CODE_SIGN_ENTITLEMENTS") || hasEntitlements) {
220145
- writeFileSync15(join20(iosDir, "App", "App.entitlements"), entitlementsTemplate(mergedEntitlements));
221058
+ writeFileSync16(join21(iosDir, "App", "App.entitlements"), entitlementsTemplate(mergedEntitlements));
220146
221059
  if (hasEntitlements)
220147
221060
  log(`${c.green("caps")} App/App.entitlements: ${Object.keys(mergedEntitlements).sort().join(", ")}`);
220148
221061
  if (!pbx.includes("CODE_SIGN_ENTITLEMENTS")) {
@@ -220158,7 +221071,7 @@ $2Info.plist,`);
220158
221071
  }
220159
221072
  }
220160
221073
  }
220161
- writeFileSync15(pbxPath, pbx);
221074
+ writeFileSync16(pbxPath, pbx);
220162
221075
  if (config.icon) {
220163
221076
  await writeIosAppIcon(iosDir, await loadIconMaster(root, config));
220164
221077
  }
@@ -220170,7 +221083,7 @@ $2Info.plist,`);
220170
221083
  var appPlugin = async (args) => {
220171
221084
  const sub = args._[1];
220172
221085
  const root = projectRoot();
220173
- const configPath = join20(root, "app.json");
221086
+ const configPath = join21(root, "app.json");
220174
221087
  const config = readJson(configPath);
220175
221088
  if (!config)
220176
221089
  throw new CliError("No app.json here — run `lecodes app init <ios|android>` first.");
@@ -220181,7 +221094,7 @@ var appPlugin = async (args) => {
220181
221094
  log("No plugins installed.");
220182
221095
  return;
220183
221096
  }
220184
- const lock = readJson(join20(root, "plugins.lock.json"))?.plugins ?? {};
221097
+ const lock = readJson(join21(root, "plugins.lock.json"))?.plugins ?? {};
220185
221098
  for (const [id2, spec] of entries) {
220186
221099
  const pin = lock[id2] ? c.dim(` ${lock[id2].repo} ${lock[id2].tag} (${lock[id2].commit.slice(0, 7)})`) : c.dim(" (not synced)");
220187
221100
  log(`${c.bold(id2)} ${spec}${pin}`);
@@ -220234,14 +221147,14 @@ var exportOptionsPlist = (method, teamId, destination) => `<?xml version="1.0" e
220234
221147
  `;
220235
221148
  var requireShell = () => {
220236
221149
  const root = projectRoot();
220237
- const config = readJson(join20(root, "app.json"));
220238
- if (!config?.ios || !existsSync19(join20(root, "ios"))) {
221150
+ const config = readJson(join21(root, "app.json"));
221151
+ if (!config?.ios || !existsSync20(join21(root, "ios"))) {
220239
221152
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
220240
221153
  }
220241
221154
  return { root, config };
220242
221155
  };
220243
221156
  var updateSdkPin = async (root, config, args, checkOnly, platform) => {
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"))];
221157
+ const [label, repo, minVersion, shellVersion] = platform === "ios" ? ["lecodes-ios-sdk", SDK_REPO, SDK_MIN_VERSION, () => shellSdkVersion(join21(root, "ios"))] : ["lecodes-android-sdk", ANDROID_SDK_REPO, ANDROID_SDK_MIN_VERSION, () => shellSdkVersionAndroid(join21(root, "android"))];
220245
221158
  const section = config[platform];
220246
221159
  const wanted = flagStr(args, "sdk");
220247
221160
  let changed = false;
@@ -220272,19 +221185,19 @@ var updateSdkPin = async (root, config, args, checkOnly, platform) => {
220272
221185
  }
220273
221186
  if (!checkOnly && nextPin !== pin) {
220274
221187
  section.sdk = nextPin;
220275
- writeJson(join20(root, "app.json"), config);
221188
+ writeJson(join21(root, "app.json"), config);
220276
221189
  }
220277
221190
  return changed;
220278
221191
  };
220279
221192
  var appUpdate = async (args) => {
220280
221193
  const root = projectRoot();
220281
- const config = readJson(join20(root, "app.json"));
221194
+ const config = readJson(join21(root, "app.json"));
220282
221195
  const platform = args._[1];
220283
221196
  if (platform !== undefined && platform !== "ios" && platform !== "android") {
220284
221197
  throw new CliError(`Unknown platform "${platform}" — use ios or android.`);
220285
221198
  }
220286
- const hasIos = !!config?.ios && existsSync19(join20(root, "ios"));
220287
- const hasAndroid = !!config?.android && existsSync19(join20(root, "android"));
221199
+ const hasIos = !!config?.ios && existsSync20(join21(root, "ios"));
221200
+ const hasAndroid = !!config?.android && existsSync20(join21(root, "android"));
220288
221201
  if (platform === "ios" && !hasIos)
220289
221202
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
220290
221203
  if (platform === "android" && !hasAndroid)
@@ -220302,7 +221215,7 @@ var appUpdate = async (args) => {
220302
221215
  changed = await updateSdkPin(root, config, args, checkOnly, "ios") || changed;
220303
221216
  if (doAndroid)
220304
221217
  changed = await updateSdkPin(root, config, args, checkOnly, "android") || changed;
220305
- const lockPath = join20(root, "plugins.lock.json");
221218
+ const lockPath = join21(root, "plugins.lock.json");
220306
221219
  const lock = readJson(lockPath)?.plugins ?? {};
220307
221220
  const specs = Object.entries(config.plugins ?? {});
220308
221221
  if (specs.length > 0 && !flagBool(args, "no-plugins")) {
@@ -220331,7 +221244,7 @@ var appUpdate = async (args) => {
220331
221244
  log(`${c.green("plugin")} ${id} ${entry?.tag ?? "—"} → ${c.bold(tag)}`);
220332
221245
  changed = true;
220333
221246
  }
220334
- if (!checkOnly && existsSync19(lockPath))
221247
+ if (!checkOnly && existsSync20(lockPath))
220335
221248
  writeJson(lockPath, { plugins: kept });
220336
221249
  }
220337
221250
  log("");
@@ -220351,9 +221264,9 @@ var appUpdate = async (args) => {
220351
221264
  };
220352
221265
  var appDoctor = async (_args) => {
220353
221266
  const root = projectRoot();
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"));
221267
+ const config = readJson(join21(root, "app.json"));
221268
+ const hasIos = !!config?.ios && existsSync20(join21(root, "ios"));
221269
+ const hasAndroid = !!config?.android && existsSync20(join21(root, "android"));
220357
221270
  if (!hasIos && !hasAndroid) {
220358
221271
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
220359
221272
  }
@@ -220364,14 +221277,14 @@ var appDoctor = async (_args) => {
220364
221277
  failed++;
220365
221278
  };
220366
221279
  if (config?.icon) {
220367
- check(existsSync19(join20(root, config.icon)), `Icon master ${config.icon}`, `app.json "icon" points at a missing file — sync will fail`);
221280
+ check(existsSync20(join21(root, config.icon)), `Icon master ${config.icon}`, `app.json "icon" points at a missing file — sync will fail`);
220368
221281
  }
220369
221282
  if (hasIos)
220370
221283
  await doctorIosChecks(root, config, check);
220371
221284
  if (hasAndroid) {
220372
221285
  await doctorAndroidChecks(root, config, check);
220373
221286
  if (!hasIos) {
220374
- const lock = readJson(join20(root, "plugins.lock.json"))?.plugins ?? {};
221287
+ const lock = readJson(join21(root, "plugins.lock.json"))?.plugins ?? {};
220375
221288
  const wanted = Object.keys(config.plugins ?? {});
220376
221289
  const inSync = wanted.every((id) => lock[id]) && Object.keys(lock).every((id) => wanted.includes(id));
220377
221290
  check(wanted.length === 0 || inSync, "Plugins synced", "run `lecodes app sync`");
@@ -220390,22 +221303,22 @@ var doctorIosChecks = async (root, config, check) => {
220390
221303
  } catch {}
220391
221304
  check(xcodeVersion !== "", `Xcode CLI (${xcodeVersion || "not found"})`, "install Xcode + command line tools");
220392
221305
  check(!!config.ios.teamId, "Development team set", "lecodes app init wrote app.json — fill ios.teamId", false);
220393
- const iconDir = join20(root, "ios", "App", "Assets.xcassets", "AppIcon.appiconset");
220394
- const hasIcon = existsSync19(iconDir) && readdirSync8(iconDir).some((f3) => f3.endsWith(".png"));
221306
+ const iconDir = join21(root, "ios", "App", "Assets.xcassets", "AppIcon.appiconset");
221307
+ const hasIcon = existsSync20(iconDir) && readdirSync9(iconDir).some((f3) => f3.endsWith(".png"));
220395
221308
  check(hasIcon, "App icon present", "drop a 1024×1024 PNG into ios/App/Assets.xcassets/AppIcon.appiconset/ (App Store requires it)", false);
220396
- const bundle = join20(root, "ios", "App", "Resources", "app.js");
220397
- check(existsSync19(bundle), "Bundled app.js", "run `lecodes app sync`");
221309
+ const bundle = join21(root, "ios", "App", "Resources", "app.js");
221310
+ check(existsSync20(bundle), "Bundled app.js", "run `lecodes app sync`");
220398
221311
  const requestedVariant = config.ios.variant ?? "auto";
220399
221312
  const validVariant = requestedVariant === "auto" || SDK_VARIANTS.includes(requestedVariant);
220400
221313
  check(validVariant, `ios.variant "${requestedVariant}"`, `use "auto" or one of ${SDK_VARIANTS.join(", ")}`);
220401
- if (existsSync19(bundle) && validVariant) {
220402
- const { variant } = resolveVariant(requestedVariant, readFileSync16(bundle, "utf8"));
220403
- const pkg = readFileSync16(join20(root, "ios", "LeCodesRuntime", "Package.swift"), "utf8");
221314
+ if (existsSync20(bundle) && validVariant) {
221315
+ const { variant } = resolveVariant(requestedVariant, readFileSync17(bundle, "utf8"));
221316
+ const pkg = readFileSync17(join21(root, "ios", "LeCodesRuntime", "Package.swift"), "utf8");
220404
221317
  const needsResources = variant === "3d" || variant === "full";
220405
- const hasResources = existsSync19(join20(root, "ios", "App", "Resources", "LeCodesSDKResources.bundle"));
221318
+ const hasResources = existsSync20(join21(root, "ios", "App", "Resources", "LeCodesSDKResources.bundle"));
220406
221319
  check(pkg.includes(`"LeCodesSDK-${variant}"`) && (!needsResources || hasResources), `SDK variant ${variant}${needsResources ? " (+ filament resources)" : ""}`, "run `lecodes app sync`");
220407
221320
  }
220408
- const linked = shellSdkVersion(join20(root, "ios"));
221321
+ const linked = shellSdkVersion(join21(root, "ios"));
220409
221322
  const pin = config.ios.sdk ?? "latest";
220410
221323
  check(!!linked, `SDK ${linked ?? "version unreadable"}`, "run `lecodes app sync`");
220411
221324
  if (linked && pin !== "latest") {
@@ -220417,27 +221330,27 @@ var doctorIosChecks = async (root, config, check) => {
220417
221330
  check(cmpVersion(linked, latest) >= 0, `SDK is the latest release (${latest})`, "run `lecodes app update`", false);
220418
221331
  } catch {}
220419
221332
  }
220420
- const lock = readJson(join20(root, "plugins.lock.json"))?.plugins ?? {};
221333
+ const lock = readJson(join21(root, "plugins.lock.json"))?.plugins ?? {};
220421
221334
  const wanted = Object.keys(config.plugins ?? {});
220422
221335
  const inSync = wanted.every((id) => lock[id]) && Object.keys(lock).every((id) => wanted.includes(id));
220423
221336
  check(wanted.length === 0 || inSync, "Plugins synced", "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`");
221337
+ const pbxFile = join21(root, "ios", "App.xcodeproj", "project.pbxproj");
221338
+ if (existsSync20(pbxFile) && readFileSync17(pbxFile, "utf8").includes("CODE_SIGN_ENTITLEMENTS")) {
221339
+ check(existsSync20(join21(root, "ios", "App", "App.entitlements")), "App/App.entitlements present", "run `lecodes app sync`");
220427
221340
  }
220428
221341
  if (wanted.includes("push")) {
220429
- const runtimeSwift = join20(root, "ios", "LeCodesRuntime", "Sources", "LeCodesRuntime", "LeCodesRuntime.swift");
220430
- const baked = existsSync19(runtimeSwift) && readFileSync16(runtimeSwift, "utf8").includes("bootProjectUuid");
221342
+ const runtimeSwift = join21(root, "ios", "LeCodesRuntime", "Sources", "LeCodesRuntime", "LeCodesRuntime.swift");
221343
+ const baked = existsSync20(runtimeSwift) && readFileSync17(runtimeSwift, "utf8").includes("bootProjectUuid");
220431
221344
  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);
220432
221345
  }
220433
221346
  if (config.update) {
220434
- const plist = readFileSync16(join20(root, "ios", "App", "Info.plist"), "utf8");
221347
+ const plist = readFileSync17(join21(root, "ios", "App", "Info.plist"), "utf8");
220435
221348
  check(plist.includes("<key>LeCodesUpdateURL</key>"), "OTA update URL in Info.plist", "run `lecodes app sync`", false);
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"));
221349
+ const appDir = join21(root, "ios", "App");
221350
+ const callsUpdater = readdirSync9(appDir).some((f3) => f3.endsWith(".swift") && readFileSync17(join21(appDir, f3), "utf8").includes("LeCodesUpdater.activeBundle"));
220438
221351
  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);
220439
221352
  }
220440
- const asc = readJson(join20(homedir7(), ".lecodes", "asc.json"));
221353
+ const asc = readJson(join21(homedir7(), ".lecodes", "asc.json"));
220441
221354
  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);
220442
221355
  };
220443
221356
  var ARCHIVE_PATH = "ios/build/App.xcarchive";
@@ -220473,8 +221386,8 @@ var appBuild = async (args) => {
220473
221386
  success(`Archive: ios/build/App.xcarchive`);
220474
221387
  if (flagBool(args, "no-export"))
220475
221388
  return;
220476
- const optionsPath = join20(root, "ios", "build", "ExportOptions.plist");
220477
- writeFileSync15(optionsPath, exportOptionsPlist(method, config.ios.teamId ?? ""));
221389
+ const optionsPath = join21(root, "ios", "build", "ExportOptions.plist");
221390
+ writeFileSync16(optionsPath, exportOptionsPlist(method, config.ios.teamId ?? ""));
220478
221391
  note(`Exporting (.ipa, method ${method})…`);
220479
221392
  runTool(root, "xcodebuild", [
220480
221393
  "-exportArchive",
@@ -220493,10 +221406,10 @@ var appUpload = async (args) => {
220493
221406
  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).");
220494
221407
  }
220495
221408
  const { root, config } = requireShell();
220496
- if (!existsSync19(join20(root, ARCHIVE_PATH))) {
221409
+ if (!existsSync20(join21(root, ARCHIVE_PATH))) {
220497
221410
  throw new CliError("No archive at ios/build/App.xcarchive — run `lecodes app build ios` first.");
220498
221411
  }
220499
- const ascPath = join20(homedir7(), ".lecodes", "asc.json");
221412
+ const ascPath = join21(homedir7(), ".lecodes", "asc.json");
220500
221413
  const stored = readJson(ascPath) ?? {};
220501
221414
  const keyPath = flagStr(args, "key-path") ?? stored.keyPath;
220502
221415
  const keyId = flagStr(args, "key-id") ?? stored.keyId;
@@ -220505,15 +221418,15 @@ var appUpload = async (args) => {
220505
221418
  throw new CliError(`App Store Connect API key needed: --key-path <AuthKey.p8> --key-id <id> --issuer-id <id>
220506
221419
  (create one at appstoreconnect.apple.com → Users and Access → Integrations; stored in ~/.lecodes/asc.json after first use)`);
220507
221420
  }
220508
- if (!existsSync19(keyPath))
221421
+ if (!existsSync20(keyPath))
220509
221422
  throw new CliError(`API key file not found: ${keyPath}`);
220510
221423
  if (flagStr(args, "key-path")) {
220511
- mkdirSync13(dirname12(ascPath), { recursive: true });
221424
+ mkdirSync14(dirname12(ascPath), { recursive: true });
220512
221425
  writeJson(ascPath, { keyPath, keyId, issuerId });
220513
221426
  note("Stored the key reference in ~/.lecodes/asc.json");
220514
221427
  }
220515
- const optionsPath = join20(root, "ios", "build", "UploadOptions.plist");
220516
- writeFileSync15(optionsPath, exportOptionsPlist("app-store-connect", config.ios.teamId ?? "", "upload"));
221428
+ const optionsPath = join21(root, "ios", "build", "UploadOptions.plist");
221429
+ writeFileSync16(optionsPath, exportOptionsPlist("app-store-connect", config.ios.teamId ?? "", "upload"));
220517
221430
  note("Uploading to App Store Connect… (processing continues on Apple's side)");
220518
221431
  runTool(root, "xcodebuild", [
220519
221432
  "-exportArchive",
@@ -220524,14 +221437,14 @@ var appUpload = async (args) => {
220524
221437
  "-exportPath",
220525
221438
  "ios/build/upload",
220526
221439
  "-authenticationKeyPath",
220527
- resolve7(keyPath),
221440
+ resolve8(keyPath),
220528
221441
  "-authenticationKeyID",
220529
221442
  keyId,
220530
221443
  "-authenticationKeyIssuerID",
220531
221444
  issuerId,
220532
221445
  "-allowProvisioningUpdates"
220533
221446
  ]);
220534
- const configPath = join20(root, "app.json");
221447
+ const configPath = join21(root, "app.json");
220535
221448
  const fresh = readJson(configPath);
220536
221449
  fresh.ios.buildNumber = (fresh.ios.buildNumber ?? 1) + 1;
220537
221450
  writeJson(configPath, fresh);
@@ -220542,19 +221455,19 @@ var appOpen = async (args) => {
220542
221455
  const root = projectRoot();
220543
221456
  let platform = args._[1];
220544
221457
  if (!platform) {
220545
- const hasIos = existsSync19(join20(root, "ios"));
220546
- const hasAndroid = existsSync19(join20(root, "android"));
221458
+ const hasIos = existsSync20(join21(root, "ios"));
221459
+ const hasAndroid = existsSync20(join21(root, "android"));
220547
221460
  if (hasIos && hasAndroid)
220548
221461
  throw new CliError("Both shells exist — `lecodes app open ios` or `lecodes app open android`.");
220549
221462
  platform = hasAndroid ? "android" : "ios";
220550
221463
  }
220551
221464
  if (platform === "ios") {
220552
- const proj = join20(root, "ios", "App.xcodeproj");
220553
- if (!existsSync19(proj))
221465
+ const proj = join21(root, "ios", "App.xcodeproj");
221466
+ if (!existsSync20(proj))
220554
221467
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
220555
221468
  if (process.platform !== "darwin")
220556
221469
  throw new CliError("Opening Xcode requires macOS.");
220557
- execFileSync4("open", [proj], { stdio: "ignore" });
221470
+ execFileSync6("open", [proj], { stdio: "ignore" });
220558
221471
  success("Opened ios/App.xcodeproj in Xcode.");
220559
221472
  return;
220560
221473
  }
@@ -220587,8 +221500,8 @@ var app = async (args) => {
220587
221500
  init_api();
220588
221501
  init_config();
220589
221502
  init_manifest();
220590
- import { existsSync as existsSync21, mkdirSync as mkdirSync15, readdirSync as readdirSync10 } from "node:fs";
220591
- import { resolve as resolve8 } from "node:path";
221503
+ import { existsSync as existsSync22, mkdirSync as mkdirSync16, readdirSync as readdirSync11 } from "node:fs";
221504
+ import { resolve as resolve9 } from "node:path";
220592
221505
  init_types4();
220593
221506
  init_ignore();
220594
221507
  init_util();
@@ -220609,11 +221522,11 @@ var clone3 = async (args) => {
220609
221522
  const uuid = parseUuid(args._[0]);
220610
221523
  const project = await getProject(apiUrl, token, uuid);
220611
221524
  const dir = args._[1] ?? slug(project.name);
220612
- const root = resolve8(dir);
220613
- if (existsSync21(root) && readdirSync10(root).length > 0) {
221525
+ const root = resolve9(dir);
221526
+ if (existsSync22(root) && readdirSync11(root).length > 0) {
220614
221527
  throw new CliError(`Directory "${dir}" already exists and is not empty.`);
220615
221528
  }
220616
- mkdirSync15(root, { recursive: true });
221529
+ mkdirSync16(root, { recursive: true });
220617
221530
  info(c.dim(`Fetching project ${c.bold(project.name)}…`));
220618
221531
  const files = await materializeAssets(root, project, apiUrl, token, progress);
220619
221532
  endProgress();
@@ -220636,8 +221549,8 @@ init_config();
220636
221549
  init_manifest();
220637
221550
  init_types4();
220638
221551
  init_ignore();
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";
221552
+ import { existsSync as existsSync23, mkdirSync as mkdirSync17, readdirSync as readdirSync12, writeFileSync as writeFileSync18 } from "node:fs";
221553
+ import { join as join23, resolve as resolve10 } from "node:path";
220641
221554
 
220642
221555
  // src/commands/projectTemplates.ts
220643
221556
  var appClaudeMd = (name) => `# ${name} — LeCodes app
@@ -220654,6 +221567,9 @@ from \`main.ts\`.
220654
221567
  lecodes render # semantic JSON of the launched UI (structure, text, names, values)
220655
221568
  lecodes render --png shot.png # pixels (needs @napi-rs/canvas installed)
220656
221569
  lecodes render --png card.png --clip card # ONE component/region: a node name or x,y,w,h
221570
+ lecodes render src/screens/cart.ts # ONE screen module, without booting the app: any file that
221571
+ # \`export default\`s a UIScreen (or a (state) => UIScreen —
221572
+ # --state <name> picks the branch). Same flags as above.
220657
221573
  lecodes render --desktop --png shot.png # NATIVE render: real Filament 3D + UI + 2D composited —
220658
221574
  # use this to SEE 3D scenes / procedural geometry (headless
220659
221575
  # renders record 3D as JSON only). Windows/Linux; downloads
@@ -220774,21 +221690,21 @@ var create = async (args) => {
220774
221690
  if (!name)
220775
221691
  throw new CliError("Usage: lecodes create <name> [directory]");
220776
221692
  const dir = args._[1] ?? slug2(name);
220777
- const root = resolve9(dir);
220778
- if (existsSync22(root) && readdirSync11(root).length > 0) {
221693
+ const root = resolve10(dir);
221694
+ if (existsSync23(root) && readdirSync12(root).length > 0) {
220779
221695
  throw new CliError(`Directory "${dir}" already exists and is not empty.`);
220780
221696
  }
220781
- mkdirSync16(root, { recursive: true });
221697
+ mkdirSync17(root, { recursive: true });
220782
221698
  info(c.dim(`Creating project ${c.bold(name)}…`));
220783
221699
  const { uuid } = await createProject(apiUrl, token, name, flagStr(args, "team"));
220784
221700
  info(c.dim("Fetching type definitions…"));
220785
221701
  await materializeTypes(root, apiUrl);
220786
221702
  writeTsconfig(root);
220787
221703
  writeDefaultIgnore(root);
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);
221704
+ writeFileSync18(join23(root, "main.ts"), STARTER);
221705
+ writeFileSync18(join23(root, "CLAUDE.md"), appClaudeMd(name));
221706
+ mkdirSync17(join23(root, ".claude"), { recursive: true });
221707
+ writeFileSync18(join23(root, ".claude", "settings.json"), CLAUDE_SETTINGS);
220792
221708
  const commits = await getCommits(apiUrl, token, uuid);
220793
221709
  writeManifest(root, { uuid, apiUrl, name, baseHeadId: commits.headId, files: {} });
220794
221710
  success(`Created ${c.bold(name)} in ${c.bold(dir)}/`);
@@ -220828,9 +221744,12 @@ var run = async (args) => {
220828
221744
  }
220829
221745
  const appCfg = readAppConfigAt(root);
220830
221746
  const win = appCfg?.desktop?.window;
220831
- const { js, resources } = await buildBundle(args, undefined, { localAssets: true });
220832
- note("Launching LeCodes Desktop… (close the window to return)");
221747
+ const renderer = resolveDesktopRenderer(root, args);
221748
+ const { js, resources } = await buildBundle(args, undefined, { localAssets: true, desktopRenderer: renderer });
221749
+ assertRendererSupportsBundle(renderer, js);
221750
+ note(`Launching LeCodes Desktop${renderer === "vulkan" ? " (Vulkan)" : ""}… (close the window to return)`);
220833
221751
  await runDesktopApp(js, resources, {
221752
+ renderer,
220834
221753
  width: Number(flagStr(args, "width")) || win?.width,
220835
221754
  height: Number(flagStr(args, "height")) || win?.height,
220836
221755
  minWidth: win?.minWidth,
@@ -220840,7 +221759,8 @@ var run = async (args) => {
220840
221759
  fullscreen: flagBool(args, "fullscreen") ? true : flagBool(args, "windowed") ? false : win?.fullscreen,
220841
221760
  scale: resolveScale(flagStr(args, "scale"), win?.scale),
220842
221761
  renderScale: resolveRenderScale(flagStr(args, "render-scale"), appCfg?.desktop?.renderScale),
220843
- maxRenderSize: appCfg?.desktop?.maxRenderSize
221762
+ maxRenderSize: appCfg?.desktop?.maxRenderSize,
221763
+ fps: flagBool(args, "fps")
220844
221764
  });
220845
221765
  };
220846
221766
  var status = async () => {
@@ -220851,6 +221771,10 @@ var status = async () => {
220851
221771
  log(`downloaded ${installed.length > 0 ? installed.join(", ") : c.dim("none")} ${c.dim("(~/.lecodes/renderer)")}`);
220852
221772
  if (process.env.LECODES_DESKTOP_EXE)
220853
221773
  log(`override LECODES_DESKTOP_EXE=${process.env.LECODES_DESKTOP_EXE}`);
221774
+ if (process.platform === "darwin") {
221775
+ const installed2 = findMacHostExe();
221776
+ log(`installed ${installed2 ?? c.dim("none")} ${c.dim("(offline fallback only)")}`);
221777
+ }
220854
221778
  try {
220855
221779
  const { latest } = await fetchDesktopManifest();
220856
221780
  const hint = semverNewer(selected, latest) > 0 ? c.yellow("(newer — run `lecodes desktop update`)") : latest === selected ? c.dim("(up to date)") : c.dim("(older than the CLI's minimum — this desktop release isn't published yet)");
@@ -220865,9 +221789,11 @@ var desktop = async (args) => {
220865
221789
  return status();
220866
221790
  if (sub === "run")
220867
221791
  return run(args);
221792
+ if (sub === "build")
221793
+ return buildDesktop(args);
220868
221794
  if (sub === "update")
220869
221795
  return updateDesktop();
220870
- throw new CliError(`Unknown subcommand "desktop ${sub}". Use \`lecodes desktop\` (status), \`lecodes desktop run\` or \`lecodes desktop update\`.`);
221796
+ throw new CliError(`Unknown subcommand "desktop ${sub}". Use \`lecodes desktop\` (status), \`lecodes desktop run\`, \`lecodes desktop build\` or \`lecodes desktop update\`.`);
220871
221797
  };
220872
221798
 
220873
221799
  // src/index.ts
@@ -220876,16 +221802,16 @@ init_design();
220876
221802
  // src/commands/dev.ts
220877
221803
  init_manifest();
220878
221804
  init_ignore();
220879
- import { existsSync as existsSync24, mkdirSync as mkdirSync18, readFileSync as readFileSync20, watch, writeFileSync as writeFileSync19 } from "node:fs";
221805
+ import { existsSync as existsSync25, mkdirSync as mkdirSync19, readFileSync as readFileSync22, watch, writeFileSync as writeFileSync20 } from "node:fs";
220880
221806
  import { randomBytes as randomBytes3 } from "node:crypto";
220881
221807
  import { networkInterfaces } from "node:os";
220882
- import { basename as basename10, join as join25 } from "node:path";
221808
+ import { basename as basename11, join as join27 } from "node:path";
220883
221809
 
220884
221810
  // src/dev/devServer.ts
220885
221811
  init_localFiles();
220886
221812
  import { createServer as createServer2 } from "node:http";
220887
- import { createReadStream, statSync as statSync8 } from "node:fs";
220888
- import { join as join24 } from "node:path";
221813
+ import { createReadStream, statSync as statSync9 } from "node:fs";
221814
+ import { join as join26 } from "node:path";
220889
221815
  init_projectCompile();
220890
221816
  init_fonts();
220891
221817
  init_assetIcons();
@@ -221159,14 +222085,14 @@ var sendText = (res, status2, type, body) => {
221159
222085
  res.end(body);
221160
222086
  };
221161
222087
  var sendJson = (res, status2, body) => sendText(res, status2, "application/json", JSON.stringify(body));
221162
- var readBody = (req) => new Promise((resolve10) => {
222088
+ var readBody = (req) => new Promise((resolve11) => {
221163
222089
  let body = "";
221164
222090
  req.on("data", (c4) => {
221165
222091
  if (body.length < 256 * 1024)
221166
222092
  body += c4.toString("utf8");
221167
222093
  });
221168
- req.on("end", () => resolve10(body));
221169
- req.on("error", () => resolve10(body));
222094
+ req.on("end", () => resolve11(body));
222095
+ req.on("error", () => resolve11(body));
221170
222096
  });
221171
222097
  var startDevServer = async (options) => {
221172
222098
  const { root, manifest, name, origin, token, entryOverride } = options;
@@ -221271,9 +222197,9 @@ var startDevServer = async (options) => {
221271
222197
  const name2 = route.slice("/shaders/".length);
221272
222198
  if (!isShaderArtifactName(name2))
221273
222199
  return sendText(res, 400, "text/plain", "Bad path");
221274
- const abs = join24(shadersCacheDir(root), name2);
222200
+ const abs = join26(shadersCacheDir(root), name2);
221275
222201
  try {
221276
- if (!statSync8(abs).isFile())
222202
+ if (!statSync9(abs).isFile())
221277
222203
  throw new Error("not a file");
221278
222204
  } catch {
221279
222205
  return sendText(res, 404, "text/plain", "Not found");
@@ -221290,7 +222216,7 @@ var startDevServer = async (options) => {
221290
222216
  return sendText(res, 400, "text/plain", "Bad path");
221291
222217
  }
221292
222218
  const abs = resolveLocalPath(root, key);
221293
- if (!abs || !statSync8(abs).isFile())
222219
+ if (!abs || !statSync9(abs).isFile())
221294
222220
  return sendText(res, 404, "text/plain", "Not found");
221295
222221
  res.writeHead(200, { "content-type": contentType(key), "cache-control": "no-store" });
221296
222222
  createReadStream(abs).pipe(res);
@@ -221333,11 +222259,11 @@ var startDevServer = async (options) => {
221333
222259
  if (conn)
221334
222260
  clients.add(conn);
221335
222261
  });
221336
- await new Promise((resolve10, reject) => {
222262
+ await new Promise((resolve11, reject) => {
221337
222263
  server2.once("error", reject);
221338
222264
  server2.listen(options.port, options.bind ?? "0.0.0.0", () => {
221339
222265
  server2.removeListener("error", reject);
221340
- resolve10();
222266
+ resolve11();
221341
222267
  });
221342
222268
  });
221343
222269
  const address = server2.address();
@@ -221385,24 +222311,24 @@ var lanCandidates = () => {
221385
222311
  return out.sort((x2, y2) => x2.rank - y2.rank).map(({ address, iface }) => ({ address, iface }));
221386
222312
  };
221387
222313
  var loadDevToken = (root) => {
221388
- const path2 = join25(root, LECODES_DIR, "dev.json");
222314
+ const path2 = join27(root, LECODES_DIR, "dev.json");
221389
222315
  try {
221390
- const token2 = JSON.parse(readFileSync20(path2, "utf8")).token;
222316
+ const token2 = JSON.parse(readFileSync22(path2, "utf8")).token;
221391
222317
  if (token2 && /^[a-z0-9]{8,}$/.test(token2))
221392
222318
  return token2;
221393
222319
  } catch {}
221394
222320
  const token = randomBytes3(6).toString("hex");
221395
- mkdirSync18(join25(root, LECODES_DIR), { recursive: true });
221396
- writeFileSync19(path2, JSON.stringify({ token }, null, 2) + `
222321
+ mkdirSync19(join27(root, LECODES_DIR), { recursive: true });
222322
+ writeFileSync20(path2, JSON.stringify({ token }, null, 2) + `
221397
222323
  `);
221398
222324
  return token;
221399
222325
  };
221400
222326
  var printQr = async (url) => {
221401
222327
  try {
221402
222328
  const qrcode = (await Promise.resolve().then(() => __toESM(require_main(), 1))).default;
221403
- await new Promise((resolve10) => qrcode.generate(url, { small: true }, (out) => {
222329
+ await new Promise((resolve11) => qrcode.generate(url, { small: true }, (out) => {
221404
222330
  log(out);
221405
- resolve10();
222331
+ resolve11();
221406
222332
  }));
221407
222333
  } catch {
221408
222334
  note("(couldn't render a QR code — open the URL below on the device)");
@@ -221418,7 +222344,7 @@ var dev = async (args) => {
221418
222344
  root = process.cwd();
221419
222345
  manifest = null;
221420
222346
  }
221421
- const name = manifest?.name ?? basename10(root);
222347
+ const name = manifest?.name ?? basename11(root);
221422
222348
  if (typeof globalThis.Bun !== "undefined") {
221423
222349
  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).");
221424
222350
  }
@@ -221510,7 +222436,7 @@ ${result.error}`);
221510
222436
  if (alternatives.length > 0) {
221511
222437
  note(`Device can't reach it? This machine also has ${alternatives.map((a2) => `${a2.address} (${a2.iface})`).join(", ")} — retry with --host <ip>.`);
221512
222438
  }
221513
- if (existsSync24(join25(root, LECODES_DIR, "manifest.json")))
222439
+ if (existsSync25(join27(root, LECODES_DIR, "manifest.json")))
221514
222440
  note(`Token stored in ${LECODES_DIR}/dev.json (delete it to rotate the URL).`);
221515
222441
  log("");
221516
222442
  log(`Watching ${root} — save a file to hot-reload. Ctrl+C to stop.`);
@@ -221525,7 +222451,8 @@ ${result.error}`);
221525
222451
  minHeight: win?.minHeight,
221526
222452
  resizable: win?.resizable,
221527
222453
  title: win?.title ?? appCfg?.name ?? name,
221528
- scale: resolveScale(undefined, win?.scale)
222454
+ scale: resolveScale(undefined, win?.scale),
222455
+ fps: flagBool(args, "fps")
221529
222456
  }).then(() => note("Desktop window closed — the dev server keeps running (Ctrl+C to stop)."), (e2) => warnErr(`Desktop host: ${e2 instanceof Error ? e2.message : String(e2)}`));
221530
222457
  }
221531
222458
  };
@@ -221538,8 +222465,8 @@ init_localFiles();
221538
222465
 
221539
222466
  // src/serverDiff.ts
221540
222467
  init_localFiles();
221541
- import { readFileSync as readFileSync21 } from "node:fs";
221542
- import { basename as basename11 } from "node:path";
222468
+ import { readFileSync as readFileSync23 } from "node:fs";
222469
+ import { basename as basename12 } from "node:path";
221543
222470
  var isTextAsset = (t2) => t2 === "text" || t2 === "shader" || t2 === "scene";
221544
222471
  var computeServerDiff = (project, scan2) => {
221545
222472
  const pathById = buildPaths(project.assets);
@@ -221556,8 +222483,8 @@ var computeServerDiff = (project, scan2) => {
221556
222483
  const server2 = serverByPath.get(path2);
221557
222484
  const local = localByPath.get(path2);
221558
222485
  if (local && !server2) {
221559
- const text = isTextKind(classifyKind(basename11(path2)));
221560
- entries.push({ path: path2, status: "added", binary: !text, serverText: "", localText: text ? readFileSync21(local.absPath, "utf8") : undefined });
222486
+ const text = isTextKind(classifyKind(basename12(path2)));
222487
+ entries.push({ path: path2, status: "added", binary: !text, serverText: "", localText: text ? readFileSync23(local.absPath, "utf8") : undefined });
221561
222488
  continue;
221562
222489
  }
221563
222490
  if (server2 && !local) {
@@ -221568,12 +222495,12 @@ var computeServerDiff = (project, scan2) => {
221568
222495
  if (server2 && local) {
221569
222496
  if (isTextAsset(server2.type)) {
221570
222497
  const serverText = server2.text ?? "";
221571
- const localText = readFileSync21(local.absPath, "utf8");
222498
+ const localText = readFileSync23(local.absPath, "utf8");
221572
222499
  if (serverText !== localText)
221573
222500
  entries.push({ path: path2, status: "modified", binary: false, serverText, localText });
221574
222501
  } else {
221575
222502
  const serverSize = server2.file?.size;
221576
- const localSize = Buffer.byteLength(readFileSync21(local.absPath));
222503
+ const localSize = Buffer.byteLength(readFileSync23(local.absPath));
221577
222504
  if (serverSize !== undefined && serverSize !== localSize) {
221578
222505
  entries.push({ path: path2, status: "modified", binary: true });
221579
222506
  }
@@ -221731,8 +222658,8 @@ var statusLabel = (e2) => e2.status === "added" ? c.green(" added ") : e2.st
221731
222658
  init_manifest();
221732
222659
  init_types4();
221733
222660
  init_ignore();
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";
222661
+ import { existsSync as existsSync26, mkdirSync as mkdirSync20, writeFileSync as writeFileSync21 } from "node:fs";
222662
+ import { basename as basename13, dirname as dirname14, join as join28, resolve as resolve11 } from "node:path";
221736
222663
  init_util();
221737
222664
  var STARTER2 = `// Welcome to LeCodes — the whole SDK is available as globals, no imports needed.
221738
222665
  // Run it live on a device with \`lecodes dev\`, or bundle it with \`lecodes compile -o app.js\`.
@@ -221840,18 +222767,18 @@ Full command list: \`lecodes --help\`.
221840
222767
  `;
221841
222768
  var init2 = async (args) => {
221842
222769
  const dirArg = (args._[0] ?? ".").replace(/[/\\]+$/, "") || ".";
221843
- const root = resolve10(dirArg);
221844
- const name = basename12(root);
221845
- mkdirSync19(root, { recursive: true });
222770
+ const root = resolve11(dirArg);
222771
+ const name = basename13(root);
222772
+ mkdirSync20(root, { recursive: true });
221846
222773
  let created = 0;
221847
222774
  const write = (rel, content) => {
221848
- const abs = join26(root, rel);
221849
- if (existsSync25(abs)) {
222775
+ const abs = join28(root, rel);
222776
+ if (existsSync26(abs)) {
221850
222777
  note(`kept ${rel}`);
221851
222778
  return;
221852
222779
  }
221853
- mkdirSync19(dirname14(abs), { recursive: true });
221854
- writeFileSync20(abs, content);
222780
+ mkdirSync20(dirname14(abs), { recursive: true });
222781
+ writeFileSync21(abs, content);
221855
222782
  log(`${c.green("created")} ${rel}`);
221856
222783
  created++;
221857
222784
  };
@@ -221898,8 +222825,8 @@ init_config();
221898
222825
  init_ignore();
221899
222826
  init_localFiles();
221900
222827
  init_manifest();
221901
- import { existsSync as existsSync26 } from "node:fs";
221902
- import { basename as basename14, join as join27, resolve as resolve11 } from "node:path";
222828
+ import { existsSync as existsSync27 } from "node:fs";
222829
+ import { basename as basename15, join as join29, resolve as resolve12 } from "node:path";
221903
222830
  init_types4();
221904
222831
  init_util();
221905
222832
 
@@ -221908,8 +222835,8 @@ init_api();
221908
222835
  init_config();
221909
222836
  init_manifest();
221910
222837
  init_localFiles();
221911
- import { basename as basename13 } from "node:path";
221912
- import { readFileSync as readFileSync22 } from "node:fs";
222838
+ import { basename as basename14 } from "node:path";
222839
+ import { readFileSync as readFileSync24 } from "node:fs";
221913
222840
  init_util();
221914
222841
  var push = async (args) => {
221915
222842
  const root = findProjectRoot(process.cwd());
@@ -221965,14 +222892,14 @@ var push = async (args) => {
221965
222892
  let uploaded = 0;
221966
222893
  for (const file of scan2.files) {
221967
222894
  const source = sourceFor(file.path);
221968
- const kind = source?.type ?? classifyKind(basename13(file.path));
222895
+ const kind = source?.type ?? classifyKind(basename14(file.path));
221969
222896
  const entry = { path: file.path, type: kind };
221970
222897
  if (source)
221971
222898
  entry.assetId = source.id;
221972
222899
  if (isTextKind(kind)) {
221973
- entry.text = readFileSync22(file.absPath, "utf8");
222900
+ entry.text = readFileSync24(file.absPath, "utf8");
221974
222901
  } else if (!source || source.sha !== file.sha) {
221975
- const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename13(file.path), readFileSync22(file.absPath));
222902
+ const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename14(file.path), readFileSync24(file.absPath));
221976
222903
  entry.blobFileId = blob.id;
221977
222904
  uploaded++;
221978
222905
  }
@@ -222079,10 +223006,10 @@ var link = async (args) => {
222079
223006
  const apiUrl = requireApiUrl(config);
222080
223007
  const token = requireToken(config);
222081
223008
  const dirArg = args._[0];
222082
- const root = dirArg ? resolve11(dirArg) : process.cwd();
222083
- if (!existsSync26(root))
223009
+ const root = dirArg ? resolve12(dirArg) : process.cwd();
223010
+ if (!existsSync27(root))
222084
223011
  throw new CliError(`No such directory: ${root}`);
222085
- if (existsSync26(join27(root, LECODES_DIR, "manifest.json")) && !flagBool(args, "force")) {
223012
+ if (existsSync27(join29(root, LECODES_DIR, "manifest.json")) && !flagBool(args, "force")) {
222086
223013
  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.");
222087
223014
  }
222088
223015
  let enclosing = null;
@@ -222109,7 +223036,7 @@ var link = async (args) => {
222109
223036
  }
222110
223037
  name = project.name;
222111
223038
  } else {
222112
- name = flagStr(args, "name") ?? basename14(root);
223039
+ name = flagStr(args, "name") ?? basename15(root);
222113
223040
  info(c.dim(`Creating project ${c.bold(name)}…`));
222114
223041
  uuid = (await createProject(apiUrl, token, name, flagStr(args, "team"))).uuid;
222115
223042
  files = {};
@@ -222144,6 +223071,173 @@ var link = async (args) => {
222144
223071
  log(` ${c.dim("(add --publish to deploy it live in the same step)")}`);
222145
223072
  };
222146
223073
 
223074
+ // src/commands/lightmap.ts
223075
+ init_manifest();
223076
+ import { existsSync as existsSync28, mkdirSync as mkdirSync21, mkdtempSync as mkdtempSync4, rmSync as rmSync9, statSync as statSync10, unlinkSync, writeFileSync as writeFileSync22 } from "node:fs";
223077
+ import { spawnSync as spawnSync6 } from "node:child_process";
223078
+ import { tmpdir as tmpdir4 } from "node:os";
223079
+ import { join as join30, relative as relative4, resolve as resolve13 } from "node:path";
223080
+ init_util();
223081
+ var num = (args, key, fallback, min, max) => {
223082
+ const raw = flagStr(args, key);
223083
+ if (raw === undefined)
223084
+ return fallback;
223085
+ const n2 = Number(raw);
223086
+ if (!Number.isFinite(n2) || n2 < min || n2 > max)
223087
+ throw new CliError(`--${key} wants a number in ${min}..${max}, got "${raw}".`);
223088
+ return n2;
223089
+ };
223090
+ var findToktx = () => {
223091
+ const env = process.env.KTX_TOKTX;
223092
+ if (env && existsSync28(env))
223093
+ return env;
223094
+ for (const base of [process.env.LOCALAPPDATA && join30(process.env.LOCALAPPDATA, "Programs"), process.env.ProgramFiles]) {
223095
+ if (!base)
223096
+ continue;
223097
+ const guess = join30(base, "KTX-Software", "bin", "toktx.exe");
223098
+ if (existsSync28(guess))
223099
+ return guess;
223100
+ }
223101
+ return "toktx";
223102
+ };
223103
+ var checkToktx = (toktx) => {
223104
+ try {
223105
+ const p2 = spawnSync6(toktx, ["--version"], { encoding: "utf8" });
223106
+ if (p2.status === 0)
223107
+ return (p2.stdout ?? "").toString().trim() || "toktx";
223108
+ } catch {}
223109
+ return null;
223110
+ };
223111
+ var requireToktx = () => {
223112
+ const toktx = findToktx();
223113
+ if (!checkToktx(toktx)) {
223114
+ throw new CliError(`lecodes lightmap bake needs KTX-Software's toktx — the atlas ships as KTX2 (GPU-compressed, mipmapped).
223115
+ Install it from https://github.com/KhronosGroup/KTX-Software/releases (make sure toktx is on PATH) or set KTX_TOKTX=<path to toktx>.`);
223116
+ }
223117
+ return toktx;
223118
+ };
223119
+ var encodeKtx2 = (toktx, png2, ktx2) => {
223120
+ const args = ["--t2", "--encode", "uastc", "--uastc_quality", "2", "--zcmp", "18", "--genmipmap", "--assign_oetf", "srgb", ktx2, png2];
223121
+ const p2 = spawnSync6(toktx, args, { encoding: "utf8" });
223122
+ if (p2.status !== 0) {
223123
+ throw new CliError(`toktx failed (${p2.status ?? p2.error?.message ?? "?"}) while encoding ${ktx2}:
223124
+ ${(p2.stderr || p2.stdout || "").trim()}`);
223125
+ }
223126
+ };
223127
+ var bake = async (args) => {
223128
+ let root;
223129
+ try {
223130
+ root = findProjectRoot(process.cwd());
223131
+ } catch {
223132
+ root = process.cwd();
223133
+ }
223134
+ const toktx = requireToktx();
223135
+ const outDir = resolve13(root, flagStr(args, "out") ?? "assets/lightmap");
223136
+ const entryFlag = flagStr(args, "entry");
223137
+ const entryFile = entryFlag ?? "main.ts";
223138
+ if (!existsSync28(resolve13(root, entryFile))) {
223139
+ throw new CliError(`No ${entryFile} in ${root} — pass --entry <file> (the file that builds the scene and calls Lightmap.load).`);
223140
+ }
223141
+ const cfg = {
223142
+ outDir: outDir.replace(/\\/g, "/"),
223143
+ size: num(args, "size", 4096, 256, 16384),
223144
+ texel: num(args, "texel", 0.02, 0.001, 2),
223145
+ sunRays: num(args, "sun-rays", 32, 1, 1024),
223146
+ aoRays: num(args, "ao-rays", 64, 1, 4096),
223147
+ aoDistance: num(args, "ao-distance", 2, 0.01, 1000),
223148
+ bias: num(args, "bias", 0.02, 0.0001, 1),
223149
+ sunAngle: num(args, "sun-angle", 0.5, 0.01, 30)
223150
+ };
223151
+ const frames = num(args, "frames", 900, 30, 1e5);
223152
+ const win = readAppConfigAt(root)?.desktop?.window;
223153
+ const width = Number(flagStr(args, "width")) || win?.width || 1280;
223154
+ const height = Number(flagStr(args, "height")) || win?.height || 720;
223155
+ const logs = flagBool(args, "logs");
223156
+ const importPath = "./" + entryFile.replace(/\\/g, "/").replace(/\.tsx?$/, "");
223157
+ const entryPath = "/.lightmap-entry.ts";
223158
+ const entryText = `globalThis.__lecodesLightmap = ${JSON.stringify(cfg)}
223159
+ import ${JSON.stringify(importPath)}
223160
+ `;
223161
+ mkdirSync21(outDir, { recursive: true });
223162
+ ensurePlaceholders(outDir);
223163
+ const desktopRenderer = resolveDesktopRenderer(root, args);
223164
+ const { js, resources } = await buildBundle(args, undefined, {
223165
+ localAssets: true,
223166
+ desktopRenderer,
223167
+ extraEntries: [{ path: entryPath, type: "text", text: entryText }],
223168
+ entryOverride: entryPath
223169
+ });
223170
+ const scratch = mkdtempSync4(join30(tmpdir4(), "lecodes-lightmap-"));
223171
+ const outPng = join30(scratch, "frame.png");
223172
+ note(`Baking ${cfg.size}² at ${(cfg.texel * 100).toFixed(1)} cm texels (${cfg.sunRays} sun + ${cfg.aoRays} AO rays) → ${relative4(root, outDir) || "."}…`);
223173
+ let output = "";
223174
+ try {
223175
+ const timeoutMs = Number(flagStr(args, "timeout")) || 600000;
223176
+ const res = await runDesktopRender(js, resources, {
223177
+ renderer: desktopRenderer,
223178
+ width,
223179
+ height,
223180
+ frames,
223181
+ outPng,
223182
+ logs,
223183
+ timeoutMs,
223184
+ stopOnLine: (chunk) => chunk.includes("[lightmap] done") || chunk.includes("[lightmap] error")
223185
+ });
223186
+ output = res.output;
223187
+ } catch (e2) {
223188
+ const msg = e2 instanceof Error ? e2.message : String(e2);
223189
+ if (!/\[lightmap\] done/.test(msg))
223190
+ throw e2;
223191
+ output = msg;
223192
+ } finally {
223193
+ rmSync9(scratch, { recursive: true, force: true });
223194
+ }
223195
+ const lines = output.split(/\r?\n/).filter((l2) => l2.startsWith("[lightmap]"));
223196
+ for (const l2 of lines) {
223197
+ const text = l2.slice("[lightmap]".length).trim();
223198
+ if (text === "done")
223199
+ continue;
223200
+ logErr(text.startsWith("error") ? c.red(`✗ ${text}`) : c.dim(` ${text}`));
223201
+ }
223202
+ if (/\[lightmap\] done/.test(output)) {
223203
+ const png2 = join30(outDir, "lightmap.png");
223204
+ const ktx2 = join30(outDir, "lightmap.ktx2");
223205
+ if (!existsSync28(png2))
223206
+ throw new CliError(`The host reported a bake but ${png2} is missing.`);
223207
+ const t0 = Date.now();
223208
+ encodeKtx2(toktx, png2, ktx2);
223209
+ const pngKb = Math.round(statSync10(png2).size / 1024), ktxKb = Math.round(statSync10(ktx2).size / 1024);
223210
+ unlinkSync(png2);
223211
+ logErr(c.dim(` encoded lightmap.ktx2 (UASTC + mips, ${ktxKb} KB from a ${pngKb} KB raster) in ${((Date.now() - t0) / 1000).toFixed(1)} s`));
223212
+ logErr(c.green(`✓ baked → ${relative4(root, outDir) || "."}/lightmap.ktx2 + lightmap.bake`));
223213
+ logErr(c.dim(` apply it with Lightmap.load(scene, { data: asset('<rel>/${relative4(root, join30(outDir, "lightmap.bake")).replace(/\\/g, "/")}'), texture: asset('<rel>/${relative4(root, ktx2).replace(/\\/g, "/")}') }) — asset() paths are relative to the calling file`));
223214
+ return;
223215
+ }
223216
+ if (lines.some((l2) => /no bake module/.test(l2))) {
223217
+ throw new CliError("This desktop host has no bake module. Build packages/desktop (CREATOR_GL_LIGHTMAP is on there) and point LECODES_DESKTOP_EXE at build/lecodes-desktop.exe, or update: lecodes desktop update.");
223218
+ }
223219
+ if (lines.some((l2) => /error/.test(l2)))
223220
+ throw new CliError("The bake failed — see the lines above.");
223221
+ warnErr("The host ran to its frame cap without baking. Make sure the app calls Lightmap.load(...) after the static scene is built, and skips menus when Lightmap.baking is true (a title screen never reaches the scene). More load headroom: --frames <n>. Host output: --logs.");
223222
+ throw new CliError("Nothing was baked.");
223223
+ };
223224
+ var PLACEHOLDER_KTX2 = Buffer.from("q0tUWCAyMLsNChoKAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAaAAAACwAAACUAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAQAQAAAAAAABAAAAAAAAAAEAAAAAAAAAAsAAAAAAAAAAIAKACmAQIAAwMAABAAAAAAAAAAAAB/AAAAAAAAAAAA/////xEAAABLVFhvcmllbnRhdGlvbgByAAAAACcAAABLVFh3cml0ZXIAdG9rdHggdjQuNC4yIC8gbGlia3R4IHY0LjQuMgAAMwAAAEtUWHdyaXRlclNjUGFyYW1zAC0tZW5jb2RlIHVhc3RjIC0tdWFzdGNfcXVhbGl0eSAwAAD3////H3zvAQAAAAAAAAAA", "base64");
223225
+ var ensurePlaceholders = (outDir) => {
223226
+ const bake2 = join30(outDir, "lightmap.bake");
223227
+ const ktx2 = join30(outDir, "lightmap.ktx2");
223228
+ if (!existsSync28(bake2))
223229
+ writeFileSync22(bake2, JSON.stringify({ version: 1, placeholder: true, size: 1, texel: 0, instances: [] }) + `
223230
+ `);
223231
+ if (!existsSync28(ktx2))
223232
+ writeFileSync22(ktx2, PLACEHOLDER_KTX2);
223233
+ };
223234
+ var lightmap = async (args) => {
223235
+ const sub = args._[0];
223236
+ if (sub === "bake")
223237
+ return bake({ ...args, _: args._.slice(1) });
223238
+ throw new CliError("Usage: lecodes lightmap bake [--out assets/lightmap] [--size 4096] [--texel 0.02] [--sun-rays 32] [--ao-rays 64] [--ao-distance 2] [--bias 0.02] [--sun-angle 0.5] [--frames 900] [--entry main.ts] [--logs] (needs KTX-Software's toktx)");
223239
+ };
223240
+
222147
223241
  // src/commands/login.ts
222148
223242
  init_api();
222149
223243
  init_config();
@@ -222161,8 +223255,9 @@ var login = async (args) => {
222161
223255
 
222162
223256
  // src/commands/shaders.ts
222163
223257
  init_manifest();
222164
- import { readFileSync as readFileSync23 } from "node:fs";
223258
+ import { readFileSync as readFileSync25 } from "node:fs";
222165
223259
  init_util();
223260
+ init_compile();
222166
223261
  var collectShaders = () => {
222167
223262
  let root;
222168
223263
  let manifest;
@@ -222188,8 +223283,27 @@ var status2 = async () => {
222188
223283
  log(`shaders ${c.dim(`(compiled = up-to-date artifacts in ${shadersCacheDir(root)})`)}`);
222189
223284
  const width = Math.max(...shaders.map((s2) => s2.path.length));
222190
223285
  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`");
223286
+ let state;
223287
+ if (matcId === null)
223288
+ state = c.dim("unknown (matc not downloaded)");
223289
+ else {
223290
+ const text = readFileSync25(shader.absPath, "utf8");
223291
+ if (shaderIsCompiled(root, text, matcId))
223292
+ state = "compiled";
223293
+ else {
223294
+ const present = shaderCompiledPlatforms(root, text, matcId);
223295
+ state = present.length === 0 ? c.yellow("not compiled — run `lecodes shaders compile`") : c.yellow(`partial (${present.join(", ")}) — \`lecodes shaders compile\` for the rest`);
223296
+ }
223297
+ }
222192
223298
  log(` ${shader.path.padEnd(width)} ${state}`);
223299
+ const schema = parseShaderSchema(readFileSync25(shader.absPath, "utf8"));
223300
+ for (const p2 of schema.params) {
223301
+ const range = p2.min !== undefined ? ` [${p2.min}..${p2.max}]` : "";
223302
+ const def = p2.default !== null && p2.default !== undefined ? ` = ${JSON.stringify(p2.default)}` : "";
223303
+ log(` ${c.dim(p2.type.padEnd(10))} ${p2.name}${c.dim(` ${p2.editor}${range}${def}`)}${p2.tooltip ? c.dim(` — ${p2.tooltip}`) : ""}`);
223304
+ }
223305
+ for (const e2 of schema.errors)
223306
+ log(` ${c.yellow(e2)}`);
222193
223307
  }
222194
223308
  }
222195
223309
  try {
@@ -222201,7 +223315,23 @@ var status2 = async () => {
222201
223315
  log(`published ${c.dim(e2 instanceof CliError ? e2.message : "unknown (couldn't reach the release manifest)")}`);
222202
223316
  }
222203
223317
  };
222204
- var compile3 = async () => {
223318
+ var parsePlatforms = (args) => {
223319
+ const raw = flagStr(args, "platform", "platforms");
223320
+ if (raw === undefined)
223321
+ return;
223322
+ const known = allShaderPlatforms();
223323
+ const asked = raw.split(",").map((p2) => p2.trim()).filter(Boolean);
223324
+ if (asked.length === 0)
223325
+ throw new CliError(`--platform needs at least one target. Known: ${known.join(", ")}`);
223326
+ const bad = asked.filter((p2) => !known.includes(p2));
223327
+ if (bad.length > 0) {
223328
+ throw new CliError(`Unknown shader platform${bad.length > 1 ? "s" : ""}: ${bad.join(", ")}
223329
+ Known: ${known.join(", ")}`);
223330
+ }
223331
+ return asked;
223332
+ };
223333
+ var compile3 = async (args) => {
223334
+ const platforms = parsePlatforms(args);
222205
223335
  const { root, entries, shaders } = collectShaders();
222206
223336
  if (shaders.length === 0) {
222207
223337
  note("No .mat shaders in this project.");
@@ -222209,11 +223339,11 @@ var compile3 = async () => {
222209
223339
  }
222210
223340
  const matcExe = await resolveMatcExe();
222211
223341
  const matcId = matcIdentity(matcExe);
222212
- const wasCompiled = new Map(shaders.map((s2) => [s2.path, shaderIsCompiled(root, readFileSync23(s2.absPath, "utf8"), matcId)]));
223342
+ const wasCompiled = new Map(shaders.map((s2) => [s2.path, shaderIsCompiled(root, readFileSync25(s2.absPath, "utf8"), matcId)]));
222213
223343
  const errors2 = [];
222214
223344
  for (const shader of shaders) {
222215
223345
  try {
222216
- const { warnings } = await applyLocalShaders([shader], { root, local: true });
223346
+ const { warnings } = await applyLocalShaders([shader], { root, local: true, platforms });
222217
223347
  for (const w of warnings)
222218
223348
  warnErr(w);
222219
223349
  log(` ${shader.path} ${wasCompiled.get(shader.path) ? c.dim("compiled (cached)") : "compiled"}`);
@@ -222228,22 +223358,23 @@ var compile3 = async () => {
222228
223358
  ${c.red("✗")} ${e2}`);
222229
223359
  if (errors2.length > 0)
222230
223360
  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)}`);
223361
+ const scope = platforms ? ` [${platforms.join(", ")}]` : "";
223362
+ note(`${shaders.length} shader${shaders.length === 1 ? "" : "s"} compiled${scope} → ${shadersCacheDir(root)}`);
222232
223363
  };
222233
223364
  var shaders = async (args) => {
222234
223365
  const sub = args._[0];
222235
223366
  if (sub === undefined)
222236
223367
  return status2();
222237
223368
  if (sub === "compile")
222238
- return compile3();
223369
+ return compile3(args);
222239
223370
  if (sub === "update")
222240
223371
  return updateMatc();
222241
- throw new CliError(`Unknown subcommand "shaders ${sub}". Use \`lecodes shaders\` (status), \`lecodes shaders compile\` or \`lecodes shaders update\`.`);
223372
+ throw new CliError(`Unknown subcommand "shaders ${sub}". Use \`lecodes shaders\` (status), \`lecodes shaders compile [--platform a,b]\` or \`lecodes shaders update\`.`);
222242
223373
  };
222243
223374
 
222244
223375
  // src/commands/render.ts
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";
223376
+ import { mkdirSync as mkdirSync22, readFileSync as readFileSync27, writeFileSync as writeFileSync23 } from "node:fs";
223377
+ import { dirname as dirname15, resolve as resolve16 } from "node:path";
222247
223378
 
222248
223379
  // src/compile/headlessBundle.ts
222249
223380
  init_api();
@@ -222254,7 +223385,7 @@ init_fonts();
222254
223385
  init_assetIcons();
222255
223386
  init_util();
222256
223387
  var LOCAL_ASSET_SCHEME2 = "lecodesfile://";
222257
- var compileHeadlessBundle = async (args) => {
223388
+ var compileHeadlessBundle = async (args, opts = {}) => {
222258
223389
  let root;
222259
223390
  let manifest;
222260
223391
  try {
@@ -222289,7 +223420,9 @@ var compileHeadlessBundle = async (args) => {
222289
223420
  warnings.push("This is a local project (no server) — assets always load from the working tree.");
222290
223421
  }
222291
223422
  }
222292
- const compileEntries = useLocalAssets ? entries.map((e2) => e2.type === "resource" && e2.absPath ? { ...e2, fileSrc: encodeURIComponent(e2.absPath) } : e2) : entries;
223423
+ const compileEntries = useLocalAssets ? entries.map((e2) => e2.type === "resource" && e2.absPath ? { ...e2, fileSrc: encodeURIComponent(e2.absPath) } : e2) : [...entries];
223424
+ if (opts.screen)
223425
+ compileEntries.push(opts.screen.entry);
222293
223426
  await applyProjectFonts(compileEntries, useLocalAssets ? {
222294
223427
  mode: "vendor",
222295
223428
  root,
@@ -222299,7 +223432,7 @@ var compileHeadlessBundle = async (args) => {
222299
223432
  } : { mode: "cdn", root });
222300
223433
  await applyAppAssetIcons(compileEntries);
222301
223434
  note("Compiling…");
222302
- const entryFlag = flagStr(args, "entry");
223435
+ const entryFlag = opts.screen?.entryPath ?? flagStr(args, "entry");
222303
223436
  if (!entryFlag) {
222304
223437
  const ambiguous = ambiguousEntryWarning(compileEntries);
222305
223438
  if (ambiguous)
@@ -222322,12 +223455,84 @@ var compileHeadlessBundle = async (args) => {
222322
223455
  return { js, root, warnings, useLocalAssets };
222323
223456
  };
222324
223457
 
223458
+ // src/compile/screenEntry.ts
223459
+ init_designMeta();
223460
+ init_util();
223461
+ import { existsSync as existsSync29, readFileSync as readFileSync26, statSync as statSync11 } from "node:fs";
223462
+ import { isAbsolute, join as join31, relative as relative5, resolve as resolve14 } from "node:path";
223463
+ var SCREEN_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs"];
223464
+ var SCREEN_ENTRY_PATH = "/.screen-entry.ts";
223465
+ var DEFAULT_DESIGN_DIR = "design";
223466
+ var screenArg = (args) => args._[0];
223467
+ var hasSeparator = (file) => file.includes("/") || file.includes("\\");
223468
+ var findScreenFile = (root, file, dirFlag) => {
223469
+ const candidates = [resolve14(process.cwd(), file)];
223470
+ if (!hasSeparator(file)) {
223471
+ candidates.push(join31(root, (dirFlag ?? DEFAULT_DESIGN_DIR).replace(/[/\\]+$/, ""), "screens", file));
223472
+ }
223473
+ for (const base of candidates) {
223474
+ if (existsSync29(base) && statSync11(base).isFile())
223475
+ return base;
223476
+ for (const ext2 of SCREEN_EXTS)
223477
+ if (existsSync29(base + ext2))
223478
+ return base + ext2;
223479
+ }
223480
+ throw new CliError(`No screen file at "${file}".` + (hasSeparator(file) ? "" : ` Pass a path, or a screen id that exists in ${dirFlag ?? DEFAULT_DESIGN_DIR}/screens/.`));
223481
+ };
223482
+ var designDirOf = (root, key) => {
223483
+ const m = /^\/(.*)screens\/([^/]+)\.[^./]+$/.exec(key);
223484
+ if (!m)
223485
+ return null;
223486
+ const dir = join31(root, m[1]);
223487
+ return existsSync29(join31(dir, "meta.json")) ? { dir, id: m[2] } : null;
223488
+ };
223489
+ var resolveScreenTarget = (root, file, opts = {}) => {
223490
+ const abs = findScreenFile(root, file, opts.dir);
223491
+ const rel = relative5(root, abs);
223492
+ if (rel.startsWith("..") || isAbsolute(rel)) {
223493
+ throw new CliError(`"${file}" is outside the project (${root}) — a screen renders as part of its project, so its ` + `imports and asset() files resolve from the same tree.`);
223494
+ }
223495
+ const key = toKey(rel);
223496
+ if (!/^\s*export\s+default\b/m.test(readFileSync26(abs, "utf8"))) {
223497
+ throw new CliError(`${key.slice(1)} has no default export. A screen file ends with ` + `\`export default UIScreen([...])\` — or \`export default (state) => UIScreen([...])\` for screen states.`);
223498
+ }
223499
+ const invoke = opts.state !== undefined ? `screen(${JSON.stringify(opts.state)})` : "screen()";
223500
+ const spec = "." + key.replace(/\.[^./]+$/, "");
223501
+ const design2 = designDirOf(root, key);
223502
+ return {
223503
+ entryPath: SCREEN_ENTRY_PATH,
223504
+ entry: {
223505
+ path: SCREEN_ENTRY_PATH,
223506
+ type: "text",
223507
+ text: `import screen from ${JSON.stringify(spec)}
223508
+ ` + `const __screen = typeof screen === "function" ? ${invoke} : screen
223509
+ ` + `__screen.open()
223510
+ `
223511
+ },
223512
+ frame: design2 ? screenFrame(design2.dir, design2.id) : undefined,
223513
+ label: (design2 ? design2.id : key.slice(1)) + (opts.state !== undefined ? `@${opts.state}` : "")
223514
+ };
223515
+ };
223516
+ var screenTargetFromArgs = (root, args) => {
223517
+ const file = screenArg(args);
223518
+ if (file === undefined) {
223519
+ if (flagStr(args, "state") !== undefined) {
223520
+ throw new CliError("--state picks a screen's state — name the screen file too: lecodes render <file> --state <name>");
223521
+ }
223522
+ return;
223523
+ }
223524
+ if (flagStr(args, "entry") !== undefined) {
223525
+ throw new CliError("Use either a screen file or --entry, not both — each names the module to compile from.");
223526
+ }
223527
+ return resolveScreenTarget(root, file, { state: flagStr(args, "state"), dir: flagStr(args, "dir") });
223528
+ };
223529
+
222325
223530
  // src/commands/render.ts
222326
223531
  init_manifest();
222327
223532
 
222328
223533
  // src/desktopScript.ts
222329
223534
  init_util();
222330
- import { resolve as resolve12 } from "node:path";
223535
+ import { resolve as resolve15 } from "node:path";
222331
223536
  var isPoint = (v) => typeof v === "object" && v !== null && typeof v.x === "number" && typeof v.y === "number";
222332
223537
  var FRAME_MS = 16;
222333
223538
  var ACTIONS = ["tap", "type", "fill", "scroll", "wait", "frames", "drag", "hold", "key", "keyDown", "keyUp", "look", "mouseTo", "wheel", "stick", "gamepad", "camera", "view"];
@@ -222355,7 +223560,7 @@ var translateScenario = (rawSteps, opts) => {
222355
223560
  }
222356
223561
  throw new CliError(`${at}: "${key}" must be { "x", "y" }.`);
222357
223562
  };
222358
- const num = (at, key, v, def) => {
223563
+ const num2 = (at, key, v, def) => {
222359
223564
  if (v === undefined && def !== undefined)
222360
223565
  return def;
222361
223566
  if (typeof v !== "number")
@@ -222382,7 +223587,7 @@ var translateScenario = (rawSteps, opts) => {
222382
223587
  }
222383
223588
  if (action === "view") {
222384
223589
  const v = s2.view;
222385
- const fov = s2.fov !== undefined ? num(at, "fov", s2.fov) : undefined;
223590
+ const fov = s2.fov !== undefined ? num2(at, "fov", s2.fov) : undefined;
222386
223591
  if (v === "off") {
222387
223592
  plan.lines.push(emit("view off"));
222388
223593
  inspecting = false;
@@ -222402,7 +223607,7 @@ var translateScenario = (rawSteps, opts) => {
222402
223607
  parts.push("camera off");
222403
223608
  } else if (typeof cam === "object" && cam !== null && isVec3(cam.at) && isVec3(cam.lookAt)) {
222404
223609
  const { at: eye, lookAt, fov } = cam;
222405
- const fovN = fov !== undefined ? num(at, "camera.fov", fov) : undefined;
223610
+ const fovN = fov !== undefined ? num2(at, "camera.fov", fov) : undefined;
222406
223611
  plan.lines.push(emit(`camera ${eye.join(" ")} ${lookAt.join(" ")}${fovN !== undefined ? ` ${fovN}` : ""}`));
222407
223612
  inspecting = true;
222408
223613
  parts.push(`camera at (${eye.join(",")}) → (${lookAt.join(",")})`);
@@ -222419,20 +223624,20 @@ var translateScenario = (rawSteps, opts) => {
222419
223624
  parts.push(`tap ${pt(p2)}`);
222420
223625
  } else if (action === "drag") {
222421
223626
  const a2 = coord(at, "drag", s2.drag), b = coord(at, "to", s2.to);
222422
- const ms = num(at, "ms", s2.ms, 300);
223627
+ const ms = num2(at, "ms", s2.ms, 300);
222423
223628
  plan.lines.push(emit(`drag ${a2.x} ${a2.y} ${b.x} ${b.y} ${Math.round(ms)}`));
222424
223629
  budgetMs += ms + 4 * FRAME_MS;
222425
223630
  parts.push(`drag ${pt(a2)} → ${pt(b)} over ${ms}ms`);
222426
223631
  } else if (action === "hold") {
222427
223632
  const p2 = coord(at, "hold", s2.hold);
222428
- const ms = num(at, "ms", s2.ms);
223633
+ const ms = num2(at, "ms", s2.ms);
222429
223634
  plan.lines.push(emit(`hold ${p2.x} ${p2.y} ${Math.round(ms)}`));
222430
223635
  budgetMs += ms + 4 * FRAME_MS;
222431
223636
  parts.push(`hold ${pt(p2)} ${ms}ms`);
222432
223637
  } else if (action === "key") {
222433
223638
  if (typeof s2.key !== "string")
222434
223639
  throw new CliError(`${at}: "key" must be a KeyboardEvent.code string.`);
222435
- const ms = num(at, "ms", s2.ms);
223640
+ const ms = num2(at, "ms", s2.ms);
222436
223641
  plan.lines.push(emit(`key ${s2.key} ${Math.round(ms)}`));
222437
223642
  budgetMs += ms + 2 * FRAME_MS;
222438
223643
  parts.push(`key ${s2.key} ${ms}ms`);
@@ -222447,7 +223652,7 @@ var translateScenario = (rawSteps, opts) => {
222447
223652
  const v = s2.look;
222448
223653
  if (!Array.isArray(v) || v.length !== 2 || !v.every((n2) => typeof n2 === "number"))
222449
223654
  throw new CliError(`${at}: "look" must be [dx, dy] (logical px of mouse motion).`);
222450
- const ms = num(at, "ms", s2.ms, FRAME_MS);
223655
+ const ms = num2(at, "ms", s2.ms, FRAME_MS);
222451
223656
  plan.lines.push(emit(`look ${v[0]} ${v[1]} ${Math.round(ms)}`));
222452
223657
  budgetMs += ms + 2 * FRAME_MS;
222453
223658
  parts.push(`look ${v[0]},${v[1]} over ${ms}ms`);
@@ -222493,12 +223698,12 @@ var translateScenario = (rawSteps, opts) => {
222493
223698
  budgetMs += 2 * FRAME_MS;
222494
223699
  parts.push(`gamepad ${s2.gamepad} #${pad}`);
222495
223700
  } else if (action === "wait") {
222496
- const ms = num(at, "wait", s2.wait);
223701
+ const ms = num2(at, "wait", s2.wait);
222497
223702
  plan.lines.push(emit(`wait ${Math.round(ms)}`));
222498
223703
  budgetMs += ms + FRAME_MS;
222499
223704
  parts.push(`wait ${ms}ms`);
222500
223705
  } else if (action === "frames") {
222501
- const n2 = num(at, "frames", s2.frames);
223706
+ const n2 = num2(at, "frames", s2.frames);
222502
223707
  plan.lines.push(emit(`frames ${Math.round(n2)}`));
222503
223708
  budgetMs += n2 * FRAME_MS + FRAME_MS;
222504
223709
  parts.push(`advance ${n2} frames`);
@@ -222528,7 +223733,7 @@ var translateScenario = (rawSteps, opts) => {
222528
223733
  if (s2.screenshot !== undefined) {
222529
223734
  if (typeof s2.screenshot !== "string")
222530
223735
  throw new CliError(`${at}: "screenshot" must be a path.`);
222531
- plan.screenshot = resolve12(opts.outDir, s2.screenshot);
223736
+ plan.screenshot = resolve15(opts.outDir, s2.screenshot);
222532
223737
  plan.lines.push(emit(`frames 2`));
222533
223738
  plan.lines.push(emit(`shot ${plan.screenshot}`));
222534
223739
  budgetMs += 3 * FRAME_MS;
@@ -222637,9 +223842,9 @@ var requireRenderer = async () => {
222637
223842
  };
222638
223843
  var fileScenarioIO = (dir) => ({
222639
223844
  write: (relPath, data) => {
222640
- const abs = resolve13(dir, relPath);
222641
- mkdirSync20(dirname15(abs), { recursive: true });
222642
- writeFileSync21(abs, data);
223845
+ const abs = resolve16(dir, relPath);
223846
+ mkdirSync22(dirname15(abs), { recursive: true });
223847
+ writeFileSync23(abs, data);
222643
223848
  }
222644
223849
  });
222645
223850
  var resolveFixtures = (value, baseDir) => {
@@ -222647,9 +223852,9 @@ var resolveFixtures = (value, baseDir) => {
222647
223852
  return;
222648
223853
  let map = value;
222649
223854
  if (typeof value === "string") {
222650
- const abs = resolve13(baseDir, value);
223855
+ const abs = resolve16(baseDir, value);
222651
223856
  try {
222652
- map = JSON.parse(readFileSync24(abs, "utf8"));
223857
+ map = JSON.parse(readFileSync27(abs, "utf8"));
222653
223858
  } catch (e2) {
222654
223859
  throw new CliError(`Can't read fixtures "${value}": ${e2 instanceof Error ? e2.message : String(e2)}`);
222655
223860
  }
@@ -222662,7 +223867,7 @@ var resolveFixtures = (value, baseDir) => {
222662
223867
  var readScenarioFile = (path2) => {
222663
223868
  let raw;
222664
223869
  try {
222665
- raw = JSON.parse(readFileSync24(resolve13(path2), "utf8"));
223870
+ raw = JSON.parse(readFileSync27(resolve16(path2), "utf8"));
222666
223871
  } catch (e2) {
222667
223872
  throw new CliError(`Can't read scenario "${path2}": ${e2 instanceof Error ? e2.message : String(e2)}`);
222668
223873
  }
@@ -222697,10 +223902,11 @@ var renderDesktop = async (args) => {
222697
223902
  root = process.cwd();
222698
223903
  }
222699
223904
  const win = readAppConfigAt(root)?.desktop?.window;
223905
+ const screen2 = screenTargetFromArgs(root, args);
222700
223906
  const scriptFlag = flagStr(args, "script");
222701
223907
  const scenario = scriptFlag !== undefined ? readScenarioFile(scriptFlag) : null;
222702
- const width = Number(flagStr(args, "width")) || scenario?.width || win?.width || 390;
222703
- const height = Number(flagStr(args, "height")) || scenario?.height || win?.height || 844;
223908
+ const width = Number(flagStr(args, "width")) || scenario?.width || screen2?.frame?.[0] || win?.width || 390;
223909
+ const height = Number(flagStr(args, "height")) || scenario?.height || screen2?.frame?.[1] || win?.height || 844;
222704
223910
  const frames = Number(flagStr(args, "frames")) || 60;
222705
223911
  const pngFlag = flagStr(args, "png") ?? "screenshot.png";
222706
223912
  const logs = flagBool(args, "logs");
@@ -222711,17 +223917,24 @@ var renderDesktop = async (args) => {
222711
223917
  const { camera, view } = resolveInspectionCamera(args);
222712
223918
  if ((camera || view) && scenario)
222713
223919
  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.');
222714
- const { js, resources } = await buildBundle(args, undefined, { localAssets: true });
222715
- mkdirSync20(dirname15(resolve13(pngFlag)), { recursive: true });
223920
+ const desktopRenderer = resolveDesktopRenderer(root, args);
223921
+ const { js, resources } = await buildBundle(args, undefined, {
223922
+ localAssets: true,
223923
+ desktopRenderer,
223924
+ extraEntries: screen2 ? [screen2.entry] : undefined,
223925
+ entryOverride: screen2?.entryPath
223926
+ });
223927
+ mkdirSync22(dirname15(resolve16(pngFlag)), { recursive: true });
222716
223928
  if (scenario) {
222717
223929
  const outDir = flagStr(args, "out-dir") ?? ".";
222718
223930
  const plan = translateScenario(scenario.steps, { outDir, headroomFrames: frames });
222719
223931
  for (const st of plan.steps)
222720
223932
  if (st.screenshot)
222721
- mkdirSync20(dirname15(st.screenshot), { recursive: true });
223933
+ mkdirSync22(dirname15(st.screenshot), { recursive: true });
222722
223934
  const timeoutMs2 = Number(flagStr(args, "timeout")) || 60000 + plan.steps.length * 5000;
222723
223935
  note(`Running ${plan.steps.length} step${plan.steps.length === 1 ? "" : "s"} (native)…`);
222724
223936
  const { output } = await runDesktopRender(js, resources, {
223937
+ renderer: desktopRenderer,
222725
223938
  width,
222726
223939
  height,
222727
223940
  frames: plan.frameCap,
@@ -222751,8 +223964,9 @@ var renderDesktop = async (args) => {
222751
223964
  process.exit(result.ok ? 0 : 1);
222752
223965
  }
222753
223966
  const timeoutMs = Number(flagStr(args, "timeout")) || 60000;
222754
- note("Rendering (native)…");
223967
+ note(screen2 ? `Rendering ${screen2.label} (native)…` : "Rendering (native)…");
222755
223968
  await runDesktopRender(js, resources, {
223969
+ renderer: desktopRenderer,
222756
223970
  width,
222757
223971
  height,
222758
223972
  frames,
@@ -222798,6 +224012,13 @@ var render = async (args) => {
222798
224012
  return renderDesktop(args);
222799
224013
  const scriptFlag = flagStr(args, "script");
222800
224014
  const scenario = scriptFlag !== undefined ? readScenarioFile(scriptFlag) : null;
224015
+ let projectRoot2;
224016
+ try {
224017
+ projectRoot2 = findProjectRoot(process.cwd());
224018
+ } catch {
224019
+ projectRoot2 = process.cwd();
224020
+ }
224021
+ const screen2 = screenTargetFromArgs(projectRoot2, args);
222801
224022
  const stepCount = scenario && Array.isArray(scenario.steps) ? scenario.steps.length : 0;
222802
224023
  const timeoutMs = Number(flagStr(args, "timeout")) || 15000 + stepCount * 3000;
222803
224024
  const killer = setTimeout(() => {
@@ -222805,15 +224026,15 @@ var render = async (args) => {
222805
224026
  process.exit(1);
222806
224027
  }, timeoutMs);
222807
224028
  killer.unref?.();
222808
- const { js, warnings, useLocalAssets, root } = await compileHeadlessBundle(args);
224029
+ const { js, warnings, useLocalAssets, root } = await compileHeadlessBundle(args, { screen: screen2 });
222809
224030
  const renderer = await requireRenderer();
222810
224031
  const projectViewport = desktopDefaultViewport(root);
222811
- const width = Number(flagStr(args, "width")) || scenario?.width || projectViewport?.width || 390;
222812
- const height = Number(flagStr(args, "height")) || scenario?.height || projectViewport?.height || 844;
224032
+ const width = Number(flagStr(args, "width")) || scenario?.width || screen2?.frame?.[0] || projectViewport?.width || 390;
224033
+ const height = Number(flagStr(args, "height")) || scenario?.height || screen2?.frame?.[1] || projectViewport?.height || 844;
222813
224034
  const settleStr = flagStr(args, "settle") ?? (scenario?.settle !== undefined ? String(scenario.settle) : undefined);
222814
224035
  const timeStr = flagStr(args, "time");
222815
224036
  const onConsole = flagBool(args, "logs") ? (level, text2) => logErr(`${c.dim(`[${level}]`)} ${text2}`) : undefined;
222816
- const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname15(resolve13(scriptFlag)) : ".");
224037
+ const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname15(resolve16(scriptFlag)) : ".");
222817
224038
  const flagFixtures = resolveFixtures(flagStr(args, "fixtures"), ".");
222818
224039
  const fixtures = envelopeFixtures || flagFixtures ? { ...envelopeFixtures, ...flagFixtures } : undefined;
222819
224040
  if (fixtures) {
@@ -222852,7 +224073,7 @@ var render = async (args) => {
222852
224073
  ${c.red(st.error ?? "failed")}`);
222853
224074
  }
222854
224075
  if (!result2.ok && result2.failureArtifacts.length > 0) {
222855
- logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p2) => resolve13(outDir, p2)).join(", ")}`));
224076
+ logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p2) => resolve16(outDir, p2)).join(", ")}`));
222856
224077
  }
222857
224078
  logErr(result2.ok ? c.green(`✓ ${result2.steps.length}/${steps.length} steps passed`) : c.red(`✗ failed at step ${result2.steps.length} of ${steps.length}`));
222858
224079
  return done((cb) => cb(), result2.ok ? 0 : 1);
@@ -222862,7 +224083,7 @@ var render = async (args) => {
222862
224083
  }
222863
224084
  const pngFlag = flagStr(args, "png") ?? (flagBool(args, "png") ? "screenshot.png" : undefined);
222864
224085
  if (pngFlag !== undefined) {
222865
- note("Rendering PNG…");
224086
+ note(screen2 ? `Rendering ${screen2.label} (PNG)…` : "Rendering PNG…");
222866
224087
  const clip = parseClip(flagStr(args, "clip"));
222867
224088
  const { png: png2, width: outW, height: outH, warnings: pngWarnings } = await renderer.renderToPng(js, {
222868
224089
  width,
@@ -222876,12 +224097,13 @@ var render = async (args) => {
222876
224097
  if (isNullSink(pngFlag)) {
222877
224098
  note(`Discarded PNG (${pngFlag})`);
222878
224099
  } else {
222879
- writeFileSync21(resolve13(pngFlag), png2);
224100
+ mkdirSync22(dirname15(resolve16(pngFlag)), { recursive: true });
224101
+ writeFileSync23(resolve16(pngFlag), png2);
222880
224102
  note(`Wrote ${pngFlag} (${png2.length} bytes, ${outW}x${outH}${clip !== undefined ? ", clipped" : ""})`);
222881
224103
  }
222882
224104
  return done((cb) => cb());
222883
224105
  }
222884
- note("Rendering…");
224106
+ note(screen2 ? `Rendering ${screen2.label}…` : "Rendering…");
222885
224107
  const result = await renderer.renderToJson(js, { width, height, ...timing });
222886
224108
  for (const w of [...warnings, ...result.warnings])
222887
224109
  warnErr(w);
@@ -222891,7 +224113,7 @@ var render = async (args) => {
222891
224113
  return done((cb) => cb());
222892
224114
  }
222893
224115
  if (out) {
222894
- writeFileSync21(resolve13(out), result.text);
224116
+ writeFileSync23(resolve16(out), result.text);
222895
224117
  note(`Wrote ${out} (${result.text.length} bytes)`);
222896
224118
  return done((cb) => cb());
222897
224119
  }
@@ -222906,8 +224128,8 @@ init_api();
222906
224128
  init_config();
222907
224129
  init_manifest();
222908
224130
  init_util();
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";
224131
+ import { appendFileSync as appendFileSync2, existsSync as existsSync30, readFileSync as readFileSync28, writeFileSync as writeFileSync24 } from "node:fs";
224132
+ import { join as join32 } from "node:path";
222911
224133
  var ENV_VAR = "LECODES_PUSH_KEY";
222912
224134
  var context = () => {
222913
224135
  const root = findProjectRoot(process.cwd());
@@ -223051,11 +224273,11 @@ var pnKeysList = async (ctx) => {
223051
224273
  };
223052
224274
  var writeEnv = (root, key) => {
223053
224275
  const touched = [];
223054
- const envPath = join28(root, ".env");
224276
+ const envPath = join32(root, ".env");
223055
224277
  const line = `${ENV_VAR}=${key}`;
223056
- const existing = existsSync27(envPath) ? readFileSync25(envPath, "utf8") : "";
224278
+ const existing = existsSync30(envPath) ? readFileSync28(envPath, "utf8") : "";
223057
224279
  if (new RegExp(`^${ENV_VAR}=`, "m").test(existing)) {
223058
- writeFileSync22(envPath, existing.replace(new RegExp(`^${ENV_VAR}=.*$`, "m"), line));
224280
+ writeFileSync24(envPath, existing.replace(new RegExp(`^${ENV_VAR}=.*$`, "m"), line));
223059
224281
  } else {
223060
224282
  const separator = !existing || existing.endsWith(`
223061
224283
  `) ? "" : `
@@ -223064,8 +224286,8 @@ var writeEnv = (root, key) => {
223064
224286
  `);
223065
224287
  }
223066
224288
  touched.push(".env");
223067
- const ignorePath = join28(root, ".gitignore");
223068
- const ignore = existsSync27(ignorePath) ? readFileSync25(ignorePath, "utf8") : "";
224289
+ const ignorePath = join32(root, ".gitignore");
224290
+ const ignore = existsSync30(ignorePath) ? readFileSync28(ignorePath, "utf8") : "";
223069
224291
  if (!ignore.split(`
223070
224292
  `).some((l2) => l2.trim() === ".env")) {
223071
224293
  const separator = !ignore || ignore.endsWith(`
@@ -223164,8 +224386,8 @@ var pn = async (args) => {
223164
224386
  init_api();
223165
224387
  init_config();
223166
224388
  init_manifest();
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";
224389
+ import { existsSync as existsSync31, mkdirSync as mkdirSync23, rmSync as rmSync10, writeFileSync as writeFileSync25 } from "node:fs";
224390
+ import { dirname as dirname16, join as join33 } from "node:path";
223169
224391
  init_types4();
223170
224392
  init_localFiles();
223171
224393
  init_util();
@@ -223188,9 +224410,9 @@ var pull = async (args) => {
223188
224410
  const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a2) => a2.id === id)?.type !== "folder").map(([, path2]) => path2));
223189
224411
  for (const path2 of Object.keys(manifest.files)) {
223190
224412
  if (!remotePaths.has(path2)) {
223191
- const abs = join29(root, ...path2.replace(/^\//, "").split("/"));
223192
- if (existsSync28(abs))
223193
- rmSync8(abs);
224413
+ const abs = join33(root, ...path2.replace(/^\//, "").split("/"));
224414
+ if (existsSync31(abs))
224415
+ rmSync10(abs);
223194
224416
  }
223195
224417
  }
223196
224418
  const files = await materializeAssets(root, project, apiUrl, token, progress);
@@ -223227,18 +224449,18 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
223227
224449
  const files = { ...manifest.files };
223228
224450
  for (const asset of targets) {
223229
224451
  const key = pathById.get(asset.id);
223230
- const abs = join29(root, ...key.replace(/^\//, "").split("/"));
223231
- mkdirSync21(dirname16(abs), { recursive: true });
224452
+ const abs = join33(root, ...key.replace(/^\//, "").split("/"));
224453
+ mkdirSync23(dirname16(abs), { recursive: true });
223232
224454
  let sha;
223233
224455
  if (asset.type === "file") {
223234
224456
  if (!asset.file?.src)
223235
224457
  continue;
223236
224458
  const bytes = await downloadBlob(apiUrl, asset.file.src);
223237
- writeFileSync23(abs, bytes);
224459
+ writeFileSync25(abs, bytes);
223238
224460
  sha = sha256(bytes);
223239
224461
  } else {
223240
224462
  const text = asset.text ?? "";
223241
- writeFileSync23(abs, text);
224463
+ writeFileSync25(abs, text);
223242
224464
  sha = sha256(text);
223243
224465
  }
223244
224466
  files[key] = { id: asset.id, type: kindOf3(asset.type), sha };
@@ -223251,8 +224473,8 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
223251
224473
 
223252
224474
  // src/commands/scene.ts
223253
224475
  init_manifest();
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";
224476
+ import { existsSync as existsSync33, readFileSync as readFileSync30, readdirSync as readdirSync14, statSync as statSync12, writeFileSync as writeFileSync27 } from "node:fs";
224477
+ import { basename as basename16, dirname as dirname17, join as join35, resolve as resolve17 } from "node:path";
223256
224478
 
223257
224479
  // src/compile/sceneCompile.ts
223258
224480
  init_localFiles();
@@ -223260,18 +224482,19 @@ init_ignore();
223260
224482
  init_distRoot();
223261
224483
  init_util();
223262
224484
  init_projectCompile();
223263
- import { existsSync as existsSync29, readFileSync as readFileSync26 } from "node:fs";
223264
- import { join as join30 } from "node:path";
224485
+ import { createHash as createHash8 } from "node:crypto";
224486
+ import { existsSync as existsSync32, mkdirSync as mkdirSync24, readFileSync as readFileSync29, writeFileSync as writeFileSync26 } from "node:fs";
224487
+ import { join as join34 } from "node:path";
223265
224488
  var TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i;
223266
224489
  var rel = (key) => key.replace(/^\//, "");
223267
224490
  var resolveHarness = (loadSceneHarness2) => {
223268
- const inRepo = join30(distRoot, "..", "projects", "src", "scene-editor");
224491
+ const inRepo = join34(distRoot, "..", "projects", "src", "scene-editor");
223269
224492
  try {
223270
- return loadSceneHarness2(existsSync29(inRepo) ? inRepo : undefined);
224493
+ return loadSceneHarness2(existsSync32(inRepo) ? inRepo : undefined);
223271
224494
  } catch {
223272
- const vendored = join30(distRoot, "runtime", "scene-harness.json");
223273
- if (existsSync29(vendored))
223274
- return JSON.parse(readFileSync26(vendored, "utf8"));
224495
+ const vendored = join34(distRoot, "runtime", "scene-harness.json");
224496
+ if (existsSync32(vendored))
224497
+ return JSON.parse(readFileSync29(vendored, "utf8"));
223275
224498
  throw new CliError("scene-editor harness sources not found — reinstall the CLI or set LECODES_SCENE_HARNESS_DIR.");
223276
224499
  }
223277
224500
  };
@@ -223283,11 +224506,32 @@ var compileSceneEditor = async (opts) => {
223283
224506
  for (const file of scan2.files) {
223284
224507
  const path2 = "./" + rel(file.path);
223285
224508
  if (TEXT_EXT.test(path2)) {
223286
- entries.push({ path: path2, type: "text", text: readFileSync26(file.absPath, "utf8") });
224509
+ entries.push({ path: path2, type: "text", text: readFileSync29(file.absPath, "utf8") });
224510
+ } else if (/\.mat$/i.test(path2)) {
224511
+ entries.push({ path: path2, type: "shader", text: readFileSync29(file.absPath, "utf8"), absPath: file.absPath });
223287
224512
  } else {
223288
224513
  entries.push({ path: path2, type: "resource", fileSrc: "/project/" + rel(file.path) });
223289
224514
  }
223290
224515
  }
224516
+ const shaderEntries = entries.filter((e2) => e2.type === "shader");
224517
+ if (shaderEntries.length > 0) {
224518
+ const { warnings } = await applyLocalShaders(shaderEntries, { root, srcPrefix: "/project/.lecodes/shaders", platforms: ["webgl", "webgl-lite"] });
224519
+ for (const w of warnings)
224520
+ warnErr(w);
224521
+ const { buildLiteMaterialArtifact: buildLiteMaterialArtifact2 } = await Promise.resolve().then(() => (init_compile(), exports_compile));
224522
+ for (const e2 of shaderEntries) {
224523
+ if (e2.shaders?.length)
224524
+ continue;
224525
+ const hash = createHash8("sha256").update(e2.text ?? "").digest("hex").slice(0, 16);
224526
+ const dir = shadersCacheDir(root);
224527
+ mkdirSync24(dir, { recursive: true });
224528
+ const liteName = `${hash}_webgl-lite.filamat`;
224529
+ if (!existsSync32(join34(dir, liteName)))
224530
+ writeFileSync26(join34(dir, liteName), JSON.stringify(buildLiteMaterialArtifact2(e2.text ?? "")));
224531
+ const src = `/project/.lecodes/shaders/${hash}_opengl.filamat`;
224532
+ e2.shaders = ["opengl", "webgl", "webgl-lite"].map((platform) => ({ platform, src }));
224533
+ }
224534
+ }
223291
224535
  const scene = scenePath.startsWith("./") ? scenePath : "./" + rel(scenePath);
223292
224536
  if (!entries.some((e2) => e2.path === scene && e2.type === "text")) {
223293
224537
  return { js: null, error: `Scene file not found: ${scene}` };
@@ -223317,7 +224561,7 @@ var DEFAULT_PORT3 = 4488;
223317
224561
  var findAppRoot = (start) => {
223318
224562
  let dir = start;
223319
224563
  while (true) {
223320
- if (existsSync30(join31(dir, "app.json")))
224564
+ if (existsSync33(join35(dir, "app.json")))
223321
224565
  return dir;
223322
224566
  const parent = dirname17(dir);
223323
224567
  if (parent === dir)
@@ -223336,10 +224580,10 @@ var getContext2 = (args) => {
223336
224580
  manifest = null;
223337
224581
  const pathArg = args._[0];
223338
224582
  if (pathArg) {
223339
- const abs = resolve14(process.cwd(), pathArg);
223340
- if (existsSync30(abs) && statSync9(abs).isDirectory())
224583
+ const abs = resolve17(process.cwd(), pathArg);
224584
+ if (existsSync33(abs) && statSync12(abs).isDirectory())
223341
224585
  root = abs;
223342
- else if (abs.endsWith(".scene.ts") && existsSync30(abs))
224586
+ else if (abs.endsWith(".scene.ts") && existsSync33(abs))
223343
224587
  root = findAppRoot(dirname17(abs)) ?? dirname17(abs);
223344
224588
  else
223345
224589
  throw new CliError(`No such folder or .scene.ts: ${pathArg}`);
@@ -223347,15 +224591,15 @@ var getContext2 = (args) => {
223347
224591
  root = findAppRoot(root) ?? root;
223348
224592
  }
223349
224593
  }
223350
- return { root, manifest, name: manifest?.name ?? basename15(root) };
224594
+ return { root, manifest, name: manifest?.name ?? basename16(root) };
223351
224595
  };
223352
224596
  var writeMcpConfig2 = (root, port) => {
223353
- const path2 = join31(root, ".mcp.json");
224597
+ const path2 = join35(root, ".mcp.json");
223354
224598
  const entry = { type: "http", url: `http://localhost:${port}/mcp` };
223355
224599
  let config = {};
223356
- if (existsSync30(path2)) {
224600
+ if (existsSync33(path2)) {
223357
224601
  try {
223358
- config = JSON.parse(readFileSync27(path2, "utf8"));
224602
+ config = JSON.parse(readFileSync30(path2, "utf8"));
223359
224603
  } catch {
223360
224604
  warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-scene server by hand.");
223361
224605
  return;
@@ -223368,17 +224612,17 @@ var writeMcpConfig2 = (root, port) => {
223368
224612
  return;
223369
224613
  }
223370
224614
  config.mcpServers["lecodes-scene"] = entry;
223371
- writeFileSync24(path2, JSON.stringify(config, null, 2) + `
224615
+ writeFileSync27(path2, JSON.stringify(config, null, 2) + `
223372
224616
  `);
223373
224617
  note(`${existing ? "updated" : "created"} .mcp.json (lecodes-scene MCP → ${entry.url})`);
223374
224618
  };
223375
224619
  var countScenes = (dir) => {
223376
224620
  let n2 = 0;
223377
- for (const name of readdirSync13(dir)) {
224621
+ for (const name of readdirSync14(dir)) {
223378
224622
  if (name === "node_modules" || name.startsWith("."))
223379
224623
  continue;
223380
- const full = join31(dir, name);
223381
- if (statSync9(full).isDirectory())
224624
+ const full = join35(dir, name);
224625
+ if (statSync12(full).isDirectory())
223382
224626
  n2 += countScenes(full);
223383
224627
  else if (name.endsWith(".scene.ts"))
223384
224628
  n2++;
@@ -223406,7 +224650,8 @@ var scene = async (args) => {
223406
224650
  manifest: ctx.manifest,
223407
224651
  name: ctx.name,
223408
224652
  scenePath
223409
- })
224653
+ }),
224654
+ shaderArtifact: (shaderText, platform) => localShaderArtifact(ctx.root, shaderText, platform)
223410
224655
  });
223411
224656
  if (!flagBool(args, "no-mcp"))
223412
224657
  writeMcpConfig2(ctx.root, port);
@@ -223420,6 +224665,79 @@ var scene = async (args) => {
223420
224665
 
223421
224666
  // src/commands/assets.ts
223422
224667
  init_util();
224668
+
224669
+ // src/cmgenTool.ts
224670
+ import { existsSync as existsSync34, readdirSync as readdirSync15 } from "node:fs";
224671
+ import { homedir as homedir8 } from "node:os";
224672
+ import { join as join36, resolve as resolve18 } from "node:path";
224673
+ init_util();
224674
+ var exeName2 = () => process.platform === "win32" ? "cmgen.exe" : "cmgen";
224675
+ var platformKey3 = () => {
224676
+ if (process.platform === "win32")
224677
+ return "windows-x64";
224678
+ if (process.platform === "linux")
224679
+ return "linux-x64";
224680
+ throw new CliError(process.platform === "darwin" ? "cmgen isn't published for macOS yet — build filament's host tools locally and set LECODES_CMGEN." : `cmgen isn't published for ${process.platform}.`);
224681
+ };
224682
+ var cacheRoot3 = () => join36(homedir8(), ".lecodes", "cmgen");
224683
+ var findCmgenIn = (dir) => {
224684
+ for (const entry of readdirSync15(dir, { withFileTypes: true })) {
224685
+ const p2 = join36(dir, entry.name);
224686
+ if (entry.isFile() && entry.name === exeName2())
224687
+ return p2;
224688
+ if (entry.isDirectory()) {
224689
+ const found = findCmgenIn(p2);
224690
+ if (found)
224691
+ return found;
224692
+ }
224693
+ }
224694
+ return null;
224695
+ };
224696
+ var installedCmgenVersions = () => {
224697
+ try {
224698
+ return readdirSync15(cacheRoot3(), { withFileTypes: true }).filter((e2) => e2.isDirectory() && e2.name.startsWith("cmgen-")).map((e2) => e2.name.slice("cmgen-".length)).filter((v) => findCmgenIn(join36(cacheRoot3(), `cmgen-${v}`)) !== null).sort(semverNewer);
224699
+ } catch {
224700
+ return [];
224701
+ }
224702
+ };
224703
+ var fetchCmgenManifest = () => fetchReleaseProduct("cmgen", "cmgen (environment tool)");
224704
+ var downloadCmgen = async (version, versions, dest) => {
224705
+ const key = platformKey3();
224706
+ const file = versions.find((v) => v.version === version)?.files[key];
224707
+ if (!file) {
224708
+ throw new CliError(`cmgen ${version} [${key}] isn't published — build filament's host tools locally and point ` + `LECODES_CMGEN at the binary.`);
224709
+ }
224710
+ note(`Downloading the environment tool (cmgen ${version}, ${(file.size / 1024 / 1024).toFixed(1)} MB)…`);
224711
+ await downloadReleaseArchive(file, {
224712
+ cacheRoot: cacheRoot3(),
224713
+ dest,
224714
+ validate: (dir) => findCmgenIn(dir) ? null : "The downloaded archive doesn't contain the cmgen binary."
224715
+ });
224716
+ };
224717
+ var resolveCmgenExeCached = () => {
224718
+ const override = process.env.LECODES_CMGEN;
224719
+ if (override) {
224720
+ if (!existsSync34(override))
224721
+ throw new CliError(`LECODES_CMGEN points at "${override}" which doesn't exist.`);
224722
+ return resolve18(override);
224723
+ }
224724
+ const cached = installedCmgenVersions()[0];
224725
+ return cached ? findCmgenIn(join36(cacheRoot3(), `cmgen-${cached}`)) : null;
224726
+ };
224727
+ var resolveCmgenExe = async () => {
224728
+ const cached = resolveCmgenExeCached();
224729
+ if (cached)
224730
+ return cached;
224731
+ const { latest, versions } = await fetchCmgenManifest();
224732
+ const dir = join36(cacheRoot3(), `cmgen-${latest}`);
224733
+ await downloadCmgen(latest, versions, dir);
224734
+ const exe = findCmgenIn(dir);
224735
+ if (!exe)
224736
+ throw new CliError(`cmgen cache at ${dir} is broken — delete the folder and retry.`);
224737
+ return exe;
224738
+ };
224739
+
224740
+ // src/commands/assets.ts
223423
224741
  var assets = async (args) => {
223424
224742
  let mod;
223425
224743
  try {
@@ -223428,6 +224746,9 @@ var assets = async (args) => {
223428
224746
  throw new CliError("The asset pipeline isn't installed. Run: npm install -g lecodes-assets");
223429
224747
  }
223430
224748
  const raw = args._raw ?? args._;
224749
+ if (raw[0] === "sky" && !process.env.LECODES_CMGEN) {
224750
+ process.env.LECODES_CMGEN = await resolveCmgenExe();
224751
+ }
223431
224752
  const code = await mod.runAssetsCli(raw);
223432
224753
  if (code !== 0)
223433
224754
  process.exitCode = code;
@@ -223466,8 +224787,8 @@ var status3 = async (_args) => {
223466
224787
  };
223467
224788
 
223468
224789
  // src/commands/test.ts
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";
224790
+ import { existsSync as existsSync35, mkdirSync as mkdirSync25, readFileSync as readFileSync31, readdirSync as readdirSync16 } from "node:fs";
224791
+ import { dirname as dirname18, resolve as resolve19 } from "node:path";
223471
224792
  init_manifest();
223472
224793
  init_util();
223473
224794
  var deriveDesignFlows = (meta, entryScreen) => {
@@ -223530,7 +224851,7 @@ var slug3 = (s2) => s2.replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "").
223530
224851
  var readFlowFile = (abs, displayName) => {
223531
224852
  let raw;
223532
224853
  try {
223533
- raw = JSON.parse(readFileSync28(abs, "utf8"));
224854
+ raw = JSON.parse(readFileSync31(abs, "utf8"));
223534
224855
  } catch (e2) {
223535
224856
  throw new CliError(`Can't read "${displayName}": ${e2 instanceof Error ? e2.message : String(e2)}`);
223536
224857
  }
@@ -223556,7 +224877,14 @@ var test = async (args) => {
223556
224877
  const desktop2 = flagBool(args, "desktop");
223557
224878
  if (desktop2 && flagBool(args, "flows"))
223558
224879
  throw new CliError("--flows needs the headless renderer (design flows tap by name) — drop --desktop.");
223559
- const native = desktop2 ? await buildBundle(args, undefined, { localAssets: true }) : null;
224880
+ let desktopRoot;
224881
+ try {
224882
+ desktopRoot = findProjectRoot(process.cwd());
224883
+ } catch {
224884
+ desktopRoot = process.cwd();
224885
+ }
224886
+ const desktopRenderer = desktop2 ? resolveDesktopRenderer(desktopRoot, args) : undefined;
224887
+ const native = desktop2 ? await buildBundle(args, undefined, { localAssets: true, desktopRenderer }) : null;
223560
224888
  const headless = desktop2 ? null : await compileHeadlessBundle(args);
223561
224889
  const warnings = headless?.warnings ?? [];
223562
224890
  const useLocalAssets = headless?.useLocalAssets ?? false;
@@ -223578,40 +224906,40 @@ var test = async (args) => {
223578
224906
  if (!Number.isFinite(nativeFixedDt) || nativeFixedDt < 0 || nativeFixedDt > 1000)
223579
224907
  throw new CliError(`--fixed-dt wants a number of ms (0 = wall clock, max 1000), got "${fixedDtFlag}".`);
223580
224908
  const projectViewport = desktopDefaultViewport(root);
223581
- const testsDir = resolve15(root, flagStr(args, "dir") ?? "tests");
223582
- const designMetaPath = resolve15(root, flagStr(args, "design-dir") ?? "design", "meta.json");
224909
+ const testsDir = resolve19(root, flagStr(args, "dir") ?? "tests");
224910
+ const designMetaPath = resolve19(root, flagStr(args, "design-dir") ?? "design", "meta.json");
223583
224911
  const asJson = flagBool(args, "json");
223584
224912
  const out = asJson ? logErr : log;
223585
224913
  const cases = [];
223586
224914
  if (args._.length > 0) {
223587
224915
  for (const given of args._) {
223588
- const candidates = [resolve15(given), resolve15(testsDir, given), resolve15(testsDir, `${given}.flow.json`)];
223589
- const abs = candidates.find((p2) => existsSync31(p2));
224916
+ const candidates = [resolve19(given), resolve19(testsDir, given), resolve19(testsDir, `${given}.flow.json`)];
224917
+ const abs = candidates.find((p2) => existsSync35(p2));
223590
224918
  if (!abs)
223591
224919
  throw new CliError(`No such test: "${given}" (looked in ${testsDir}).`);
223592
224920
  cases.push(readFlowFile(abs, given));
223593
224921
  }
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));
224922
+ } else if (existsSync35(testsDir)) {
224923
+ for (const f3 of readdirSync16(testsDir).filter((f4) => f4.endsWith(".flow.json")).sort()) {
224924
+ cases.push(readFlowFile(resolve19(testsDir, f3), f3));
223597
224925
  }
223598
224926
  }
223599
224927
  const fixturesFlag = flagStr(args, "fixtures");
223600
- const defaultPack = resolve15(testsDir, "fixtures.json");
223601
- const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") : existsSync31(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
224928
+ const defaultPack = resolve19(testsDir, "fixtures.json");
224929
+ const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") : existsSync35(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
223602
224930
  const flowsOnly = flagBool(args, "flows");
223603
224931
  const wantFlows = !flagBool(args, "no-flows") && args._.length === 0;
223604
224932
  if (flowsOnly)
223605
224933
  cases.length = 0;
223606
224934
  let skipped = [];
223607
224935
  let entryScreen = null;
223608
- if (desktop2 && wantFlows && existsSync31(designMetaPath)) {
224936
+ if (desktop2 && wantFlows && existsSync35(designMetaPath)) {
223609
224937
  note("Design-derived flows skipped with --desktop (they tap by name; run `lecodes test` headless for them).");
223610
224938
  }
223611
- if (!desktop2 && (wantFlows || flowsOnly) && existsSync31(designMetaPath)) {
224939
+ if (!desktop2 && (wantFlows || flowsOnly) && existsSync35(designMetaPath)) {
223612
224940
  let meta;
223613
224941
  try {
223614
- meta = JSON.parse(readFileSync28(designMetaPath, "utf8"));
224942
+ meta = JSON.parse(readFileSync31(designMetaPath, "utf8"));
223615
224943
  } catch (e2) {
223616
224944
  throw new CliError(`Can't read ${designMetaPath}: ${e2 instanceof Error ? e2.message : String(e2)}`);
223617
224945
  }
@@ -223643,17 +224971,18 @@ var test = async (args) => {
223643
224971
  }, timeoutMs);
223644
224972
  killer.unref?.();
223645
224973
  const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
223646
- const artifactsRoot = resolve15(root, flagStr(args, "artifacts") ?? resolve15(testsDir, ".artifacts"));
224974
+ const artifactsRoot = resolve19(root, flagStr(args, "artifacts") ?? resolve19(testsDir, ".artifacts"));
223647
224975
  const results = [];
223648
224976
  const runNativeCase = async (kase) => {
223649
- const caseDir = resolve15(artifactsRoot, slug3(kase.name));
224977
+ const caseDir = resolve19(artifactsRoot, slug3(kase.name));
223650
224978
  const plan = translateScenario(kase.steps, { outDir: caseDir, headroomFrames: nativeFrames });
223651
224979
  for (const st of plan.steps)
223652
224980
  if (st.screenshot)
223653
- mkdirSync22(dirname18(st.screenshot), { recursive: true });
223654
- mkdirSync22(caseDir, { recursive: true });
223655
- const finalPng = resolve15(caseDir, "final.png");
224981
+ mkdirSync25(dirname18(st.screenshot), { recursive: true });
224982
+ mkdirSync25(caseDir, { recursive: true });
224983
+ const finalPng = resolve19(caseDir, "final.png");
223656
224984
  const { output } = await runDesktopRender(native.js, native.resources, {
224985
+ renderer: desktopRenderer,
223657
224986
  width: Number(flagStr(args, "width")) || kase.width || projectViewport?.width || 390,
223658
224987
  height: Number(flagStr(args, "height")) || kase.height || projectViewport?.height || 844,
223659
224988
  frames: plan.frameCap,
@@ -223703,14 +225032,14 @@ var test = async (args) => {
223703
225032
  onConsole
223704
225033
  });
223705
225034
  try {
223706
- const io = fileScenarioIO(resolve15(artifactsRoot, slug3(kase.name)));
225035
+ const io = fileScenarioIO(resolve19(artifactsRoot, slug3(kase.name)));
223707
225036
  const outcome = await renderer.runScenario(session, steps, io);
223708
225037
  result = {
223709
225038
  name: kase.name,
223710
225039
  kind: kase.kind,
223711
225040
  ok: outcome.ok,
223712
225041
  steps: outcome.steps.map((s2) => ({ desc: s2.desc, ok: s2.ok, error: s2.error })),
223713
- artifacts: outcome.failureArtifacts.map((p2) => resolve15(artifactsRoot, slug3(kase.name), p2))
225042
+ artifacts: outcome.failureArtifacts.map((p2) => resolve19(artifactsRoot, slug3(kase.name), p2))
223714
225043
  };
223715
225044
  } finally {
223716
225045
  session.dispose();
@@ -223778,11 +225107,11 @@ init_config();
223778
225107
  init_manifest();
223779
225108
  init_types4();
223780
225109
  init_util();
223781
- import { resolve as resolve16 } from "node:path";
225110
+ import { resolve as resolve20 } from "node:path";
223782
225111
  var label = (bundle) => `${Object.keys(bundle).length} file(s)`;
223783
225112
  var types4 = async (args) => {
223784
225113
  const dirArg = args._[0];
223785
- const root = dirArg ? resolve16(dirArg) : (() => {
225114
+ const root = dirArg ? resolve20(dirArg) : (() => {
223786
225115
  try {
223787
225116
  return findProjectRoot(process.cwd());
223788
225117
  } catch {
@@ -223841,23 +225170,23 @@ var types4 = async (args) => {
223841
225170
  };
223842
225171
 
223843
225172
  // src/index.ts
223844
- import { existsSync as existsSync33, readFileSync as readFileSync30, realpathSync as realpathSync2 } from "node:fs";
225173
+ import { existsSync as existsSync38, readFileSync as readFileSync34, realpathSync as realpathSync2 } from "node:fs";
223845
225174
  import { createRequire as createRequire4 } from "node:module";
223846
- import { delimiter as delimiter2, dirname as dirname20, join as join33 } from "node:path";
225175
+ import { delimiter as delimiter2, dirname as dirname21, join as join39 } from "node:path";
223847
225176
 
223848
225177
  // src/commands/install.ts
223849
225178
  init_util();
223850
- import { execFileSync as execFileSync5 } from "node:child_process";
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";
225179
+ import { execFileSync as execFileSync7 } from "node:child_process";
225180
+ import { appendFileSync as appendFileSync3, chmodSync as chmodSync2, cpSync as cpSync5, existsSync as existsSync36, mkdirSync as mkdirSync26, readFileSync as readFileSync32, realpathSync, rmSync as rmSync11, symlinkSync } from "node:fs";
225181
+ import { homedir as homedir9 } from "node:os";
225182
+ import { basename as basename17, delimiter, dirname as dirname19, join as join37, resolve as resolve21, sep as sep2 } from "node:path";
223854
225183
  var isWin = process.platform === "win32";
223855
- var HOME = homedir8();
225184
+ var HOME = homedir9();
223856
225185
  var safeRealpath = (p2) => {
223857
225186
  try {
223858
225187
  return realpathSync(p2);
223859
225188
  } catch {
223860
- return resolve17(p2);
225189
+ return resolve21(p2);
223861
225190
  }
223862
225191
  };
223863
225192
  var onPath = (dir) => {
@@ -223874,24 +225203,24 @@ var onPath = (dir) => {
223874
225203
  };
223875
225204
  var install = async (args) => {
223876
225205
  const bundle = dirname19(process.execPath);
223877
- const exeName2 = basename16(process.execPath);
225206
+ const exeName3 = basename17(process.execPath);
223878
225207
  const chiselName = isWin ? "chisel.exe" : "chisel";
223879
- if (!existsSync32(join32(bundle, "runtime")) || !existsSync32(join32(bundle, chiselName))) {
225208
+ if (!existsSync36(join37(bundle, "runtime")) || !existsSync36(join37(bundle, chiselName))) {
223880
225209
  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.");
223881
225210
  }
223882
- const companions = [exeName2, chiselName, "runtime", "README.txt"].filter((f3) => existsSync32(join32(bundle, f3)));
225211
+ const companions = [exeName3, chiselName, "runtime", "README.txt"].filter((f3) => existsSync36(join37(bundle, f3)));
223883
225212
  if (isWin)
223884
225213
  return installWindows(args, bundle, companions);
223885
225214
  return installUnix(args, bundle, companions);
223886
225215
  };
223887
225216
  var copyBundle = (bundle, target, companions) => {
223888
- mkdirSync23(target, { recursive: true });
225217
+ mkdirSync26(target, { recursive: true });
223889
225218
  if (safeRealpath(target) === safeRealpath(bundle))
223890
225219
  return;
223891
225220
  for (const f3 of companions) {
223892
- const dest = join32(target, f3);
223893
- rmSync9(dest, { recursive: true, force: true });
223894
- cpSync4(join32(bundle, f3), dest, { recursive: true });
225221
+ const dest = join37(target, f3);
225222
+ rmSync11(dest, { recursive: true, force: true });
225223
+ cpSync5(join37(bundle, f3), dest, { recursive: true });
223895
225224
  }
223896
225225
  };
223897
225226
  var permError = (e2, dir, system) => {
@@ -223903,41 +225232,41 @@ var permError = (e2, dir, system) => {
223903
225232
  };
223904
225233
  var installUnix = (args, bundle, companions) => {
223905
225234
  const system = flagBool(args, "system");
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");
225235
+ const libDir = resolve21(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join37(HOME, ".local", "lib", "lecodes-cli")));
225236
+ const binDir = resolve21(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join37(HOME, ".local", "bin")));
225237
+ const link2 = join37(binDir, "lecodes");
223909
225238
  try {
223910
225239
  copyBundle(bundle, libDir, companions);
223911
- chmodSync2(join32(libDir, "lecodes"), 493);
223912
- chmodSync2(join32(libDir, "chisel"), 493);
225240
+ chmodSync2(join37(libDir, "lecodes"), 493);
225241
+ chmodSync2(join37(libDir, "chisel"), 493);
223913
225242
  } catch (e2) {
223914
225243
  throw permError(e2, libDir, system);
223915
225244
  }
223916
225245
  try {
223917
- mkdirSync23(binDir, { recursive: true });
223918
- rmSync9(link2, { force: true });
223919
- symlinkSync(join32(libDir, "lecodes"), link2);
225246
+ mkdirSync26(binDir, { recursive: true });
225247
+ rmSync11(link2, { force: true });
225248
+ symlinkSync(join37(libDir, "lecodes"), link2);
223920
225249
  } catch (e2) {
223921
225250
  throw permError(e2, binDir, system);
223922
225251
  }
223923
225252
  success(`Installed to ${c.bold(libDir)}`);
223924
- log(` ${c.dim(link2 + " → " + join32(libDir, "lecodes"))}`);
225253
+ log(` ${c.dim(link2 + " → " + join37(libDir, "lecodes"))}`);
223925
225254
  finishPathUnix(args, binDir);
223926
225255
  };
223927
225256
  var shellRc = () => {
223928
- const shell = basename16(process.env.SHELL ?? "");
225257
+ const shell = basename17(process.env.SHELL ?? "");
223929
225258
  if (shell === "zsh")
223930
- return { file: join32(HOME, ".zshrc"), name: "~/.zshrc" };
225259
+ return { file: join37(HOME, ".zshrc"), name: "~/.zshrc" };
223931
225260
  if (shell === "bash")
223932
- return { file: join32(HOME, ".bashrc"), name: "~/.bashrc" };
223933
- return { file: join32(HOME, ".profile"), name: "~/.profile" };
225261
+ return { file: join37(HOME, ".bashrc"), name: "~/.bashrc" };
225262
+ return { file: join37(HOME, ".profile"), name: "~/.profile" };
223934
225263
  };
223935
225264
  var PATH_MARKER = "# Added by `lecodes install`";
223936
225265
  var appendPathToShellRc = (binDir) => {
223937
225266
  const { file, name } = shellRc();
223938
225267
  const line = `export PATH="${binDir}:$PATH"`;
223939
225268
  try {
223940
- const existing = existsSync32(file) ? readFileSync29(file, "utf8") : "";
225269
+ const existing = existsSync36(file) ? readFileSync32(file, "utf8") : "";
223941
225270
  if (existing.includes(line))
223942
225271
  return name;
223943
225272
  appendFileSync3(file, `${existing && !existing.endsWith(`
@@ -223975,7 +225304,7 @@ var finishPathUnix = (args, binDir) => {
223975
225304
  log(` • or install system-wide (no PATH edit): ${c.bold("sudo lecodes install --system")}`);
223976
225305
  };
223977
225306
  var installWindows = (args, bundle, companions) => {
223978
- const target = resolve17(flagStr(args, "dir") ?? join32(process.env.LOCALAPPDATA ?? join32(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
225307
+ const target = resolve21(flagStr(args, "dir") ?? join37(process.env.LOCALAPPDATA ?? join37(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
223979
225308
  try {
223980
225309
  copyBundle(bundle, target, companions);
223981
225310
  } catch (e2) {
@@ -224003,7 +225332,7 @@ var installWindows = (args, bundle, companions) => {
224003
225332
  var addToUserPathWindows = (dir) => {
224004
225333
  const script = "$d = $env:LECODES_INSTALL_DIR;" + "$p = [Environment]::GetEnvironmentVariable('Path','User'); if (-not $p) { $p = '' };" + "$parts = @($p.Split(';') | Where-Object { $_ -ne '' });" + "if ($parts -notcontains $d) { [Environment]::SetEnvironmentVariable('Path', (@($parts + $d) -join ';'), 'User') }";
224005
225334
  try {
224006
- execFileSync5("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], { stdio: "ignore", env: { ...process.env, LECODES_INSTALL_DIR: dir } });
225335
+ execFileSync7("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], { stdio: "ignore", env: { ...process.env, LECODES_INSTALL_DIR: dir } });
224007
225336
  return true;
224008
225337
  } catch {
224009
225338
  return false;
@@ -224012,10 +225341,85 @@ var addToUserPathWindows = (dir) => {
224012
225341
 
224013
225342
  // src/index.ts
224014
225343
  init_util();
225344
+
225345
+ // src/projectEnv.ts
225346
+ import { existsSync as existsSync37, readFileSync as readFileSync33 } from "node:fs";
225347
+ import { homedir as homedir10 } from "node:os";
225348
+ import { dirname as dirname20, join as join38, resolve as resolve22 } from "node:path";
225349
+ var KEY_RE = /^(LECODES|CREATOR)_[A-Z0-9_]+$/;
225350
+ var parseDotEnv = (text) => {
225351
+ const out = {};
225352
+ for (const raw of text.split(/\r?\n/)) {
225353
+ const line = raw.trim();
225354
+ if (!line || line.startsWith("#"))
225355
+ continue;
225356
+ const m = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
225357
+ if (!m)
225358
+ continue;
225359
+ const key = m[1];
225360
+ let value = m[2];
225361
+ if (value.startsWith('"')) {
225362
+ const end = value.indexOf('"', 1);
225363
+ value = (end < 0 ? value.slice(1) : value.slice(1, end)).replace(/\\n/g, `
225364
+ `).replace(/\\t/g, "\t").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
225365
+ } else if (value.startsWith("'")) {
225366
+ const end = value.indexOf("'", 1);
225367
+ value = end < 0 ? value.slice(1) : value.slice(1, end);
225368
+ } else {
225369
+ const hash = value.search(/\s#/);
225370
+ value = (hash >= 0 ? value.slice(0, hash) : value).trim();
225371
+ }
225372
+ out[key] = value;
225373
+ }
225374
+ return out;
225375
+ };
225376
+ var applyDotEnv = (parsed, dir, env) => {
225377
+ const out = {};
225378
+ for (const [key, raw] of Object.entries(parsed)) {
225379
+ if (!KEY_RE.test(key) || env[key] !== undefined)
225380
+ continue;
225381
+ let value = raw;
225382
+ if (value.startsWith("./") || value.startsWith("../"))
225383
+ value = resolve22(dir, value);
225384
+ else if (value === "~" || value.startsWith("~/"))
225385
+ value = join38(homedir10(), value.slice(1));
225386
+ out[key] = value;
225387
+ }
225388
+ return out;
225389
+ };
225390
+ var findEnvRoot = (start) => {
225391
+ let dir = resolve22(start);
225392
+ while (true) {
225393
+ if (existsSync37(join38(dir, "app.json")) || existsSync37(join38(dir, ".lecodes")))
225394
+ return dir;
225395
+ const parent = dirname20(dir);
225396
+ if (parent === dir)
225397
+ return null;
225398
+ dir = parent;
225399
+ }
225400
+ };
225401
+ var loadProjectEnv = (cwd = process.cwd()) => {
225402
+ try {
225403
+ const root = findEnvRoot(cwd);
225404
+ if (!root)
225405
+ return [];
225406
+ const file = join38(root, ".env");
225407
+ if (!existsSync37(file))
225408
+ return [];
225409
+ const added = applyDotEnv(parseDotEnv(readFileSync33(file, "utf8")), root, process.env);
225410
+ for (const [k, v] of Object.entries(added))
225411
+ process.env[k] = v;
225412
+ return Object.keys(added);
225413
+ } catch {
225414
+ return [];
225415
+ }
225416
+ };
225417
+
225418
+ // src/index.ts
224015
225419
  var STANDALONE = (() => {
224016
- const dir = dirname20(process.execPath);
225420
+ const dir = dirname21(process.execPath);
224017
225421
  const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
224018
- return existsSync33(join33(dir, "runtime")) && existsSync33(join33(dir, chisel));
225422
+ return existsSync38(join39(dir, "runtime")) && existsSync38(join39(dir, chisel));
224019
225423
  })();
224020
225424
  var INSTALLED = STANDALONE && (() => {
224021
225425
  const exe = process.platform === "win32" ? "lecodes.exe" : "lecodes";
@@ -224030,8 +225434,8 @@ var INSTALLED = STANDALONE && (() => {
224030
225434
  if (!d)
224031
225435
  return false;
224032
225436
  try {
224033
- const cand = join33(d, exe);
224034
- return existsSync33(cand) && realpathSync2(cand) === self2;
225437
+ const cand = join39(d, exe);
225438
+ return existsSync38(cand) && realpathSync2(cand) === self2;
224035
225439
  } catch {
224036
225440
  return false;
224037
225441
  }
@@ -224102,6 +225506,7 @@ ${c.bold("Commands:")}
224102
225506
  --desktop also open the app in the native desktop host
224103
225507
  (same hot reload; window from app.json's
224104
225508
  desktop block; closing it keeps the server)
225509
+ --fps (--desktop) draw the host's frame counter
224105
225510
  compile Compile the local project to JS (prints to stdout)
224106
225511
  (was 'lecodes preview' — the old name still works)
224107
225512
  -o, --out <file> write to a file instead of stdout
@@ -224110,8 +225515,16 @@ ${c.bold("Commands:")}
224110
225515
  --no-minify keep the bundle readable (no compress/mangle)
224111
225516
  --no-sourcemap don't generate/inline the source map
224112
225517
  --public-url <url> asset URL prefix (default: the API origin)
224113
- render Render the local UI project to a semantic JSON description
225518
+ render [file] Render the local UI project to a semantic JSON description
224114
225519
  (needs the optional 'lecodes-renderer' package; UI projects only)
225520
+ [file] render ONE screen module instead of the app:
225521
+ a file that default-exports a UIScreen (or a
225522
+ (state) => UIScreen) — every 'lecodes design'
225523
+ screen is written that way. A bare id
225524
+ ("login") resolves in design/screens/, and a
225525
+ design screen renders at its meta.json frame
225526
+ --state <name> ([file]) the state to pass a
225527
+ (state) => UIScreen screen
224115
225528
  -o, --out <file> write JSON to a file instead of stdout
224116
225529
  --png [file] rasterize a PNG (default screenshot.png;
224117
225530
  needs @napi-rs/canvas)
@@ -224204,7 +225617,7 @@ ${c.bold("Commands:")}
224204
225617
  add "<text>" [--screen <id>] [--state <s>] [--element <name>]
224205
225618
  reply <number> "<text>" · resolve <number> [--reply "<text>"] [--reopen]
224206
225619
  (--ai signs a write as Claude; sends with the next "lecodes push")
224207
- desktop [run|update] The native desktop host ('lecodes render --desktop' uses it too):
225620
+ desktop [run|build|update] The desktop target — the host 'lecodes render --desktop' uses too:
224208
225621
  no args shows the version status (selected / downloaded /
224209
225622
  newest published)
224210
225623
  run compile the current project and open it in a real
@@ -224214,11 +225627,45 @@ ${c.bold("Commands:")}
224214
225627
  app.json fullscreen; --scale <0.5..4|fit> overrides
224215
225628
  the display scale — "fit" sizes a 1080p-designed app
224216
225629
  to the monitor with the density raised to match;
224217
- --entry overrides the entrypoint; asset() resources
225630
+ --entry overrides the entrypoint; --fps draws the
225631
+ host's frame counter in the corner — composited fps,
225632
+ plus the 3D fps when they diverge; asset() resources
224218
225633
  load from the working tree — --remote-assets or
224219
225634
  --public-url <url> uses the server's copies instead)
225635
+ build stage a portable app folder (default dist/<name>/):
225636
+ the cached host renamed to <Name>.exe + app.js +
225637
+ assets/ (the project's files) + runtime/ (the engine's)
225638
+ + app.json (the host reads title/size/fullscreen from
225639
+ it) + app.ico from app.json "icon". Duplicate asset
225640
+ basenames are a build error (they silently alias in
225641
+ the bundle). Also spelled 'lecodes app build desktop'.
225642
+ On macOS: a signed .app instead (the downloaded LeCodes
225643
+ Desktop.app + the project inside it); app.json
225644
+ desktop.macos = bundleId / buildNumber / category /
225645
+ signing.identity / notarize.profile (a notarytool
225646
+ keychain profile — notarized + stapled, so it opens
225647
+ on any Mac without "Open Anyway").
225648
+ -o <dir> output folder (default dist/<name>)
225649
+ --slug <name> folder/zip name override (Latin-only)
225650
+ --renderer <gl|vulkan> host + shader variant
225651
+ --probe headless-render the staged folder to PNG
225652
+ (macOS: a brief windowed run + snapshot)
225653
+ --zip also pack the result for sending
225654
+ --sign <identity|adhoc> macOS codesign identity
225655
+ --notarize <profile> / --no-notarize macOS
225656
+ --zip <out>.zip, UTF-8/forward-slash entries
224220
225657
  update move to the newest published release
224221
225658
  (cached in ~/.lecodes/renderer)
225659
+ lightmap bake Bake static shadows + ambient occlusion into a lightmap atlas
225660
+ (assets/lightmap/lightmap.{png,bake}) by running the app in
225661
+ the desktop host: the statics are what the app registers
225662
+ with Lightmap.add, the bake fires at its Lightmap.load call.
225663
+ Props need UVs first: lecodes assets doctor … --lightmap-uv
225664
+ --out <dir> where the atlas lands (assets/lightmap)
225665
+ --size <px> atlas edge (4096) --texel <m> density (0.02)
225666
+ --sun-rays/--ao-rays <n> quality (32 / 64)
225667
+ --ao-distance <m> AO reach (2) --sun-angle <deg> (0.5)
225668
+ --frames <n> load headroom (900) --logs host output
224222
225669
  shaders [compile|update] The project's custom shaders (.mat) and their local compile:
224223
225670
  no args lists the shaders + their compile state; compile
224224
225671
  compiles them all into .lecodes/shaders (the same pass
@@ -224252,6 +225699,11 @@ ${c.bold("Commands:")}
224252
225699
  Skeleton, clips (+ root motion / first-frame-vs-rest per clip), meshes,
224253
225700
  materials, textures, bind sanity, duplicate names; --filmstrip renders
224254
225701
  a CPU-skinned PNG strip per clip (no GPU)
225702
+ assets sky <env.exr|.hdr> [-o dir] [--size N] [--ibl-size N] [--ibl-only]
225703
+ Equirectangular HDR → the two cubemaps a scene needs: sky.ktx (sharp,
225704
+ for 'skybox: { texture }') and ibl.ktx (prefiltered + SH, lights the
225705
+ scene) — they are NOT interchangeable. Reports where the sun is in
225706
+ the environment so the scene's light can be aimed to match
224255
225707
  assets doctor <in.glb|dir> [--fix] [--ktx2] [--dump]
224256
225708
  Analyze / fix a GLB against engine limits (bone cap 256, texture
224257
225709
  formats/size, KTX2 via toktx)
@@ -224300,15 +225752,8 @@ ${c.bold("Commands:")}
224300
225752
  --configuration <c> Release (default) or Debug
224301
225753
  --no-aab skip the .aab (APK only)
224302
225754
  --no-sync skip the lecodes app sync step
224303
- app build desktop Stage a portable desktop app folder (default dist/<name>/):
224304
- the cached host renamed to <Name>.exe + app.js + every asset +
224305
- app.json (the host ≥1.3.0 reads title/size/fullscreen from it)
224306
- + app.ico from app.json "icon". Duplicate asset basenames are
224307
- a build error (they silently alias in the bundle).
224308
- -o <dir> output folder (default dist/<name>)
224309
- --slug <name> folder/zip name override (e.g. Latin-only)
224310
- --probe headless-render the staged folder to a PNG
224311
- --zip <out>.zip, UTF-8/forward-slash entries
225755
+ app build desktop Same as 'lecodes desktop build' (kept for symmetry with the
225756
+ ios/android shells) — see the desktop group above.
224312
225757
  app upload ios Upload the archive to App Store Connect (then bumps buildNumber)
224313
225758
  --key-path <p8> --key-id <id> --issuer-id <id>
224314
225759
  ASC API key; stored in ~/.lecodes/asc.json
@@ -224322,8 +225767,11 @@ ${c.bold("Commands:")}
224322
225767
  pn keys [new|revoke] Push keys for YOUR server: list, mint (owner only, shown once
224323
225768
  — 'new --env' banks it in .env), revoke <id>
224324
225769
 
224325
- Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKEN).`;
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 };
225770
+ Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKEN).
225771
+ A .env next to app.json is read at startup for LECODES_* / CREATOR_* keys (the shell's own
225772
+ environment wins; ./ paths resolve against the project; the file is never pushed) — e.g.
225773
+ LECODES_DESKTOP_EXE, LECODES_MATC, LECODES_NOTARY_PROFILE, LECODES_MACOS_SIGN_ID, LECODES_TOKEN.`;
225774
+ 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, lightmap };
224327
225775
  if (STANDALONE)
224328
225776
  commands.install = install;
224329
225777
  var readVersion = () => package_default.version ?? "unknown";
@@ -224336,15 +225784,15 @@ var OPTIONAL_PEERS = [
224336
225784
  var peerVersion = (name, probe) => {
224337
225785
  try {
224338
225786
  const require2 = createRequire4(import.meta.url);
224339
- let dir = dirname20(require2.resolve(probe));
225787
+ let dir = dirname21(require2.resolve(probe));
224340
225788
  for (let depth = 0;depth < 16; depth++) {
224341
- const manifest = join33(dir, "package.json");
224342
- if (existsSync33(manifest)) {
224343
- const parsed = JSON.parse(readFileSync30(manifest, "utf8"));
225789
+ const manifest = join39(dir, "package.json");
225790
+ if (existsSync38(manifest)) {
225791
+ const parsed = JSON.parse(readFileSync34(manifest, "utf8"));
224344
225792
  if (parsed.name === name)
224345
225793
  return parsed.version ?? "unknown";
224346
225794
  }
224347
- const parent = dirname20(dir);
225795
+ const parent = dirname21(dir);
224348
225796
  if (parent === dir)
224349
225797
  break;
224350
225798
  dir = parent;
@@ -224369,6 +225817,7 @@ var printVersion = () => {
224369
225817
  var main = async () => {
224370
225818
  const argv = process.argv.slice(2);
224371
225819
  const command = argv[0];
225820
+ loadProjectEnv();
224372
225821
  if (!command || command === "help" || command === "--help" || command === "-h") {
224373
225822
  log(HELP);
224374
225823
  return;