lecodes-cli 2.0.13 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -52,7 +52,7 @@ var package_default;
52
52
  var init_package = __esm(() => {
53
53
  package_default = {
54
54
  name: "lecodes-cli",
55
- version: "2.0.13",
55
+ version: "2.1.1",
56
56
  dependencies: {
57
57
  "@letary/chisel": "^0.13.0",
58
58
  jimp: "^1.6.1",
@@ -76,7 +76,7 @@ var init_package = __esm(() => {
76
76
  },
77
77
  peerDependencies: {
78
78
  "lecodes-design": "^0.11.0",
79
- "lecodes-headless": "^2.0.13",
79
+ "lecodes-headless": "^2.1.1",
80
80
  "lecodes-3d-editor": "^0.9.0",
81
81
  "lecodes-assets": "^0.2.1",
82
82
  "heic-decode": "^2.1.0",
@@ -146,7 +146,7 @@ var init_package = __esm(() => {
146
146
  });
147
147
 
148
148
  // ../../sdk/src/version.ts
149
- var SDK_VERSION = "2.0.13", sdkMajor = (version) => {
149
+ var SDK_VERSION = "2.1.1", sdkMajor = (version) => {
150
150
  const m = version?.match(/^(\d+)\.\d+\.\d+/);
151
151
  return m ? Number(m[1]) : null;
152
152
  }, bundleSdkVersion = (head) => head.slice(0, 2000).match(/^\/\/ sdk: (\d+\.\d+\.\d+\S*)/m)?.[1] ?? null, bundleRunsOn = (bundleVersion, hostVersion) => sdkMajor(bundleVersion) !== null && sdkMajor(bundleVersion) === sdkMajor(hostVersion);
@@ -561,37 +561,6 @@ var init_manifest = __esm(() => {
561
561
  init_cli();
562
562
  });
563
563
 
564
- // src/platform/config.ts
565
- import { homedir } from "node:os";
566
- import { join as join2 } from "node:path";
567
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
568
- var DEFAULT_API = "https://le.codes", CONFIG_DIR, CONFIG_FILE, loadConfig = () => {
569
- let stored = {};
570
- try {
571
- stored = JSON.parse(readFileSync2(CONFIG_FILE, "utf8"));
572
- } catch {}
573
- return {
574
- apiUrl: process.env.LECODES_API ?? stored.apiUrl,
575
- token: process.env.LECODES_TOKEN ?? stored.token
576
- };
577
- }, saveConfig = (config) => {
578
- mkdirSync2(CONFIG_DIR, { recursive: true });
579
- writeFileSync2(CONFIG_FILE, JSON.stringify(config, null, 2));
580
- }, normalizeApiUrl = (url) => url.trim().replace(/\/+$/, ""), requireToken = (config) => {
581
- if (!config.token)
582
- throw new CliError("Not logged in. Run `lecodes login` first.");
583
- return config.token;
584
- }, requireApiUrl = (config) => {
585
- if (!config.apiUrl)
586
- throw new CliError("No server URL configured. Run `lecodes login` first.");
587
- return normalizeApiUrl(config.apiUrl);
588
- };
589
- var init_config = __esm(() => {
590
- init_cli();
591
- CONFIG_DIR = join2(homedir(), ".lecodes");
592
- CONFIG_FILE = join2(CONFIG_DIR, "config.json");
593
- });
594
-
595
564
  // src/platform/api.ts
596
565
  var errorMessage = (body, status) => {
597
566
  if (typeof body === "string" && body)
@@ -700,8 +669,8 @@ var init_paths = __esm(() => {
700
669
  });
701
670
 
702
671
  // src/project/ignore.ts
703
- import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
704
- import { join as join3 } from "node:path";
672
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
673
+ import { join as join2 } from "node:path";
705
674
  var BUILTIN_PATTERNS, DEFAULT_IGNORE_FILE = `# Files matched here are NOT pushed to LeCodes.
706
675
  # Only untracked files are affected — assets already in the project stay tracked
707
676
  # even if they match a pattern. Built-in defaults (node_modules, _*, *.log, .artifacts)
@@ -782,18 +751,18 @@ design/comments/
782
751
  return (path) => match(path.startsWith("/") ? path : "/" + path, false);
783
752
  }, loadIgnore = (root) => {
784
753
  const lines = [...BUILTIN_PATTERNS];
785
- const file = join3(root, ".lecodesignore");
754
+ const file = join2(root, ".lecodesignore");
786
755
  if (existsSync2(file)) {
787
756
  try {
788
- lines.push(...readFileSync3(file, "utf8").split(/\r?\n/));
757
+ lines.push(...readFileSync2(file, "utf8").split(/\r?\n/));
789
758
  } catch {}
790
759
  }
791
760
  const rules = lines.map(compile).filter((r) => r !== null);
792
761
  return buildMatcher(rules);
793
762
  }, writeDefaultIgnore = (root) => {
794
- const file = join3(root, ".lecodesignore");
763
+ const file = join2(root, ".lecodesignore");
795
764
  if (!existsSync2(file))
796
- writeFileSync3(file, DEFAULT_IGNORE_FILE);
765
+ writeFileSync2(file, DEFAULT_IGNORE_FILE);
797
766
  }, DEFAULT_GITIGNORE = `# LeCodes per-checkout state: generated IDE types + this machine's sync cursor
798
767
  # (.lecodes/manifest.json). Regenerate with \`lecodes update\` / re-attach with \`lecodes link\`.
799
768
  .lecodes/
@@ -808,19 +777,19 @@ node_modules/
808
777
 
809
778
  dist/
810
779
  `, ensureGitignore = (root) => {
811
- const file = join3(root, ".gitignore");
780
+ const file = join2(root, ".gitignore");
812
781
  if (!existsSync2(file)) {
813
- writeFileSync3(file, DEFAULT_GITIGNORE);
782
+ writeFileSync2(file, DEFAULT_GITIGNORE);
814
783
  return "created";
815
784
  }
816
- const text = readFileSync3(file, "utf8");
785
+ const text = readFileSync2(file, "utf8");
817
786
  const covered = text.split(/\r?\n/).some((l) => /^\/?\.lecodes\/?\s*$/.test(l.trim()));
818
787
  if (covered)
819
788
  return null;
820
789
  const sep = text.length === 0 || text.endsWith(`
821
790
  `) ? "" : `
822
791
  `;
823
- writeFileSync3(file, `${text}${sep}
792
+ writeFileSync2(file, `${text}${sep}
824
793
  # LeCodes per-checkout state (generated types + sync cursor) — not source
825
794
  .lecodes/
826
795
  `);
@@ -831,8 +800,8 @@ var init_ignore = __esm(() => {
831
800
  });
832
801
 
833
802
  // src/project/localFiles.ts
834
- import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync4 } from "node:fs";
835
- import { join as join4, relative } from "node:path";
803
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3 } from "node:fs";
804
+ import { join as join3, relative } from "node:path";
836
805
  var SKIP_DIRS, SKIP_FILES, TEXT_EXTS, ext = (name) => {
837
806
  const i = name.lastIndexOf(".");
838
807
  return i < 0 ? "" : name.slice(i + 1).toLowerCase();
@@ -860,7 +829,7 @@ var SKIP_DIRS, SKIP_FILES, TEXT_EXTS, ext = (name) => {
860
829
  };
861
830
  const walk = (dir, parentIgnored) => {
862
831
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
863
- const abs = join4(dir, entry.name);
832
+ const abs = join3(dir, entry.name);
864
833
  const key = toKey(relative(root, abs));
865
834
  if (entry.isDirectory()) {
866
835
  if (SKIP_DIRS.has(entry.name))
@@ -880,14 +849,14 @@ var SKIP_DIRS, SKIP_FILES, TEXT_EXTS, ext = (name) => {
880
849
  ignored.push(key);
881
850
  continue;
882
851
  }
883
- files.push({ path: key, absPath: abs, sha: sha256(readFileSync4(abs)) });
852
+ files.push({ path: key, absPath: abs, sha: sha256(readFileSync3(abs)) });
884
853
  }
885
854
  }
886
855
  };
887
856
  walk(root, false);
888
857
  return { files, dirs, ignored };
889
858
  }, resolveLocalPath = (root, key) => {
890
- const direct = join4(root, key.slice(1));
859
+ const direct = join3(root, key.slice(1));
891
860
  if (existsSync3(direct))
892
861
  return direct;
893
862
  let dir = root;
@@ -896,7 +865,7 @@ var SKIP_DIRS, SKIP_FILES, TEXT_EXTS, ext = (name) => {
896
865
  const match = readdirSync(dir).find((entry) => entry === name || sanitizeAssetName(entry) === name);
897
866
  if (!match)
898
867
  return null;
899
- dir = join4(dir, match);
868
+ dir = join3(dir, match);
900
869
  }
901
870
  } catch {
902
871
  return null;
@@ -956,32 +925,32 @@ var init_localFiles = __esm(() => {
956
925
 
957
926
  // src/hosts/distRoot.ts
958
927
  import { existsSync as existsSync4 } from "node:fs";
959
- import { dirname as dirname3, join as join6 } from "node:path";
928
+ import { dirname as dirname3, join as join5 } from "node:path";
960
929
  import { fileURLToPath } from "node:url";
961
930
  var distRoot;
962
931
  var init_distRoot = __esm(() => {
963
932
  distRoot = (() => {
964
933
  const here = dirname3(fileURLToPath(import.meta.url));
965
934
  const candidates = [
966
- join6(here, ".."),
967
- join6(here, "..", ".."),
935
+ join5(here, ".."),
936
+ join5(here, "..", ".."),
968
937
  dirname3(process.execPath)
969
938
  ];
970
939
  for (const c2 of candidates)
971
- if (existsSync4(join6(c2, "runtime")) || existsSync4(join6(c2, "package.json")))
940
+ if (existsSync4(join5(c2, "runtime")) || existsSync4(join5(c2, "package.json")))
972
941
  return c2;
973
942
  return candidates[0];
974
943
  })();
975
944
  });
976
945
 
977
946
  // src/hosts/peers.ts
978
- import { existsSync as existsSync5, readFileSync as readFileSync7 } from "node:fs";
947
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "node:fs";
979
948
  import { createRequire as createRequire2 } from "node:module";
980
- import { homedir as homedir2 } from "node:os";
981
- import { dirname as dirname4, join as join7, resolve } from "node:path";
982
- var OPTIONAL_PEERS, peersDir = () => process.env.LECODES_PEERS_DIR ? resolve(process.env.LECODES_PEERS_DIR) : join7(homedir2(), ".lecodes", "peers"), peerDirInPrefix = (name) => join7(peersDir(), "node_modules", ...name.split("/")), readVersion = (dir) => {
949
+ import { homedir } from "node:os";
950
+ import { dirname as dirname4, join as join6, resolve } from "node:path";
951
+ var OPTIONAL_PEERS, peersDir = () => process.env.LECODES_PEERS_DIR ? resolve(process.env.LECODES_PEERS_DIR) : join6(homedir(), ".lecodes", "peers"), peerDirInPrefix = (name) => join6(peersDir(), "node_modules", ...name.split("/")), readVersion = (dir) => {
983
952
  try {
984
- return JSON.parse(readFileSync7(join7(dir, "package.json"), "utf8")).version ?? "unknown";
953
+ return JSON.parse(readFileSync6(join6(dir, "package.json"), "utf8")).version ?? "unknown";
985
954
  } catch {
986
955
  return null;
987
956
  }
@@ -990,9 +959,9 @@ var OPTIONAL_PEERS, peersDir = () => process.env.LECODES_PEERS_DIR ? resolve(pro
990
959
  const require2 = createRequire2(import.meta.url);
991
960
  let dir = dirname4(require2.resolve(probe));
992
961
  for (let depth = 0;depth < 16; depth++) {
993
- const manifest = join7(dir, "package.json");
962
+ const manifest = join6(dir, "package.json");
994
963
  if (existsSync5(manifest)) {
995
- const parsed = JSON.parse(readFileSync7(manifest, "utf8"));
964
+ const parsed = JSON.parse(readFileSync6(manifest, "utf8"));
996
965
  if (parsed.name === name)
997
966
  return parsed.version ?? "unknown";
998
967
  }
@@ -1067,8 +1036,8 @@ var init_archives = __esm(() => {
1067
1036
 
1068
1037
  // src/hosts/packageManager.ts
1069
1038
  import { spawnSync } from "node:child_process";
1070
- import { homedir as homedir3 } from "node:os";
1071
- import { join as join8, resolve as resolve2, sep } from "node:path";
1039
+ import { homedir as homedir2 } from "node:os";
1040
+ import { join as join7, resolve as resolve2, sep } from "node:path";
1072
1041
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1073
1042
  var onPath = (cmd) => {
1074
1043
  try {
@@ -1076,7 +1045,7 @@ var onPath = (cmd) => {
1076
1045
  } catch {
1077
1046
  return false;
1078
1047
  }
1079
- }, bunGlobalDir = () => join8(process.env.BUN_INSTALL ? resolve2(process.env.BUN_INSTALL) : join8(homedir3(), ".bun"), "install", "global"), ownManager = (file = fileURLToPath2(import.meta.url)) => {
1048
+ }, bunGlobalDir = () => join7(process.env.BUN_INSTALL ? resolve2(process.env.BUN_INSTALL) : join7(homedir2(), ".bun"), "install", "global"), ownManager = (file = fileURLToPath2(import.meta.url)) => {
1080
1049
  const norm = (p) => process.platform === "win32" ? p.toLowerCase() : p;
1081
1050
  return norm(resolve2(file)).startsWith(norm(bunGlobalDir()) + sep) ? "bun" : "npm";
1082
1051
  }, pickManager = () => {
@@ -1088,8 +1057,8 @@ var init_packageManager = () => {};
1088
1057
 
1089
1058
  // src/hosts/peerInstall.ts
1090
1059
  import { spawnSync as spawnSync2 } from "node:child_process";
1091
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
1092
- import { join as join9 } from "node:path";
1060
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
1061
+ import { join as join8 } from "node:path";
1093
1062
  import { pathToFileURL } from "node:url";
1094
1063
  async function loadPeer(name, subpath, opts) {
1095
1064
  const spec = subpath ? `${name}/${subpath}` : name;
@@ -1100,7 +1069,7 @@ async function loadPeer(name, subpath, opts) {
1100
1069
  throw e;
1101
1070
  }
1102
1071
  const dir = peerDirInPrefix(name);
1103
- if (!existsSync6(join9(dir, "package.json"))) {
1072
+ if (!existsSync6(join8(dir, "package.json"))) {
1104
1073
  if (opts.install === false)
1105
1074
  return null;
1106
1075
  if (peerVersionGlobal(name, spec) !== null)
@@ -1111,7 +1080,7 @@ async function loadPeer(name, subpath, opts) {
1111
1080
  }
1112
1081
  var CANVAS_RANGE_FALLBACK = "^0.1.65", readJson = (path) => {
1113
1082
  try {
1114
- return JSON.parse(readFileSync8(path, "utf8"));
1083
+ return JSON.parse(readFileSync7(path, "utf8"));
1115
1084
  } catch {
1116
1085
  return null;
1117
1086
  }
@@ -1120,7 +1089,7 @@ var CANVAS_RANGE_FALLBACK = "^0.1.65", readJson = (path) => {
1120
1089
  if (archive)
1121
1090
  return archive;
1122
1091
  if (name === "@napi-rs/canvas") {
1123
- const renderer = readJson(join9(peerDirInPrefix("lecodes-headless"), "package.json"));
1092
+ const renderer = readJson(join8(peerDirInPrefix("lecodes-headless"), "package.json"));
1124
1093
  const range2 = renderer?.peerDependencies?.["@napi-rs/canvas"] ?? renderer?.optionalDependencies?.["@napi-rs/canvas"] ?? CANVAS_RANGE_FALLBACK;
1125
1094
  return `@napi-rs/canvas@${range2}`;
1126
1095
  }
@@ -1128,10 +1097,10 @@ var CANVAS_RANGE_FALLBACK = "^0.1.65", readJson = (path) => {
1128
1097
  return range ? `${name}@${range}` : `${name}@latest`;
1129
1098
  }, companions = (name) => name === "lecodes-headless" ? ["lecodes-headless", "@napi-rs/canvas"] : name === "sharp" ? ["sharp", "heic-decode"] : [name], manualCommand = (dir, specs) => commandLine(ownManager(), installIntoArgv(ownManager(), dir, specs)), installPeers = async (names, why, opts = {}) => {
1130
1099
  const dir = peersDir();
1131
- mkdirSync4(dir, { recursive: true });
1132
- const manifest = join9(dir, "package.json");
1100
+ mkdirSync3(dir, { recursive: true });
1101
+ const manifest = join8(dir, "package.json");
1133
1102
  if (!existsSync6(manifest)) {
1134
- writeFileSync5(manifest, JSON.stringify({
1103
+ writeFileSync4(manifest, JSON.stringify({
1135
1104
  name: "lecodes-peers",
1136
1105
  private: true,
1137
1106
  description: "Optional lecodes-cli peers, installed on first use. Safe to delete — the CLI re-installs what a command needs."
@@ -1164,11 +1133,11 @@ var CANVAS_RANGE_FALLBACK = "^0.1.65", readJson = (path) => {
1164
1133
  throw new CliError(`${tool} could not run: ${r.error.message}`);
1165
1134
  if (r.status !== 0)
1166
1135
  throw new CliError(`${tool} exited with ${r.status ?? "a signal"} while installing ${spec}. By hand: ${manualCommand(dir, [spec])}`);
1167
- if (!existsSync6(join9(peerDirInPrefix(name), "package.json")))
1136
+ if (!existsSync6(join8(peerDirInPrefix(name), "package.json")))
1168
1137
  throw new CliError(`${name} did not land in ${dir} — the installer finished but the package is missing`);
1169
1138
  }
1170
1139
  }, resolveEntry = (pkgDir, subpath) => {
1171
- const manifest = readJson(join9(pkgDir, "package.json"));
1140
+ const manifest = readJson(join8(pkgDir, "package.json"));
1172
1141
  if (!manifest)
1173
1142
  throw new CliError(`${pkgDir} holds no package.json`);
1174
1143
  const key = subpath ? `./${subpath}` : ".";
@@ -1196,7 +1165,7 @@ var CANVAS_RANGE_FALLBACK = "^0.1.65", readJson = (path) => {
1196
1165
  rel = typeof manifest.main === "string" ? manifest.main : "index.js";
1197
1166
  if (!rel)
1198
1167
  throw new CliError(`${manifest.name} has no export "${key}"`);
1199
- return join9(pkgDir, rel);
1168
+ return join8(pkgDir, rel);
1200
1169
  }, isNotFound = (e, spec) => {
1201
1170
  const code = e?.code;
1202
1171
  const msg = e instanceof Error ? e.message : String(e);
@@ -1789,17 +1758,17 @@ var init_chisel = __esm(() => {
1789
1758
  });
1790
1759
 
1791
1760
  // ../../sdk/src/compile/bundler.ts
1792
- import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync9, statSync } from "node:fs";
1761
+ import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync8, statSync } from "node:fs";
1793
1762
  import { spawnSync as spawnSync3 } from "node:child_process";
1794
1763
  import { createRequire as createRequire3 } from "node:module";
1795
- import { dirname as dirname5, join as join10, relative as relative2 } from "node:path";
1764
+ import { dirname as dirname5, join as join9, relative as relative2 } from "node:path";
1796
1765
  import { fileURLToPath as fileURLToPath3 } from "node:url";
1797
1766
  var resolveInject = () => {
1798
1767
  const override = process.env.LECODES_SDK_INJECT;
1799
1768
  if (override && existsSync7(override))
1800
1769
  return override;
1801
1770
  const here = dirname5(fileURLToPath3(import.meta.url));
1802
- const src = join10(here, "..", "inject.ts");
1771
+ const src = join9(here, "..", "inject.ts");
1803
1772
  if (existsSync7(src))
1804
1773
  return src;
1805
1774
  throw new Error("sdk: could not locate the inject source (LECODES_SDK_INJECT / src/inject.ts)");
@@ -1816,7 +1785,7 @@ var resolveInject = () => {
1816
1785
  return "/" + parts.join("/");
1817
1786
  }, CHISEL_DEFINE, CHISEL_KEEP, CLIENT_INJECT = "/__sdk/inject.ts", SERVER_INJECT = "/__sdk/server/inject.ts", EDITOR_INJECT = "/__sdk/inject.editor.ts", readInjectSources = () => {
1818
1787
  const client = resolveInject();
1819
- return { client: readFileSync9(client, "utf8"), server: readFileSync9(join10(dirname5(client), "server", "inject.ts"), "utf8") };
1788
+ return { client: readFileSync8(client, "utf8"), server: readFileSync8(join9(dirname5(client), "server", "inject.ts"), "utf8") };
1820
1789
  }, resolveSdkSrc = () => {
1821
1790
  const inject = resolveInject();
1822
1791
  if (!inject.endsWith("inject.ts")) {
@@ -1830,11 +1799,11 @@ var resolveInject = () => {
1830
1799
  const out = {};
1831
1800
  const walk = (dir) => {
1832
1801
  for (const name of readdirSync2(dir)) {
1833
- const p = join10(dir, name);
1802
+ const p = join9(dir, name);
1834
1803
  if (statSync(p).isDirectory())
1835
1804
  walk(p);
1836
1805
  else if (isSdkSource(p))
1837
- out["/__sdk/" + relative2(sdkSrc, p).split("\\").join("/")] = readFileSync9(p, "utf8");
1806
+ out["/__sdk/" + relative2(sdkSrc, p).split("\\").join("/")] = readFileSync8(p, "utf8");
1838
1807
  }
1839
1808
  };
1840
1809
  walk(sdkSrc);
@@ -172261,12 +172230,12 @@ var init_compile = __esm(() => {
172261
172230
  // src/compile/projectCompile.ts
172262
172231
  import { existsSync as existsSync8 } from "node:fs";
172263
172232
  import { createRequire as createRequire4 } from "node:module";
172264
- import { dirname as dirname6, join as join11 } from "node:path";
172233
+ import { dirname as dirname6, join as join10 } from "node:path";
172265
172234
  var resolveSdkInject = () => {
172266
172235
  try {
172267
172236
  return createRequire4(import.meta.url).resolve("lecodes-sdk/inject");
172268
172237
  } catch {}
172269
- const vendored = join11(distRoot, "runtime", "sdk", "inject.ts");
172238
+ const vendored = join10(distRoot, "runtime", "sdk", "inject.ts");
172270
172239
  return existsSync8(vendored) ? vendored : null;
172271
172240
  }, sdkInject, resolveChiselBin2 = () => {
172272
172241
  if (process.env.LECODES_CHISEL_BIN)
@@ -172276,7 +172245,7 @@ var resolveSdkInject = () => {
172276
172245
  if (fromPkg)
172277
172246
  return fromPkg;
172278
172247
  } catch {}
172279
- const local = join11(dirname6(process.execPath), process.platform === "win32" ? "chisel.exe" : "chisel");
172248
+ const local = join10(dirname6(process.execPath), process.platform === "win32" ? "chisel.exe" : "chisel");
172280
172249
  return existsSync8(local) ? local : null;
172281
172250
  }, chiselBin, ambiguousEntryWarning = (entries) => {
172282
172251
  const { entry, roots } = detectEntry(entries, scanModulesChisel);
@@ -172309,15 +172278,15 @@ var resolveSdkInject = () => {
172309
172278
  };
172310
172279
  const dir = peerDirInPrefix("typescript");
172311
172280
  const peer = () => {
172312
- if (!existsSync8(join11(dir, "package.json")))
172281
+ if (!existsSync8(join10(dir, "package.json")))
172313
172282
  return null;
172314
172283
  try {
172315
- return api(createRequire4(import.meta.url)(join11(dir, "lib", "typescript.js")));
172284
+ return api(createRequire4(import.meta.url)(join10(dir, "lib", "typescript.js")));
172316
172285
  } catch {
172317
172286
  return null;
172318
172287
  }
172319
172288
  };
172320
- const found = from(join11(root, "package.json")) ?? from(import.meta.url) ?? peer();
172289
+ const found = from(join10(root, "package.json")) ?? from(import.meta.url) ?? peer();
172321
172290
  if (found)
172322
172291
  return found;
172323
172292
  await installPeers(["typescript"], "The project's server code (*.server.ts)");
@@ -172344,8 +172313,8 @@ var init_projectCompile = __esm(() => {
172344
172313
 
172345
172314
  // src/compile/fonts.ts
172346
172315
  import { createHash as createHash2 } from "node:crypto";
172347
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "node:fs";
172348
- import { dirname as dirname7, join as join12 } from "node:path";
172316
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
172317
+ import { dirname as dirname7, join as join11 } from "node:path";
172349
172318
  var applyProjectFonts = async (entries, opts) => {
172350
172319
  const registry = (opts.registry ?? fontRegistryBase()).replace(/\/+$/, "");
172351
172320
  const byPath = () => new Map(entries.map((e) => [e.path.replace(/^\//, ""), e]));
@@ -172354,9 +172323,9 @@ var applyProjectFonts = async (entries, opts) => {
172354
172323
  registryBase: registry,
172355
172324
  readBytes: (entryPath) => {
172356
172325
  const entry = byPath().get(entryPath);
172357
- const abs = entry?.absPath ?? join12(opts.root, entryPath);
172326
+ const abs = entry?.absPath ?? join11(opts.root, entryPath);
172358
172327
  try {
172359
- return new Uint8Array(readFileSync10(abs));
172328
+ return new Uint8Array(readFileSync9(abs));
172360
172329
  } catch {
172361
172330
  return null;
172362
172331
  }
@@ -172364,7 +172333,7 @@ var applyProjectFonts = async (entries, opts) => {
172364
172333
  vendorFace: opts.mode === "vendor" ? async (face) => {
172365
172334
  const vendorDir = opts.vendorDir ?? opts.root;
172366
172335
  const rel = `assets/fonts/${face.fileName}`;
172367
- const abs = join12(vendorDir, rel);
172336
+ const abs = join11(vendorDir, rel);
172368
172337
  const entryPath = `${opts.vendorPrefix ?? ""}/${rel}`;
172369
172338
  if (!existsSync9(abs))
172370
172339
  await downloadFace(registry, vendorDir, face, rel, abs);
@@ -172388,13 +172357,13 @@ var applyProjectFonts = async (entries, opts) => {
172388
172357
  return res.ok ? new Uint8Array(await res.arrayBuffer()) : null;
172389
172358
  }
172390
172359
  try {
172391
- return new Uint8Array(readFileSync10(join12(registry, rel)));
172360
+ return new Uint8Array(readFileSync9(join11(registry, rel)));
172392
172361
  } catch {
172393
172362
  return null;
172394
172363
  }
172395
172364
  }, readLock = (path) => {
172396
172365
  try {
172397
- const lock = JSON.parse(readFileSync10(path, "utf8"));
172366
+ const lock = JSON.parse(readFileSync9(path, "utf8"));
172398
172367
  if (lock && typeof lock === "object" && lock.faces && lock.families)
172399
172368
  return lock;
172400
172369
  } catch {}
@@ -172426,10 +172395,10 @@ var applyProjectFonts = async (entries, opts) => {
172426
172395
  `);
172427
172396
  }, writeIfChanged = (path, text) => {
172428
172397
  try {
172429
- if (readFileSync10(path, "utf8") === text)
172398
+ if (readFileSync9(path, "utf8") === text)
172430
172399
  return;
172431
172400
  } catch {}
172432
- writeFileSync6(path, text);
172401
+ writeFileSync5(path, text);
172433
172402
  }, downloadFace = async (registry, vendorDir, face, rel, abs) => {
172434
172403
  const licRaw = await readRegistry(registry, "licenses.json");
172435
172404
  if (!licRaw)
@@ -172441,15 +172410,15 @@ var applyProjectFonts = async (entries, opts) => {
172441
172410
  const bytes = await readRegistry(registry, `${face.id}/${face.fileName}`);
172442
172411
  if (!bytes)
172443
172412
  throw new Error(`font("${face.id}"): face "${face.fileName}" not found in the registry (${registryHome(registry)}).`);
172444
- mkdirSync5(dirname7(abs), { recursive: true });
172445
- writeFileSync6(abs, bytes);
172446
- const lockPath = join12(vendorDir, "fonts.lock.json");
172413
+ mkdirSync4(dirname7(abs), { recursive: true });
172414
+ writeFileSync5(abs, bytes);
172415
+ const lockPath = join11(vendorDir, "fonts.lock.json");
172447
172416
  const lock = readLock(lockPath) ?? { registry, faces: {}, families: {} };
172448
172417
  lock.registry = registry;
172449
172418
  lock.faces[`${face.id}@${face.weight}${face.italic ? "i" : ""}`] = { id: face.id, family: face.family, weight: face.weight, italic: face.italic, hash: sha12(bytes), spdx: lic.spdx, path: rel };
172450
172419
  lock.families[face.id] = lic;
172451
172420
  writeIfChanged(lockPath, serializeLock(lock));
172452
- writeIfChanged(join12(vendorDir, "FONT-NOTICES.md"), renderNotices(lock));
172421
+ writeIfChanged(join11(vendorDir, "FONT-NOTICES.md"), renderNotices(lock));
172453
172422
  };
172454
172423
  var init_fonts = __esm(() => {
172455
172424
  init_compile();
@@ -172457,9 +172426,9 @@ var init_fonts = __esm(() => {
172457
172426
 
172458
172427
  // src/compile/assetIcons.ts
172459
172428
  import { createHash as createHash5 } from "node:crypto";
172460
- import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "node:fs";
172461
- import { homedir as homedir5 } from "node:os";
172462
- import { dirname as dirname8, join as join16 } from "node:path";
172429
+ import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "node:fs";
172430
+ import { homedir as homedir4 } from "node:os";
172431
+ import { dirname as dirname8, join as join15 } from "node:path";
172463
172432
  var DEFAULT_REGISTRY, isHttp2 = (s) => /^https?:/i.test(s), sha122 = (s) => createHash5("sha256").update(s).digest("hex").slice(0, 12), readText = async (registry, rel) => {
172464
172433
  if (isHttp2(registry)) {
172465
172434
  try {
@@ -172470,13 +172439,13 @@ var DEFAULT_REGISTRY, isHttp2 = (s) => /^https?:/i.test(s), sha122 = (s) => crea
172470
172439
  }
172471
172440
  }
172472
172441
  try {
172473
- return readFileSync13(join16(registry, rel), "utf8");
172442
+ return readFileSync12(join15(registry, rel), "utf8");
172474
172443
  } catch {
172475
172444
  return null;
172476
172445
  }
172477
172446
  }, safeRead = (path) => {
172478
172447
  try {
172479
- return readFileSync13(path, "utf8");
172448
+ return readFileSync12(path, "utf8");
172480
172449
  } catch {
172481
172450
  return null;
172482
172451
  }
@@ -172484,7 +172453,7 @@ var DEFAULT_REGISTRY, isHttp2 = (s) => /^https?:/i.test(s), sha122 = (s) => crea
172484
172453
  const current = existsSync13(path) ? safeRead(path) : null;
172485
172454
  if (current === text)
172486
172455
  return false;
172487
- writeFileSync9(path, text);
172456
+ writeFileSync8(path, text);
172488
172457
  return true;
172489
172458
  }, fetchIcon = async (registry, pack, name) => {
172490
172459
  const svg = await readText(registry, `${pack}/${name}.svg`);
@@ -172498,7 +172467,7 @@ var DEFAULT_REGISTRY, isHttp2 = (s) => /^https?:/i.test(s), sha122 = (s) => crea
172498
172467
  return JSON.parse(raw);
172499
172468
  }, readLock2 = (path) => {
172500
172469
  try {
172501
- const lock = JSON.parse(readFileSync13(path, "utf8"));
172470
+ const lock = JSON.parse(readFileSync12(path, "utf8"));
172502
172471
  if (lock && typeof lock === "object" && lock.icons && lock.packs)
172503
172472
  return lock;
172504
172473
  } catch {}
@@ -172533,13 +172502,13 @@ var DEFAULT_REGISTRY, isHttp2 = (s) => /^https?:/i.test(s), sha122 = (s) => crea
172533
172502
  `);
172534
172503
  }, vendorIcons = async (designDir, ids, opts = {}) => {
172535
172504
  const registry = (opts.registry ?? DEFAULT_REGISTRY).replace(/\/+$/, "");
172536
- const lockPath = join16(designDir, "icons.lock.json");
172505
+ const lockPath = join15(designDir, "icons.lock.json");
172537
172506
  const lock = readLock2(lockPath) ?? { registry, icons: {}, packs: {} };
172538
172507
  const svgById = new Map;
172539
172508
  const missing = [];
172540
172509
  for (const id of ids) {
172541
172510
  const li = lock.icons[id];
172542
- const cached = li && safeRead(join16(designDir, li.path));
172511
+ const cached = li && safeRead(join15(designDir, li.path));
172543
172512
  if (cached)
172544
172513
  svgById.set(id, cached);
172545
172514
  else
@@ -172555,9 +172524,9 @@ var DEFAULT_REGISTRY, isHttp2 = (s) => /^https?:/i.test(s), sha122 = (s) => crea
172555
172524
  throw new Error(`assetIcon: unknown icon pack "${pack}" (in "${id}"). Available packs: ${Object.keys(licenses).sort().join(", ")}.`);
172556
172525
  const svg = await fetchIcon(registry, pack, name);
172557
172526
  const rel = `assets/icons/${pack}/${name}.svg`;
172558
- const abs = join16(designDir, rel);
172559
- mkdirSync8(dirname8(abs), { recursive: true });
172560
- writeFileSync9(abs, svg);
172527
+ const abs = join15(designDir, rel);
172528
+ mkdirSync7(dirname8(abs), { recursive: true });
172529
+ writeFileSync8(abs, svg);
172561
172530
  lock.icons[id] = { pack, name, hash: sha122(svg), spdx: lic.spdx, path: rel };
172562
172531
  lock.packs[pack] = { spdx: lic.spdx, title: lic.title, url: lic.url, attribution: lic.attribution, modifiedNotice: lic.modifiedNotice };
172563
172532
  svgById.set(id, svg);
@@ -172566,25 +172535,25 @@ var DEFAULT_REGISTRY, isHttp2 = (s) => /^https?:/i.test(s), sha122 = (s) => crea
172566
172535
  writeIfChanged2(lockPath, serializeLock2(lock));
172567
172536
  }
172568
172537
  if (Object.keys(lock.icons).length)
172569
- writeIfChanged2(join16(designDir, "ICON-NOTICES.md"), renderNotices2(lock));
172538
+ writeIfChanged2(join15(designDir, "ICON-NOTICES.md"), renderNotices2(lock));
172570
172539
  return svgById;
172571
172540
  }, applyAssetIcons = async (entries, designDir, opts = {}) => {
172572
172541
  await applyAssetIconMacro(entries, (ids) => vendorIcons(designDir, ids, opts));
172573
- }, defaultCacheDir = () => join16(homedir5(), ".lecodes", "cache", "icons"), cacheIcons = async (ids, opts = {}) => {
172542
+ }, defaultCacheDir = () => join15(homedir4(), ".lecodes", "cache", "icons"), cacheIcons = async (ids, opts = {}) => {
172574
172543
  const registry = (opts.registry ?? DEFAULT_REGISTRY).replace(/\/+$/, "");
172575
172544
  const cacheDir = opts.cacheDir ?? defaultCacheDir();
172576
172545
  const svgById = new Map;
172577
172546
  for (const id of ids) {
172578
172547
  const [, pack, name] = ICON_ID.exec(id);
172579
- const cachePath = join16(cacheDir, pack, `${name}.svg`);
172548
+ const cachePath = join15(cacheDir, pack, `${name}.svg`);
172580
172549
  const cached = safeRead(cachePath);
172581
172550
  if (cached) {
172582
172551
  svgById.set(id, cached);
172583
172552
  continue;
172584
172553
  }
172585
172554
  const svg = await fetchIcon(registry, pack, name);
172586
- mkdirSync8(dirname8(cachePath), { recursive: true });
172587
- writeFileSync9(cachePath, svg);
172555
+ mkdirSync7(dirname8(cachePath), { recursive: true });
172556
+ writeFileSync8(cachePath, svg);
172588
172557
  svgById.set(id, svg);
172589
172558
  }
172590
172559
  return svgById;
@@ -172602,6 +172571,37 @@ var init_assetIcons = __esm(() => {
172602
172571
  DEFAULT_REGISTRY = process.env.LECODES_ICON_REGISTRY?.trim() || "https://icon-registry.jt3.ru/v1";
172603
172572
  });
172604
172573
 
172574
+ // src/platform/config.ts
172575
+ import { homedir as homedir5 } from "node:os";
172576
+ import { join as join16 } from "node:path";
172577
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "node:fs";
172578
+ var DEFAULT_API = "https://le.codes", CONFIG_DIR, CONFIG_FILE, loadConfig = () => {
172579
+ let stored = {};
172580
+ try {
172581
+ stored = JSON.parse(readFileSync13(CONFIG_FILE, "utf8"));
172582
+ } catch {}
172583
+ return {
172584
+ apiUrl: process.env.LECODES_API ?? stored.apiUrl,
172585
+ token: process.env.LECODES_TOKEN ?? stored.token
172586
+ };
172587
+ }, saveConfig = (config) => {
172588
+ mkdirSync8(CONFIG_DIR, { recursive: true });
172589
+ writeFileSync9(CONFIG_FILE, JSON.stringify(config, null, 2));
172590
+ }, normalizeApiUrl = (url) => url.trim().replace(/\/+$/, ""), requireToken = (config) => {
172591
+ if (!config.token)
172592
+ throw new CliError("Not logged in. Run `lecodes login` first.");
172593
+ return config.token;
172594
+ }, requireApiUrl = (config) => {
172595
+ if (!config.apiUrl)
172596
+ throw new CliError("No server URL configured. Run `lecodes login` first.");
172597
+ return normalizeApiUrl(config.apiUrl);
172598
+ };
172599
+ var init_config = __esm(() => {
172600
+ init_cli();
172601
+ CONFIG_DIR = join16(homedir5(), ".lecodes");
172602
+ CONFIG_FILE = join16(CONFIG_DIR, "config.json");
172603
+ });
172604
+
172605
172605
  // src/commands/shared.ts
172606
172606
  import { basename as basename4 } from "node:path";
172607
172607
  var bundleFlags, outFlag, bundleOptions = (flags) => ({
@@ -215995,8 +215995,10 @@ var describeField = (f5) => {
215995
215995
  const fields = Object.entries(m.fields).filter(([k]) => !shown[name] || shown[name].includes(k));
215996
215996
  const described = Object.fromEntries(fields.map(([k, f5]) => [k, describeField(f5)]));
215997
215997
  for (const f5 of files) {
215998
- if (f5.model === name)
215999
- described[f5.key] = { kind: "file", optional: false, unique: false, index: false, hasDefault: false, array: f5.array, isId: false };
215998
+ if (f5.model !== name)
215999
+ continue;
216000
+ const declared = db.$given[name]?.fields?.[f5.key]?.def;
216001
+ described[f5.key] = { kind: "file", optional: declared?.optional ?? false, unique: false, index: false, hasDefault: false, array: f5.array, isId: false, ...f5.image ? { image: f5.image } : {} };
216000
216002
  }
216001
216003
  models[name] = { fields: described, ...name === FILE_MODEL ? { files: true } : shown[name] ? { auth: true } : {} };
216002
216004
  }
@@ -217173,7 +217175,6 @@ import { delimiter as delimiter3, dirname as dirname28, join as join59 } from "n
217173
217175
 
217174
217176
  // src/commands/app/index.ts
217175
217177
  init_manifest();
217176
- init_config();
217177
217178
  import { execFileSync as execFileSync6 } from "node:child_process";
217178
217179
  import { cpSync as cpSync4, existsSync as existsSync23, mkdirSync as mkdirSync15, readFileSync as readFileSync20, readdirSync as readdirSync9, rmSync as rmSync7, statSync as statSync9, writeFileSync as writeFileSync18 } from "node:fs";
217179
217180
  import { homedir as homedir10 } from "node:os";
@@ -217188,16 +217189,16 @@ import { basename as basename5, resolve as resolve5 } from "node:path";
217188
217189
  // src/compile/collect.ts
217189
217190
  init_localFiles();
217190
217191
  init_ignore();
217191
- import { readFileSync as readFileSync5 } from "node:fs";
217192
+ import { readFileSync as readFileSync4 } from "node:fs";
217192
217193
  import { basename } from "node:path";
217193
217194
 
217194
217195
  // src/project/materialize.ts
217195
217196
  init_api();
217196
217197
  init_paths();
217197
- import { dirname as dirname2, join as join5 } from "node:path";
217198
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "node:fs";
217198
+ import { dirname as dirname2, join as join4 } from "node:path";
217199
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "node:fs";
217199
217200
  var kindOf = (type) => type === "file" ? "resource" : type;
217200
- var absOf = (root, key) => join5(root, ...key.replace(/^\//, "").split("/"));
217201
+ var absOf = (root, key) => join4(root, ...key.replace(/^\//, "").split("/"));
217201
217202
  var buildPaths = (assets) => {
217202
217203
  const pathById = new Map;
217203
217204
  for (const asset of assets) {
@@ -217215,20 +217216,20 @@ var materializeAssets = async (root, project, apiUrl, token, onProgress) => {
217215
217216
  const key = pathById.get(asset.id);
217216
217217
  const abs = absOf(root, key);
217217
217218
  if (asset.type === "folder") {
217218
- mkdirSync3(abs, { recursive: true });
217219
+ mkdirSync2(abs, { recursive: true });
217219
217220
  continue;
217220
217221
  }
217221
- mkdirSync3(dirname2(abs), { recursive: true });
217222
+ mkdirSync2(dirname2(abs), { recursive: true });
217222
217223
  let sha;
217223
217224
  if (asset.type === "file") {
217224
217225
  if (!asset.file?.src)
217225
217226
  continue;
217226
217227
  const bytes = await downloadBlob(apiUrl, asset.file.src);
217227
- writeFileSync4(abs, bytes);
217228
+ writeFileSync3(abs, bytes);
217228
217229
  sha = sha256(bytes);
217229
217230
  } else {
217230
217231
  const text = asset.text ?? "";
217231
- writeFileSync4(abs, text);
217232
+ writeFileSync3(abs, text);
217232
217233
  sha = sha256(text);
217233
217234
  }
217234
217235
  files[key] = { id: asset.id, type: kindOf(asset.type), sha };
@@ -217262,7 +217263,7 @@ var collectEntries = (root, manifest, project, opts = {}) => {
217262
217263
  warnings.push(`No compiled shader for ${file.path} — push it to compile the shader.`);
217263
217264
  entries.push({ path: file.path, type, shaders, absPath: file.absPath });
217264
217265
  } else {
217265
- entries.push({ path: file.path, type, text: readFileSync5(file.absPath, "utf8") });
217266
+ entries.push({ path: file.path, type, text: readFileSync4(file.absPath, "utf8") });
217266
217267
  }
217267
217268
  }
217268
217269
  return { entries, warnings };
@@ -217271,7 +217272,7 @@ var collectEntries = (root, manifest, project, opts = {}) => {
217271
217272
  // src/compile/collectLocal.ts
217272
217273
  init_localFiles();
217273
217274
  init_ignore();
217274
- import { readFileSync as readFileSync6 } from "node:fs";
217275
+ import { readFileSync as readFileSync5 } from "node:fs";
217275
217276
  import { basename as basename2 } from "node:path";
217276
217277
  var collectLocalEntries = (root, manifest, assetBase, opts = {}) => {
217277
217278
  const entries = [];
@@ -217287,7 +217288,7 @@ var collectLocalEntries = (root, manifest, assetBase, opts = {}) => {
217287
217288
  }
217288
217289
  entries.push({ path: file.path, type, shaders: [], absPath: file.absPath });
217289
217290
  } else {
217290
- entries.push({ path: file.path, type, text: readFileSync6(file.absPath, "utf8") });
217291
+ entries.push({ path: file.path, type, text: readFileSync5(file.absPath, "utf8") });
217291
217292
  }
217292
217293
  }
217293
217294
  return { entries, warnings };
@@ -217301,26 +217302,26 @@ init_fonts();
217301
217302
  init_compile();
217302
217303
  import { execFile } from "node:child_process";
217303
217304
  import { createHash as createHash4, randomBytes } from "node:crypto";
217304
- import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync12, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync8 } from "node:fs";
217305
+ import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync11, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync7 } from "node:fs";
217305
217306
  import { tmpdir } from "node:os";
217306
- import { basename as basename3, join as join15 } from "node:path";
217307
+ import { basename as basename3, join as join14 } from "node:path";
217307
217308
 
217308
217309
  // src/hosts/matcTool.ts
217309
217310
  import { existsSync as existsSync11, readdirSync as readdirSync3 } from "node:fs";
217310
- import { homedir as homedir4 } from "node:os";
217311
- import { join as join14, resolve as resolve3 } from "node:path";
217311
+ import { homedir as homedir3 } from "node:os";
217312
+ import { join as join13, resolve as resolve3 } from "node:path";
217312
217313
 
217313
217314
  // src/hosts/releases.ts
217314
217315
  init_cli();
217315
217316
  import { createHash as createHash3 } from "node:crypto";
217316
- import { existsSync as existsSync10, mkdirSync as mkdirSync6, mkdtempSync, readFileSync as readFileSync11, renameSync, rmSync, writeFileSync as writeFileSync7 } from "node:fs";
217317
- import { join as join13 } from "node:path";
217317
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, mkdtempSync, readFileSync as readFileSync10, renameSync, rmSync, writeFileSync as writeFileSync6 } from "node:fs";
217318
+ import { join as join12 } from "node:path";
217318
217319
  import { spawnSync as spawnSync4 } from "node:child_process";
217319
217320
  var MANIFEST_URL = "https://storage.yandexcloud.net/le-codes/releases/versions.json";
217320
217321
  var ARTIFACT_STAMP = ".lecodes-artifact.json";
217321
217322
  var cachedArtifactSha = (dir) => {
217322
217323
  try {
217323
- return JSON.parse(readFileSync11(join13(dir, ARTIFACT_STAMP), "utf8")).sha256 ?? null;
217324
+ return JSON.parse(readFileSync10(join12(dir, ARTIFACT_STAMP), "utf8")).sha256 ?? null;
217324
217325
  } catch {
217325
217326
  return null;
217326
217327
  }
@@ -217366,16 +217367,16 @@ var downloadReleaseArchive = async (file, opts) => {
217366
217367
  const sha = createHash3("sha256").update(bytes).digest("hex");
217367
217368
  if (sha !== file.sha256)
217368
217369
  throw new CliError(`Downloaded archive is corrupt (sha256 mismatch) — try again.`);
217369
- mkdirSync6(opts.cacheRoot, { recursive: true });
217370
- const stage = mkdtempSync(join13(opts.cacheRoot, "dl-"));
217370
+ mkdirSync5(opts.cacheRoot, { recursive: true });
217371
+ const stage = mkdtempSync(join12(opts.cacheRoot, "dl-"));
217371
217372
  try {
217372
217373
  const archiveName = file.url.split("/").pop() ?? "archive";
217373
- const archive = join13(stage, archiveName);
217374
- writeFileSync7(archive, bytes);
217374
+ const archive = join12(stage, archiveName);
217375
+ writeFileSync6(archive, bytes);
217375
217376
  if (opts.extract) {
217376
217377
  opts.extract(archive, stage);
217377
217378
  } else {
217378
- const winTar = join13(process.env.SystemRoot ?? "C:\\Windows", "System32", "tar.exe");
217379
+ const winTar = join12(process.env.SystemRoot ?? "C:\\Windows", "System32", "tar.exe");
217379
217380
  const tarExe = process.platform === "win32" && existsSync10(winTar) ? winTar : "tar";
217380
217381
  const tar = spawnSync4(tarExe, ["-xf", archiveName], { cwd: stage, stdio: ["ignore", "ignore", "pipe"] });
217381
217382
  if (tar.status !== 0)
@@ -217385,7 +217386,7 @@ var downloadReleaseArchive = async (file, opts) => {
217385
217386
  const bad = opts.validate(stage);
217386
217387
  if (bad)
217387
217388
  throw new CliError(bad);
217388
- writeFileSync7(join13(stage, ARTIFACT_STAMP), JSON.stringify({ sha256: file.sha256, url: file.url }, null, 2));
217389
+ writeFileSync6(join12(stage, ARTIFACT_STAMP), JSON.stringify({ sha256: file.sha256, url: file.url }, null, 2));
217389
217390
  try {
217390
217391
  renameSync(stage, opts.dest);
217391
217392
  } catch {
@@ -217415,11 +217416,11 @@ var platformKey = () => {
217415
217416
  return macPlatformKey();
217416
217417
  throw new CliError(`matc isn't published for ${process.platform}.`);
217417
217418
  };
217418
- var cacheRoot = () => join14(homedir4(), ".lecodes", "matc");
217419
+ var cacheRoot = () => join13(homedir3(), ".lecodes", "matc");
217419
217420
  var findMatc = (dir) => {
217420
217421
  const name = matcName();
217421
217422
  for (const entry of readdirSync3(dir, { withFileTypes: true })) {
217422
- const p = join14(dir, entry.name);
217423
+ const p = join13(dir, entry.name);
217423
217424
  if (entry.isFile() && entry.name === name)
217424
217425
  return p;
217425
217426
  if (entry.isDirectory()) {
@@ -217432,7 +217433,7 @@ var findMatc = (dir) => {
217432
217433
  };
217433
217434
  var installedMatcVersions = () => {
217434
217435
  try {
217435
- return readdirSync3(cacheRoot(), { withFileTypes: true }).filter((e) => e.isDirectory() && e.name.startsWith("matc-")).map((e) => e.name.slice("matc-".length)).filter((v) => findMatc(join14(cacheRoot(), `matc-${v}`)) !== null).sort(semverNewer);
217436
+ return readdirSync3(cacheRoot(), { withFileTypes: true }).filter((e) => e.isDirectory() && e.name.startsWith("matc-")).map((e) => e.name.slice("matc-".length)).filter((v) => findMatc(join13(cacheRoot(), `matc-${v}`)) !== null).sort(semverNewer);
217436
217437
  } catch {
217437
217438
  return [];
217438
217439
  }
@@ -217462,7 +217463,7 @@ var resolveMatcExeCached = () => {
217462
217463
  return resolve3(override);
217463
217464
  }
217464
217465
  const cached = installedMatcVersions()[0];
217465
- return cached ? findMatc(join14(cacheRoot(), `matc-${cached}`)) : null;
217466
+ return cached ? findMatc(join13(cacheRoot(), `matc-${cached}`)) : null;
217466
217467
  };
217467
217468
  var resolveMatcExe = async () => {
217468
217469
  const cached = resolveMatcExeCached();
@@ -217470,7 +217471,7 @@ var resolveMatcExe = async () => {
217470
217471
  return cached;
217471
217472
  const { versions } = await fetchMatcManifest();
217472
217473
  const { version, file } = pickMatc(versions);
217473
- const dir = join14(cacheRoot(), `matc-${version}`);
217474
+ const dir = join13(cacheRoot(), `matc-${version}`);
217474
217475
  await downloadMatc(version, file, dir);
217475
217476
  const exe = findMatc(dir);
217476
217477
  if (!exe)
@@ -217481,7 +217482,7 @@ var updateMatc = async () => {
217481
217482
  const { versions } = await fetchMatcManifest();
217482
217483
  const { version, file } = pickMatc(versions);
217483
217484
  const before = installedMatcVersions()[0];
217484
- const dir = join14(cacheRoot(), `matc-${version}`);
217485
+ const dir = join13(cacheRoot(), `matc-${version}`);
217485
217486
  if (existsSync11(dir) && findMatc(dir) !== null) {
217486
217487
  note(`Already up to date: matc ${version}.`);
217487
217488
  return;
@@ -217494,7 +217495,7 @@ var updateMatc = async () => {
217494
217495
  init_manifest();
217495
217496
  init_cli();
217496
217497
  var SHADERS_DIR = "shaders";
217497
- var shadersCacheDir = (root) => join15(root, LECODES_DIR, SHADERS_DIR);
217498
+ var shadersCacheDir = (root) => join14(root, LECODES_DIR, SHADERS_DIR);
217498
217499
  var isShaderArtifactName = (name) => /^[0-9a-f]{16}_[a-z-]+\.filamat$/.test(name);
217499
217500
  var matcIdentity = (exe) => {
217500
217501
  const stat = statSync2(exe);
@@ -217504,17 +217505,17 @@ var shaderHash = (text, matcId) => createHash4("sha256").update(`${TARGET_MATRIX
217504
217505
  var artifactFamily = () => [...MATC_PLATFORMS, ...LOCAL_ONLY_PLATFORMS, "webgl-lite"];
217505
217506
  var shaderIsCompiled = (root, text, matcId) => {
217506
217507
  const hash = shaderHash(text, matcId);
217507
- return artifactFamily().every((p) => existsSync12(join15(shadersCacheDir(root), `${hash}_${p}.filamat`)));
217508
+ return artifactFamily().every((p) => existsSync12(join14(shadersCacheDir(root), `${hash}_${p}.filamat`)));
217508
217509
  };
217509
217510
  var shaderCompiledPlatforms = (root, text, matcId) => {
217510
217511
  const hash = shaderHash(text, matcId);
217511
- return artifactFamily().filter((p) => existsSync12(join15(shadersCacheDir(root), `${hash}_${p}.filamat`)));
217512
+ return artifactFamily().filter((p) => existsSync12(join14(shadersCacheDir(root), `${hash}_${p}.filamat`)));
217512
217513
  };
217513
217514
  var localShaderArtifact = (root, text, platform) => {
217514
217515
  const matcExe = resolveMatcExeCached();
217515
217516
  if (!matcExe)
217516
217517
  return null;
217517
- const file = join15(shadersCacheDir(root), `${shaderHash(text, matcIdentity(matcExe))}_${platform}.filamat`);
217518
+ const file = join14(shadersCacheDir(root), `${shaderHash(text, matcIdentity(matcExe))}_${platform}.filamat`);
217518
217519
  return existsSync12(file) ? file : null;
217519
217520
  };
217520
217521
  var allShaderPlatforms = () => [...Object.keys(ALL_PLATFORM_BUILD), "webgl-lite"];
@@ -217548,25 +217549,25 @@ var applyLocalShaders = async (entries, opts) => {
217548
217549
  }
217549
217550
  const matcId = matcIdentity(matcExe);
217550
217551
  const cacheDir = shadersCacheDir(opts.root);
217551
- mkdirSync7(cacheDir, { recursive: true });
217552
+ mkdirSync6(cacheDir, { recursive: true });
217552
217553
  const artifacts = [];
217553
217554
  for (const entry of shaderEntries) {
217554
- const text = readFileSync12(entry.absPath, "utf8");
217555
+ const text = readFileSync11(entry.absPath, "utf8");
217555
217556
  const hash = shaderHash(text, matcId);
217556
217557
  const family = artifactFamily();
217557
217558
  const artifact = (platform) => `${hash}_${platform}.filamat`;
217558
- const has = (platform) => existsSync12(join15(cacheDir, artifact(platform)));
217559
+ const has = (platform) => existsSync12(join14(cacheDir, artifact(platform)));
217559
217560
  const wanted = opts.platforms ? family.filter((p) => opts.platforms.includes(p)) : family;
217560
217561
  const missing = wanted.filter((p) => !has(p));
217561
217562
  const matcMissing = missing.filter((p) => p !== "webgl-lite");
217562
217563
  if (matcMissing.length > 0) {
217563
217564
  const tempName = `lecodes-matc-${randomBytes(6).toString("hex")}.mat`;
217564
- const tempFile = join15(tmpdir(), tempName);
217565
+ const tempFile = join14(tmpdir(), tempName);
217565
217566
  try {
217566
217567
  for (const platform of matcMissing) {
217567
217568
  const build = ALL_PLATFORM_BUILD[platform];
217568
- writeFileSync8(tempFile, build.replaceExternal ? text.replace(externalSamplerReg, "type:sampler2d") : text);
217569
- const out = join15(cacheDir, artifact(platform));
217569
+ writeFileSync7(tempFile, build.replaceExternal ? text.replace(externalSamplerReg, "type:sampler2d") : text);
217570
+ const out = join14(cacheDir, artifact(platform));
217570
217571
  const { ok, stderr } = await runMatc(matcExe, matcArgs(platform, out, tempFile));
217571
217572
  if (!ok) {
217572
217573
  rmSync2(out, { force: true });
@@ -217579,7 +217580,7 @@ ${cleanMatcOutput(stderr, tempName, basename3(entry.path))}`);
217579
217580
  }
217580
217581
  }
217581
217582
  if (missing.includes("webgl-lite")) {
217582
- writeFileSync8(join15(cacheDir, artifact("webgl-lite")), JSON.stringify(buildLiteMaterialArtifact(text)));
217583
+ writeFileSync7(join14(cacheDir, artifact("webgl-lite")), JSON.stringify(buildLiteMaterialArtifact(text)));
217583
217584
  }
217584
217585
  if (opts.local) {
217585
217586
  const staged = opts.platforms ? ALL_NATIVE_PLATFORMS.filter((p) => opts.platforms.includes(p)) : NATIVE_PLATFORMS;
@@ -217589,7 +217590,7 @@ ${cleanMatcOutput(stderr, tempName, basename3(entry.path))}`);
217589
217590
  ];
217590
217591
  entry.shadersLocal = true;
217591
217592
  for (const platform of staged) {
217592
- artifacts.push({ name: artifact(platform), absPath: join15(cacheDir, artifact(platform)) });
217593
+ artifacts.push({ name: artifact(platform), absPath: join14(cacheDir, artifact(platform)) });
217593
217594
  }
217594
217595
  } else {
217595
217596
  const order = ["opengl", "metal", "desktop", "webgl-lite"];
@@ -217605,6 +217606,7 @@ init_ignore();
217605
217606
 
217606
217607
  // src/commands/app/shared.ts
217607
217608
  init_manifest();
217609
+ init_config();
217608
217610
  init_ignore();
217609
217611
  init_cli();
217610
217612
  import { execFileSync } from "node:child_process";
@@ -217812,6 +217814,26 @@ var desktopDefaultViewport = (root) => {
217812
217814
  };
217813
217815
  var swiftPackageName = (url) => (url.replace(/\/+$/, "").split("/").pop() ?? url).replace(/\.git$/, "");
217814
217816
  var isLocalPluginSpec = (spec) => spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || spec.startsWith("file:");
217817
+ var resolveUpdateUrl = (update, root, sdkVersion) => {
217818
+ if (update === false)
217819
+ return;
217820
+ let url = typeof update === "string" ? update : typeof update === "object" ? update.url : undefined;
217821
+ if (!url) {
217822
+ let manifest = null;
217823
+ try {
217824
+ manifest = readManifest(root);
217825
+ } catch {}
217826
+ if (!manifest) {
217827
+ if (update)
217828
+ warn(`app.json "update": true needs a server-backed project — use "update": {"url": "https://…/app.js"} for custom hosting. OTA left unconfigured.`);
217829
+ return;
217830
+ }
217831
+ url = `${requireApiUrl(loadConfig())}/code/${manifest.uuid}.js?sdk=${sdkVersion}`;
217832
+ }
217833
+ if (!url.startsWith("https://"))
217834
+ warn(`update URL is not https — the phones block it on device (ATS / cleartext policy): ${url}`);
217835
+ return url;
217836
+ };
217815
217837
  var sdkSatisfies = (version, range) => {
217816
217838
  const op = range.startsWith("^") ? "^" : range.startsWith("~") ? "~" : "";
217817
217839
  const want = range.slice(op.length);
@@ -217901,6 +217923,15 @@ var resolveVariant = (requested, bundleJs, configKey = "ios.variant") => {
217901
217923
  }
217902
217924
  return { variant, detected };
217903
217925
  };
217926
+ var ANDROID_SDK_VARIANTS = [...SDK_VARIANTS, "vulkan"];
217927
+ var ANDROID_SDK_VULKAN_MIN = "2.1.0";
217928
+ var androidSdkArtifact = (variant, sdkVersion) => {
217929
+ if (variant === "vulkan" && sdkVersion && cmpVersion(sdkVersion, ANDROID_SDK_VULKAN_MIN) < 0) {
217930
+ throw new CliError(`android.variant "vulkan" needs lecodes-android-sdk >= ${ANDROID_SDK_VULKAN_MIN} (pinned: ${sdkVersion}) — run \`lecodes app update\` or set android.sdk.`);
217931
+ }
217932
+ return variant === "full" ? "lecodes-sdk" : `lecodes-sdk-${variant}`;
217933
+ };
217934
+ var androidVariantEngines = (variant) => variant === "vulkan" ? "full" : variant;
217904
217935
  var VERSION_RE = /^\d+\.\d+\.\d+$/;
217905
217936
  var cmpVersion = (a, b) => {
217906
217937
  const pa = a.split(".").map(Number);
@@ -219833,14 +219864,14 @@ android {
219833
219864
  minSdk = ${MIN_SDK}
219834
219865
  }
219835
219866
  compileOptions {
219836
- sourceCompatibility = JavaVersion.VERSION_11
219837
- targetCompatibility = JavaVersion.VERSION_11
219867
+ sourceCompatibility = JavaVersion.VERSION_17
219868
+ targetCompatibility = JavaVersion.VERSION_17
219838
219869
  }
219839
219870
  }
219840
219871
 
219841
219872
  kotlin {
219842
219873
  compilerOptions {
219843
- jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
219874
+ jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
219844
219875
  }
219845
219876
  }
219846
219877
 
@@ -219853,6 +219884,15 @@ dependencies {
219853
219884
  }
219854
219885
  `;
219855
219886
  };
219887
+ var runtimeManifestTemplate = (updateUrl) => `<?xml version="1.0" encoding="utf-8"?>
219888
+ <!-- Generated by \`lecodes app sync\` — regenerated on every sync, do not edit. -->
219889
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
219890
+ <application>${updateUrl ? `
219891
+ <!-- lecodes:update — native OTA source (app.json "update"); LeCodesShell.boot checks it at launch -->
219892
+ <meta-data android:name="io.letary.lecodes.UPDATE_URL" android:value="${updateUrl.replace(/&/g, "&amp;")}" />` : ""}
219893
+ </application>
219894
+ </manifest>
219895
+ `;
219856
219896
  var runtimeKotlinTemplate = (plugins, projectUuid) => {
219857
219897
  const identity = projectUuid ? ` engine.bootProjectUuid = "${projectUuid}"
219858
219898
  ` : "";
@@ -219915,14 +219955,14 @@ android {
219915
219955
  minSdk = ${MIN_SDK}${consumer}
219916
219956
  }
219917
219957
  compileOptions {
219918
- sourceCompatibility = JavaVersion.VERSION_11
219919
- targetCompatibility = JavaVersion.VERSION_11
219958
+ sourceCompatibility = JavaVersion.VERSION_17
219959
+ targetCompatibility = JavaVersion.VERSION_17
219920
219960
  }
219921
219961
  }
219922
219962
 
219923
219963
  kotlin {
219924
219964
  compilerOptions {
219925
- jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
219965
+ jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
219926
219966
  }
219927
219967
  }
219928
219968
 
@@ -219976,6 +220016,10 @@ engines the bundle uses and cannot see those, so a project using \`Physics\`,
219976
220016
  \`CharacterController\`, \`NavMesh\` or networking must set \`android.variant: "full"\` in
219977
220017
  \`../app.json\` — otherwise those aspects silently do nothing on device.
219978
220018
 
220019
+ \`android.variant: "vulkan"\` links \`lecodes-sdk-vulkan\`: the full engine set with Filament on
220020
+ Vulkan (OpenGL ES where the device has no Vulkan 1.1), \`arm64-v8a\` only — for 3D games. Never
220021
+ auto-selected; needs lecodes-android-sdk >= 2.1.0.
220022
+
219979
220023
  ## App-local native code
219980
220024
 
219981
220025
  Drop Kotlin files into \`app/src/main/java/\` and register them in \`MainActivity\` after the
@@ -220036,7 +220080,22 @@ the init-time placeholder.
220036
220080
 
220037
220081
  ## Notes
220038
220082
 
220039
- - No native OTA on Android yet — app.json \`"update"\` is ignored for this shell (iOS-only).
220083
+
220084
+ ## Over-the-air updates
220085
+
220086
+ Native, before the JS engine: \`LeCodesShell.boot(engine, context)\` in \`AppViewModel\` asks the
220087
+ source for newer code (a conditional GET; offline is a no-op), stores it and runs it on the NEXT
220088
+ launch — the current launch runs the newest LOCAL bundle. Configured in \`../app.json\`:
220089
+ \`"update": true\` = the project's live bundle on the platform (a pushed project), \`"update":
220090
+ { "url": "https://…/app.js" }\` = your own hosting, \`false\` = off. Sync writes the URL into
220091
+ \`lecodes-runtime/src/main/AndroidManifest.xml\` (meta-data \`io.letary.lecodes.UPDATE_URL\`). A
220092
+ download runs only when it is of this shell's SDK major and not older than the bundle running; a
220093
+ store update always wins over a downloaded bundle. A shell generated before OTA existed reads
220094
+ \`assets/app.js\` itself — replace that with:
220095
+
220096
+ \`\`\`kotlin
220097
+ if (!LeCodesShell.boot(engine, getApplication<Application>())) Log.e("LeCodes", "bundled app.js is missing — run \`lecodes app sync\`")
220098
+ \`\`\`
220040
220099
  - Push: the notification service needs \`app/google-services.json\` from the le.codes Firebase
220041
220100
  project; \`lecodes app sync\` applies the google-services Gradle plugin only when that file
220042
220101
  exists (and warns otherwise).
@@ -220096,14 +220155,14 @@ android {
220096
220155
  }
220097
220156
  }
220098
220157
  compileOptions {
220099
- sourceCompatibility = JavaVersion.VERSION_11
220100
- targetCompatibility = JavaVersion.VERSION_11
220158
+ sourceCompatibility = JavaVersion.VERSION_17
220159
+ targetCompatibility = JavaVersion.VERSION_17
220101
220160
  }
220102
220161
  }
220103
220162
 
220104
220163
  kotlin {
220105
220164
  compilerOptions {
220106
- jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
220165
+ jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
220107
220166
  }
220108
220167
  }
220109
220168
 
@@ -220243,6 +220302,7 @@ package ${pkg}
220243
220302
  import android.app.Activity
220244
220303
  import android.app.Application
220245
220304
  import androidx.lifecycle.AndroidViewModel
220305
+ import io.letary.lecodes.LeCodesShell
220246
220306
  import io.letary.lecodes.LecodesEngine
220247
220307
 
220248
220308
  class AppViewModel(application: Application) : AndroidViewModel(application) {
@@ -220266,8 +220326,14 @@ class AppViewModel(application: Application) : AndroidViewModel(application) {
220266
220326
  fun onActivityCreate(activity: Activity) {
220267
220327
  engine.attach(activity)
220268
220328
  if (!isCreated) {
220269
- val code = getApplication<Application>().assets.open("app.js").bufferedReader().use { it.readText() }
220270
- engine.run(code)
220329
+ // OTA (native, no JS involved): LeCodesShell.boot starts the check FIRST — even a bundle
220330
+ // that crashes on boot can never block its own fix — then runs the newest local bundle
220331
+ // (a downloaded update, or the embedded assets/app.js). New code applies on the next
220332
+ // launch. Configured via app.json "update" (→ the runtime module's manifest meta-data);
220333
+ // offline: no-op.
220334
+ if (!LeCodesShell.boot(engine, getApplication<Application>())) {
220335
+ android.util.Log.e("LeCodes", "bundled app.js is missing — run \`lecodes app sync\`")
220336
+ }
220271
220337
  isCreated = true
220272
220338
  }
220273
220339
  }
@@ -220667,9 +220733,8 @@ if "%OS%"=="Windows_NT" endlocal
220667
220733
 
220668
220734
  // src/commands/app/android.ts
220669
220735
  var ANDROID_SDK_REPO = "letary/lecodes-android-sdk";
220670
- var ANDROID_SDK_MIN_VERSION = "1.2.0";
220736
+ var ANDROID_SDK_MIN_VERSION = "2.0.13";
220671
220737
  var ANDROID_SDK_VARIANTS_MIN = "1.3.0";
220672
- var variantArtifact = (variant) => variant === "full" ? "lecodes-sdk" : `lecodes-sdk-${variant}`;
220673
220738
  var ANDROID_SDK_URL_DEFAULT = `https://github.com/${ANDROID_SDK_REPO}/releases/download`;
220674
220739
  var sdkRepoUrl = () => process.env.LECODES_ANDROID_SDK_URL ?? ANDROID_SDK_URL_DEFAULT;
220675
220740
  var ANDROID_SDK_OPTS = { repo: ANDROID_SDK_REPO, minVersion: ANDROID_SDK_MIN_VERSION, configKey: "android.sdk" };
@@ -220711,8 +220776,8 @@ var initAndroid = async (opts) => {
220711
220776
  }
220712
220777
  const existing = readJson2(join21(root, "app.json")) ?? {};
220713
220778
  const variant = opts.variant ?? existing.android?.variant ?? "auto";
220714
- if (variant !== "auto" && !SDK_VARIANTS.includes(variant)) {
220715
- throw new CliError(`--variant must be auto, ${SDK_VARIANTS.join(", ")} (got "${variant}")`);
220779
+ if (variant !== "auto" && !ANDROID_SDK_VARIANTS.includes(variant)) {
220780
+ throw new CliError(`--variant must be auto, ${ANDROID_SDK_VARIANTS.join(", ")} (got "${variant}")`);
220716
220781
  }
220717
220782
  const sdkFlag = opts.sdk ?? existing.android?.sdk;
220718
220783
  const sdkVersion = await resolveSdkVersion(process.env.LECODES_ANDROID_SDK ?? sdkFlag ?? "latest", null, ANDROID_SDK_OPTS);
@@ -220773,7 +220838,7 @@ var initAndroid = async (opts) => {
220773
220838
  write("app/src/main/res/values/strings.xml", stringsXmlTemplate(name));
220774
220839
  write("app/src/main/res/drawable/ic_launcher.xml", IC_LAUNCHER_XML);
220775
220840
  const initVariant = variant === "auto" ? "core" : variant;
220776
- const initArtifact = cmpVersion(sdkVersion, ANDROID_SDK_VARIANTS_MIN) >= 0 ? variantArtifact(initVariant) : "lecodes-sdk";
220841
+ const initArtifact = cmpVersion(sdkVersion, ANDROID_SDK_VARIANTS_MIN) >= 0 ? androidSdkArtifact(initVariant, sdkVersion) : "lecodes-sdk";
220777
220842
  write("lecodes-runtime/build.gradle.kts", runtimeBuildGradleTemplate(sdkVersion, [], initArtifact));
220778
220843
  write("lecodes-runtime/src/main/kotlin/io/letary/lecodes/runtime/LeCodesRuntime.kt", runtimeKotlinTemplate([]));
220779
220844
  log("");
@@ -220817,11 +220882,12 @@ var syncAndroid = async (root, config, prior) => {
220817
220882
  bundleJs = readFileSync18(join21(assetsDir, "app.js"), "utf8");
220818
220883
  }
220819
220884
  const requestedVariant = config.android?.variant ?? "auto";
220820
- if (requestedVariant !== "auto" && !SDK_VARIANTS.includes(requestedVariant)) {
220821
- throw new CliError(`app.json android.variant must be "auto" or one of ${SDK_VARIANTS.join(", ")} (got "${requestedVariant}").`);
220885
+ if (requestedVariant !== "auto" && !ANDROID_SDK_VARIANTS.includes(requestedVariant)) {
220886
+ throw new CliError(`app.json android.variant must be "auto" or one of ${ANDROID_SDK_VARIANTS.join(", ")} (got "${requestedVariant}").`);
220822
220887
  }
220823
- const { variant, detected } = resolveVariant(requestedVariant, bundleJs, "android.variant");
220824
- let sdkArtifact = variantArtifact(variant);
220888
+ const { variant: covered, detected } = resolveVariant(requestedVariant === "auto" ? "auto" : androidVariantEngines(requestedVariant), bundleJs, "android.variant");
220889
+ const variant = requestedVariant === "vulkan" ? "vulkan" : covered;
220890
+ let sdkArtifact = androidSdkArtifact(variant, sdkVersion);
220825
220891
  if (sdkArtifact !== "lecodes-sdk" && cmpVersion(sdkVersion, ANDROID_SDK_VARIANTS_MIN) < 0) {
220826
220892
  warn(`lecodes-android-sdk ${sdkVersion} predates the variant matrix (${ANDROID_SDK_VARIANTS_MIN}) — linking the full lecodes-sdk AAR; run \`lecodes app update\` to pick up variant builds.`);
220827
220893
  sdkArtifact = "lecodes-sdk";
@@ -220931,6 +220997,10 @@ var syncAndroid = async (root, config, prior) => {
220931
220997
  const runtimeKt = join21(androidDir, "lecodes-runtime", "src", "main", "kotlin", "io", "letary", "lecodes", "runtime", "LeCodesRuntime.kt");
220932
220998
  mkdirSync11(dirname12(runtimeKt), { recursive: true });
220933
220999
  writeFileSync14(runtimeKt, runtimeKotlinTemplate(runtimePlugins, shellUuid));
221000
+ const updateUrl = resolveUpdateUrl(config.update, root, sdkVersion);
221001
+ writeFileSync14(join21(androidDir, "lecodes-runtime", "src", "main", "AndroidManifest.xml"), runtimeManifestTemplate(updateUrl));
221002
+ if (updateUrl)
221003
+ log(`${c.green("update")} OTA source ${updateUrl} — checked natively at launch (LeCodesShell.boot), new code applies next launch`);
220934
221004
  if (pluginSpecs.length > 0 || existsSync19(lockPath))
220935
221005
  writeJson(lockPath, { plugins: newLock });
220936
221006
  restampAppBuildGradle(androidDir, config, gradlePlugins);
@@ -220938,9 +221008,6 @@ var syncAndroid = async (root, config, prior) => {
220938
221008
  await writeAndroidIcons(androidDir, await loadIconMaster(root, config));
220939
221009
  restampManifestIcon(androidDir);
220940
221010
  }
220941
- if (config.update) {
220942
- note(`app.json "update" is iOS-only for now — the Android runtime has no native updater, so it's ignored for this shell.`);
220943
- }
220944
221011
  log("");
220945
221012
  success(`Android shell synced: app.js + ${runtimePlugins.length} plugin(s), SDK ${sdkVersion}, v${config.version ?? "1.0.0"} (${config.android?.versionCode ?? 1}).`);
220946
221013
  note("Build & run from Android Studio (`lecodes app open android`) — user files in android/app/ were not touched.");
@@ -221450,8 +221517,8 @@ var doctorAndroidChecks = async (root, config, check) => {
221450
221517
  const hasSdkHome = !!(process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT) || existsSync19(join21(androidDir, "local.properties"));
221451
221518
  check(hasSdkHome, "Android SDK location", "set ANDROID_HOME or create android/local.properties");
221452
221519
  const requestedVariant = config.android?.variant ?? "auto";
221453
- const validVariant = requestedVariant === "auto" || SDK_VARIANTS.includes(requestedVariant);
221454
- check(validVariant, `android.variant "${requestedVariant}"`, `use "auto" or one of ${SDK_VARIANTS.join(", ")}`);
221520
+ const validVariant = requestedVariant === "auto" || ANDROID_SDK_VARIANTS.includes(requestedVariant);
221521
+ check(validVariant, `android.variant "${requestedVariant}"`, `use "auto" or one of ${ANDROID_SDK_VARIANTS.join(", ")}`);
221455
221522
  const wrapperJar = join21(androidDir, "gradle", "wrapper", "gradle-wrapper.jar");
221456
221523
  check(existsSync19(wrapperJar) && statSync5(wrapperJar).size > 0, "Gradle wrapper jar", `run \`gradle wrapper --gradle-version ${GRADLE_VERSION}\` in android/ (or let Android Studio repair it)`);
221457
221524
  check(existsSync19(join21(androidDir, "app", "src", "main", "assets", "app.js")), "Bundled app.js", "run `lecodes app sync`");
@@ -221467,6 +221534,14 @@ var doctorAndroidChecks = async (root, config, check) => {
221467
221534
  check(cmpVersion(linked, latest) >= 0, `SDK is the latest release (${latest})`, "run `lecodes app update`", false);
221468
221535
  } catch {}
221469
221536
  }
221537
+ if (config.update) {
221538
+ const manifest = join21(androidDir, "lecodes-runtime", "src", "main", "AndroidManifest.xml");
221539
+ check(existsSync19(manifest) && readFileSync18(manifest, "utf8").includes("io.letary.lecodes.UPDATE_URL"), "OTA update URL in the runtime manifest", "run `lecodes app sync`", false);
221540
+ const appSrc = join21(androidDir, "app", "src", "main", "java");
221541
+ const kt = existsSync19(appSrc) ? readdirSync5(appSrc, { recursive: true }).map(String).filter((f5) => f5.endsWith(".kt")) : [];
221542
+ const boots = kt.some((f5) => readFileSync18(join21(appSrc, f5), "utf8").includes("LeCodesShell.boot"));
221543
+ check(boots, "App code launches via the updater (LeCodesShell.boot)", 'no app/src/main/java/**/*.kt calls LeCodesShell.boot(engine, context) — downloaded updates would never run; see "Over-the-air updates" in android/README.md for the AppViewModel snippet', false);
221544
+ }
221470
221545
  const signing = resolveAndroidSigning(androidDir);
221471
221546
  check(!!signing, "Release signing key", "`lecodes app keystore create` or `keystore use <file>` (release builds are refused without one)", false);
221472
221547
  if (signing) {
@@ -222761,7 +222836,7 @@ var init = defineCommand({
222761
222836
  "bundle-id": str("iOS bundle identifier (default com.example.<slug>)", { value: "<id>" }),
222762
222837
  "app-id": str("Android applicationId (default com.example.<slug>)", { value: "<id>" }),
222763
222838
  team: str("Apple development team id", { value: "<id>" }),
222764
- variant: str("SDK variant: auto (default — sync picks the smallest of core/2d/3d/full covering what the bundle uses), or one of those to pin", { value: "<v>" }),
222839
+ variant: str("SDK variant: auto (default — sync picks the smallest of core/2d/3d/full covering what the bundle uses), or one of those to pin; Android also vulkan (full on Vulkan, arm64 only)", { value: "<v>" }),
222765
222840
  sdk: str("native SDK release (default: the newest one, pinned in app.json); 'latest' floats it — every sync re-resolves", { value: "<v|latest>" }),
222766
222841
  fullscreen: bool("(desktop) borderless fullscreen on the primary monitor — kiosks")
222767
222842
  },
@@ -222779,26 +222854,6 @@ var init = defineCommand({
222779
222854
  throw new UsageError("Which platform? `lecodes app init <ios|android|desktop>`");
222780
222855
  }
222781
222856
  });
222782
- var resolveUpdateUrl = (update, root, sdkVersion) => {
222783
- if (update === false)
222784
- return;
222785
- let url = typeof update === "string" ? update : typeof update === "object" ? update.url : undefined;
222786
- if (!url) {
222787
- let manifest = null;
222788
- try {
222789
- manifest = readManifest(root);
222790
- } catch {}
222791
- if (!manifest) {
222792
- if (update)
222793
- warn(`app.json "update": true needs a server-backed project — use "update": {"url": "https://…/app.js"} for custom hosting. OTA left unconfigured.`);
222794
- return;
222795
- }
222796
- url = `${requireApiUrl(loadConfig())}/code/${manifest.uuid}.js?sdk=${sdkVersion}`;
222797
- }
222798
- if (!url.startsWith("https://"))
222799
- warn(`update URL is not https — iOS blocks it on device (ATS): ${url}`);
222800
- return url;
222801
- };
222802
222857
  var runSync = async (platformArg) => {
222803
222858
  const root = projectRoot();
222804
222859
  const config = readJson2(join25(root, "app.json"));
@@ -223005,7 +223060,7 @@ var sync2 = defineCommand({
223005
223060
  usage: "[ios|android]",
223006
223061
  description: `Compiles the bundle and embeds the assets it references (ios/App/Resources/ resp. the android assets/; offline by default, also for server projects — app.json "resources": {"remote": [globs]} keeps heavy assets streaming), picks the SDK variant from the bundle's engine use, vendors the app.json plugins (pinned in plugins.lock.json) and regenerates the CLI-owned shell files. User files (ios/App/*.swift, android/app/) are never touched.
223007
223062
 
223008
- Native OTA JS updates at launch (iOS): on by default for a pushed project (its bundle on the platform, the launcher's own URL), app.json "update": {"url": …} for custom hosting, "update": false turns it off. app.json "icon" (a square ≥1024px image) renders the app icons every sync ("iconBackground": the adaptive / flatten backdrop). No platform = every shell that exists; a dual-shell project compiles once.`,
223063
+ Native OTA JS updates at launch (both shells; the current launch runs the newest LOCAL bundle, a download applies on the next): on by default for a pushed project (its bundle on the platform, the launcher's own URL), app.json "update": {"url": …} for custom hosting, "update": false turns it off. A download runs only when it is of the shell's SDK major and not older than the bundle running. app.json "icon" (a square ≥1024px image) renders the app icons every sync ("iconBackground": the adaptive / flatten backdrop). No platform = every shell that exists; a dual-shell project compiles once.`,
223009
223064
  examples: ["lecodes app sync", "lecodes app sync android"],
223010
223065
  run: ({ args }) => runSync(args[0])
223011
223066
  });
@@ -223256,8 +223311,14 @@ var doctorIosChecks = async (root, config, check) => {
223256
223311
  const plist = readFileSync20(join25(root, "ios", "App", "Info.plist"), "utf8");
223257
223312
  check(plist.includes("<key>LeCodesUpdateURL</key>"), "OTA update URL in Info.plist", "run `lecodes app sync`", false);
223258
223313
  const appDir = join25(root, "ios", "App");
223259
- const callsUpdater = readdirSync9(appDir).some((f5) => f5.endsWith(".swift") && readFileSync20(join25(appDir, f5), "utf8").includes("LeCodesUpdater.activeBundle"));
223260
- 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);
223314
+ const swift = readdirSync9(appDir).filter((f5) => f5.endsWith(".swift")).map((f5) => readFileSync20(join25(appDir, f5), "utf8"));
223315
+ const launchesViaUpdater = swift.some((t2) => t2.includes("LeCodesAppViewController") || t2.includes("LeCodesUpdater.activeBundle"));
223316
+ check(launchesViaUpdater, "App code launches via the updater", `no App/*.swift subclasses LeCodesAppViewController or calls LeCodesUpdater.activeBundle() — downloaded updates would never run; see "Over-the-air updates" in ios/README.md`, false);
223317
+ }
223318
+ {
223319
+ const vc = join25(root, "ios", "App", "ViewController.swift");
223320
+ const is2x = !existsSync23(vc) || readFileSync20(vc, "utf8").includes("LeCodesAppViewController");
223321
+ check(is2x, "App/ViewController.swift is the 2.x shell (LeCodesAppViewController)", "a 1.x shell's App/*.swift: replace AppDelegate.swift, SceneDelegate.swift and ViewController.swift with the ones `lecodes app init ios` writes today (your own code goes into ViewController.configure(_:))", false);
223261
223322
  }
223262
223323
  const asc = readJson2(join25(homedir10(), ".lecodes", "asc.json"));
223263
223324
  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);