lecodes-cli 2.0.13 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.0",
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.0",
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.0", 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) => ({
@@ -217173,7 +217173,6 @@ import { delimiter as delimiter3, dirname as dirname28, join as join59 } from "n
217173
217173
 
217174
217174
  // src/commands/app/index.ts
217175
217175
  init_manifest();
217176
- init_config();
217177
217176
  import { execFileSync as execFileSync6 } from "node:child_process";
217178
217177
  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
217178
  import { homedir as homedir10 } from "node:os";
@@ -217188,16 +217187,16 @@ import { basename as basename5, resolve as resolve5 } from "node:path";
217188
217187
  // src/compile/collect.ts
217189
217188
  init_localFiles();
217190
217189
  init_ignore();
217191
- import { readFileSync as readFileSync5 } from "node:fs";
217190
+ import { readFileSync as readFileSync4 } from "node:fs";
217192
217191
  import { basename } from "node:path";
217193
217192
 
217194
217193
  // src/project/materialize.ts
217195
217194
  init_api();
217196
217195
  init_paths();
217197
- import { dirname as dirname2, join as join5 } from "node:path";
217198
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "node:fs";
217196
+ import { dirname as dirname2, join as join4 } from "node:path";
217197
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "node:fs";
217199
217198
  var kindOf = (type) => type === "file" ? "resource" : type;
217200
- var absOf = (root, key) => join5(root, ...key.replace(/^\//, "").split("/"));
217199
+ var absOf = (root, key) => join4(root, ...key.replace(/^\//, "").split("/"));
217201
217200
  var buildPaths = (assets) => {
217202
217201
  const pathById = new Map;
217203
217202
  for (const asset of assets) {
@@ -217215,20 +217214,20 @@ var materializeAssets = async (root, project, apiUrl, token, onProgress) => {
217215
217214
  const key = pathById.get(asset.id);
217216
217215
  const abs = absOf(root, key);
217217
217216
  if (asset.type === "folder") {
217218
- mkdirSync3(abs, { recursive: true });
217217
+ mkdirSync2(abs, { recursive: true });
217219
217218
  continue;
217220
217219
  }
217221
- mkdirSync3(dirname2(abs), { recursive: true });
217220
+ mkdirSync2(dirname2(abs), { recursive: true });
217222
217221
  let sha;
217223
217222
  if (asset.type === "file") {
217224
217223
  if (!asset.file?.src)
217225
217224
  continue;
217226
217225
  const bytes = await downloadBlob(apiUrl, asset.file.src);
217227
- writeFileSync4(abs, bytes);
217226
+ writeFileSync3(abs, bytes);
217228
217227
  sha = sha256(bytes);
217229
217228
  } else {
217230
217229
  const text = asset.text ?? "";
217231
- writeFileSync4(abs, text);
217230
+ writeFileSync3(abs, text);
217232
217231
  sha = sha256(text);
217233
217232
  }
217234
217233
  files[key] = { id: asset.id, type: kindOf(asset.type), sha };
@@ -217262,7 +217261,7 @@ var collectEntries = (root, manifest, project, opts = {}) => {
217262
217261
  warnings.push(`No compiled shader for ${file.path} — push it to compile the shader.`);
217263
217262
  entries.push({ path: file.path, type, shaders, absPath: file.absPath });
217264
217263
  } else {
217265
- entries.push({ path: file.path, type, text: readFileSync5(file.absPath, "utf8") });
217264
+ entries.push({ path: file.path, type, text: readFileSync4(file.absPath, "utf8") });
217266
217265
  }
217267
217266
  }
217268
217267
  return { entries, warnings };
@@ -217271,7 +217270,7 @@ var collectEntries = (root, manifest, project, opts = {}) => {
217271
217270
  // src/compile/collectLocal.ts
217272
217271
  init_localFiles();
217273
217272
  init_ignore();
217274
- import { readFileSync as readFileSync6 } from "node:fs";
217273
+ import { readFileSync as readFileSync5 } from "node:fs";
217275
217274
  import { basename as basename2 } from "node:path";
217276
217275
  var collectLocalEntries = (root, manifest, assetBase, opts = {}) => {
217277
217276
  const entries = [];
@@ -217287,7 +217286,7 @@ var collectLocalEntries = (root, manifest, assetBase, opts = {}) => {
217287
217286
  }
217288
217287
  entries.push({ path: file.path, type, shaders: [], absPath: file.absPath });
217289
217288
  } else {
217290
- entries.push({ path: file.path, type, text: readFileSync6(file.absPath, "utf8") });
217289
+ entries.push({ path: file.path, type, text: readFileSync5(file.absPath, "utf8") });
217291
217290
  }
217292
217291
  }
217293
217292
  return { entries, warnings };
@@ -217301,26 +217300,26 @@ init_fonts();
217301
217300
  init_compile();
217302
217301
  import { execFile } from "node:child_process";
217303
217302
  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";
217303
+ import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync11, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync7 } from "node:fs";
217305
217304
  import { tmpdir } from "node:os";
217306
- import { basename as basename3, join as join15 } from "node:path";
217305
+ import { basename as basename3, join as join14 } from "node:path";
217307
217306
 
217308
217307
  // src/hosts/matcTool.ts
217309
217308
  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";
217309
+ import { homedir as homedir3 } from "node:os";
217310
+ import { join as join13, resolve as resolve3 } from "node:path";
217312
217311
 
217313
217312
  // src/hosts/releases.ts
217314
217313
  init_cli();
217315
217314
  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";
217315
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, mkdtempSync, readFileSync as readFileSync10, renameSync, rmSync, writeFileSync as writeFileSync6 } from "node:fs";
217316
+ import { join as join12 } from "node:path";
217318
217317
  import { spawnSync as spawnSync4 } from "node:child_process";
217319
217318
  var MANIFEST_URL = "https://storage.yandexcloud.net/le-codes/releases/versions.json";
217320
217319
  var ARTIFACT_STAMP = ".lecodes-artifact.json";
217321
217320
  var cachedArtifactSha = (dir) => {
217322
217321
  try {
217323
- return JSON.parse(readFileSync11(join13(dir, ARTIFACT_STAMP), "utf8")).sha256 ?? null;
217322
+ return JSON.parse(readFileSync10(join12(dir, ARTIFACT_STAMP), "utf8")).sha256 ?? null;
217324
217323
  } catch {
217325
217324
  return null;
217326
217325
  }
@@ -217366,16 +217365,16 @@ var downloadReleaseArchive = async (file, opts) => {
217366
217365
  const sha = createHash3("sha256").update(bytes).digest("hex");
217367
217366
  if (sha !== file.sha256)
217368
217367
  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-"));
217368
+ mkdirSync5(opts.cacheRoot, { recursive: true });
217369
+ const stage = mkdtempSync(join12(opts.cacheRoot, "dl-"));
217371
217370
  try {
217372
217371
  const archiveName = file.url.split("/").pop() ?? "archive";
217373
- const archive = join13(stage, archiveName);
217374
- writeFileSync7(archive, bytes);
217372
+ const archive = join12(stage, archiveName);
217373
+ writeFileSync6(archive, bytes);
217375
217374
  if (opts.extract) {
217376
217375
  opts.extract(archive, stage);
217377
217376
  } else {
217378
- const winTar = join13(process.env.SystemRoot ?? "C:\\Windows", "System32", "tar.exe");
217377
+ const winTar = join12(process.env.SystemRoot ?? "C:\\Windows", "System32", "tar.exe");
217379
217378
  const tarExe = process.platform === "win32" && existsSync10(winTar) ? winTar : "tar";
217380
217379
  const tar = spawnSync4(tarExe, ["-xf", archiveName], { cwd: stage, stdio: ["ignore", "ignore", "pipe"] });
217381
217380
  if (tar.status !== 0)
@@ -217385,7 +217384,7 @@ var downloadReleaseArchive = async (file, opts) => {
217385
217384
  const bad = opts.validate(stage);
217386
217385
  if (bad)
217387
217386
  throw new CliError(bad);
217388
- writeFileSync7(join13(stage, ARTIFACT_STAMP), JSON.stringify({ sha256: file.sha256, url: file.url }, null, 2));
217387
+ writeFileSync6(join12(stage, ARTIFACT_STAMP), JSON.stringify({ sha256: file.sha256, url: file.url }, null, 2));
217389
217388
  try {
217390
217389
  renameSync(stage, opts.dest);
217391
217390
  } catch {
@@ -217415,11 +217414,11 @@ var platformKey = () => {
217415
217414
  return macPlatformKey();
217416
217415
  throw new CliError(`matc isn't published for ${process.platform}.`);
217417
217416
  };
217418
- var cacheRoot = () => join14(homedir4(), ".lecodes", "matc");
217417
+ var cacheRoot = () => join13(homedir3(), ".lecodes", "matc");
217419
217418
  var findMatc = (dir) => {
217420
217419
  const name = matcName();
217421
217420
  for (const entry of readdirSync3(dir, { withFileTypes: true })) {
217422
- const p = join14(dir, entry.name);
217421
+ const p = join13(dir, entry.name);
217423
217422
  if (entry.isFile() && entry.name === name)
217424
217423
  return p;
217425
217424
  if (entry.isDirectory()) {
@@ -217432,7 +217431,7 @@ var findMatc = (dir) => {
217432
217431
  };
217433
217432
  var installedMatcVersions = () => {
217434
217433
  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);
217434
+ 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
217435
  } catch {
217437
217436
  return [];
217438
217437
  }
@@ -217462,7 +217461,7 @@ var resolveMatcExeCached = () => {
217462
217461
  return resolve3(override);
217463
217462
  }
217464
217463
  const cached = installedMatcVersions()[0];
217465
- return cached ? findMatc(join14(cacheRoot(), `matc-${cached}`)) : null;
217464
+ return cached ? findMatc(join13(cacheRoot(), `matc-${cached}`)) : null;
217466
217465
  };
217467
217466
  var resolveMatcExe = async () => {
217468
217467
  const cached = resolveMatcExeCached();
@@ -217470,7 +217469,7 @@ var resolveMatcExe = async () => {
217470
217469
  return cached;
217471
217470
  const { versions } = await fetchMatcManifest();
217472
217471
  const { version, file } = pickMatc(versions);
217473
- const dir = join14(cacheRoot(), `matc-${version}`);
217472
+ const dir = join13(cacheRoot(), `matc-${version}`);
217474
217473
  await downloadMatc(version, file, dir);
217475
217474
  const exe = findMatc(dir);
217476
217475
  if (!exe)
@@ -217481,7 +217480,7 @@ var updateMatc = async () => {
217481
217480
  const { versions } = await fetchMatcManifest();
217482
217481
  const { version, file } = pickMatc(versions);
217483
217482
  const before = installedMatcVersions()[0];
217484
- const dir = join14(cacheRoot(), `matc-${version}`);
217483
+ const dir = join13(cacheRoot(), `matc-${version}`);
217485
217484
  if (existsSync11(dir) && findMatc(dir) !== null) {
217486
217485
  note(`Already up to date: matc ${version}.`);
217487
217486
  return;
@@ -217494,7 +217493,7 @@ var updateMatc = async () => {
217494
217493
  init_manifest();
217495
217494
  init_cli();
217496
217495
  var SHADERS_DIR = "shaders";
217497
- var shadersCacheDir = (root) => join15(root, LECODES_DIR, SHADERS_DIR);
217496
+ var shadersCacheDir = (root) => join14(root, LECODES_DIR, SHADERS_DIR);
217498
217497
  var isShaderArtifactName = (name) => /^[0-9a-f]{16}_[a-z-]+\.filamat$/.test(name);
217499
217498
  var matcIdentity = (exe) => {
217500
217499
  const stat = statSync2(exe);
@@ -217504,17 +217503,17 @@ var shaderHash = (text, matcId) => createHash4("sha256").update(`${TARGET_MATRIX
217504
217503
  var artifactFamily = () => [...MATC_PLATFORMS, ...LOCAL_ONLY_PLATFORMS, "webgl-lite"];
217505
217504
  var shaderIsCompiled = (root, text, matcId) => {
217506
217505
  const hash = shaderHash(text, matcId);
217507
- return artifactFamily().every((p) => existsSync12(join15(shadersCacheDir(root), `${hash}_${p}.filamat`)));
217506
+ return artifactFamily().every((p) => existsSync12(join14(shadersCacheDir(root), `${hash}_${p}.filamat`)));
217508
217507
  };
217509
217508
  var shaderCompiledPlatforms = (root, text, matcId) => {
217510
217509
  const hash = shaderHash(text, matcId);
217511
- return artifactFamily().filter((p) => existsSync12(join15(shadersCacheDir(root), `${hash}_${p}.filamat`)));
217510
+ return artifactFamily().filter((p) => existsSync12(join14(shadersCacheDir(root), `${hash}_${p}.filamat`)));
217512
217511
  };
217513
217512
  var localShaderArtifact = (root, text, platform) => {
217514
217513
  const matcExe = resolveMatcExeCached();
217515
217514
  if (!matcExe)
217516
217515
  return null;
217517
- const file = join15(shadersCacheDir(root), `${shaderHash(text, matcIdentity(matcExe))}_${platform}.filamat`);
217516
+ const file = join14(shadersCacheDir(root), `${shaderHash(text, matcIdentity(matcExe))}_${platform}.filamat`);
217518
217517
  return existsSync12(file) ? file : null;
217519
217518
  };
217520
217519
  var allShaderPlatforms = () => [...Object.keys(ALL_PLATFORM_BUILD), "webgl-lite"];
@@ -217548,25 +217547,25 @@ var applyLocalShaders = async (entries, opts) => {
217548
217547
  }
217549
217548
  const matcId = matcIdentity(matcExe);
217550
217549
  const cacheDir = shadersCacheDir(opts.root);
217551
- mkdirSync7(cacheDir, { recursive: true });
217550
+ mkdirSync6(cacheDir, { recursive: true });
217552
217551
  const artifacts = [];
217553
217552
  for (const entry of shaderEntries) {
217554
- const text = readFileSync12(entry.absPath, "utf8");
217553
+ const text = readFileSync11(entry.absPath, "utf8");
217555
217554
  const hash = shaderHash(text, matcId);
217556
217555
  const family = artifactFamily();
217557
217556
  const artifact = (platform) => `${hash}_${platform}.filamat`;
217558
- const has = (platform) => existsSync12(join15(cacheDir, artifact(platform)));
217557
+ const has = (platform) => existsSync12(join14(cacheDir, artifact(platform)));
217559
217558
  const wanted = opts.platforms ? family.filter((p) => opts.platforms.includes(p)) : family;
217560
217559
  const missing = wanted.filter((p) => !has(p));
217561
217560
  const matcMissing = missing.filter((p) => p !== "webgl-lite");
217562
217561
  if (matcMissing.length > 0) {
217563
217562
  const tempName = `lecodes-matc-${randomBytes(6).toString("hex")}.mat`;
217564
- const tempFile = join15(tmpdir(), tempName);
217563
+ const tempFile = join14(tmpdir(), tempName);
217565
217564
  try {
217566
217565
  for (const platform of matcMissing) {
217567
217566
  const build = ALL_PLATFORM_BUILD[platform];
217568
- writeFileSync8(tempFile, build.replaceExternal ? text.replace(externalSamplerReg, "type:sampler2d") : text);
217569
- const out = join15(cacheDir, artifact(platform));
217567
+ writeFileSync7(tempFile, build.replaceExternal ? text.replace(externalSamplerReg, "type:sampler2d") : text);
217568
+ const out = join14(cacheDir, artifact(platform));
217570
217569
  const { ok, stderr } = await runMatc(matcExe, matcArgs(platform, out, tempFile));
217571
217570
  if (!ok) {
217572
217571
  rmSync2(out, { force: true });
@@ -217579,7 +217578,7 @@ ${cleanMatcOutput(stderr, tempName, basename3(entry.path))}`);
217579
217578
  }
217580
217579
  }
217581
217580
  if (missing.includes("webgl-lite")) {
217582
- writeFileSync8(join15(cacheDir, artifact("webgl-lite")), JSON.stringify(buildLiteMaterialArtifact(text)));
217581
+ writeFileSync7(join14(cacheDir, artifact("webgl-lite")), JSON.stringify(buildLiteMaterialArtifact(text)));
217583
217582
  }
217584
217583
  if (opts.local) {
217585
217584
  const staged = opts.platforms ? ALL_NATIVE_PLATFORMS.filter((p) => opts.platforms.includes(p)) : NATIVE_PLATFORMS;
@@ -217589,7 +217588,7 @@ ${cleanMatcOutput(stderr, tempName, basename3(entry.path))}`);
217589
217588
  ];
217590
217589
  entry.shadersLocal = true;
217591
217590
  for (const platform of staged) {
217592
- artifacts.push({ name: artifact(platform), absPath: join15(cacheDir, artifact(platform)) });
217591
+ artifacts.push({ name: artifact(platform), absPath: join14(cacheDir, artifact(platform)) });
217593
217592
  }
217594
217593
  } else {
217595
217594
  const order = ["opengl", "metal", "desktop", "webgl-lite"];
@@ -217605,6 +217604,7 @@ init_ignore();
217605
217604
 
217606
217605
  // src/commands/app/shared.ts
217607
217606
  init_manifest();
217607
+ init_config();
217608
217608
  init_ignore();
217609
217609
  init_cli();
217610
217610
  import { execFileSync } from "node:child_process";
@@ -217812,6 +217812,26 @@ var desktopDefaultViewport = (root) => {
217812
217812
  };
217813
217813
  var swiftPackageName = (url) => (url.replace(/\/+$/, "").split("/").pop() ?? url).replace(/\.git$/, "");
217814
217814
  var isLocalPluginSpec = (spec) => spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || spec.startsWith("file:");
217815
+ var resolveUpdateUrl = (update, root, sdkVersion) => {
217816
+ if (update === false)
217817
+ return;
217818
+ let url = typeof update === "string" ? update : typeof update === "object" ? update.url : undefined;
217819
+ if (!url) {
217820
+ let manifest = null;
217821
+ try {
217822
+ manifest = readManifest(root);
217823
+ } catch {}
217824
+ if (!manifest) {
217825
+ if (update)
217826
+ warn(`app.json "update": true needs a server-backed project — use "update": {"url": "https://…/app.js"} for custom hosting. OTA left unconfigured.`);
217827
+ return;
217828
+ }
217829
+ url = `${requireApiUrl(loadConfig())}/code/${manifest.uuid}.js?sdk=${sdkVersion}`;
217830
+ }
217831
+ if (!url.startsWith("https://"))
217832
+ warn(`update URL is not https — the phones block it on device (ATS / cleartext policy): ${url}`);
217833
+ return url;
217834
+ };
217815
217835
  var sdkSatisfies = (version, range) => {
217816
217836
  const op = range.startsWith("^") ? "^" : range.startsWith("~") ? "~" : "";
217817
217837
  const want = range.slice(op.length);
@@ -219833,14 +219853,14 @@ android {
219833
219853
  minSdk = ${MIN_SDK}
219834
219854
  }
219835
219855
  compileOptions {
219836
- sourceCompatibility = JavaVersion.VERSION_11
219837
- targetCompatibility = JavaVersion.VERSION_11
219856
+ sourceCompatibility = JavaVersion.VERSION_17
219857
+ targetCompatibility = JavaVersion.VERSION_17
219838
219858
  }
219839
219859
  }
219840
219860
 
219841
219861
  kotlin {
219842
219862
  compilerOptions {
219843
- jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
219863
+ jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
219844
219864
  }
219845
219865
  }
219846
219866
 
@@ -219853,6 +219873,15 @@ dependencies {
219853
219873
  }
219854
219874
  `;
219855
219875
  };
219876
+ var runtimeManifestTemplate = (updateUrl) => `<?xml version="1.0" encoding="utf-8"?>
219877
+ <!-- Generated by \`lecodes app sync\` — regenerated on every sync, do not edit. -->
219878
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
219879
+ <application>${updateUrl ? `
219880
+ <!-- lecodes:update — native OTA source (app.json "update"); LeCodesShell.boot checks it at launch -->
219881
+ <meta-data android:name="io.letary.lecodes.UPDATE_URL" android:value="${updateUrl.replace(/&/g, "&amp;")}" />` : ""}
219882
+ </application>
219883
+ </manifest>
219884
+ `;
219856
219885
  var runtimeKotlinTemplate = (plugins, projectUuid) => {
219857
219886
  const identity = projectUuid ? ` engine.bootProjectUuid = "${projectUuid}"
219858
219887
  ` : "";
@@ -219915,14 +219944,14 @@ android {
219915
219944
  minSdk = ${MIN_SDK}${consumer}
219916
219945
  }
219917
219946
  compileOptions {
219918
- sourceCompatibility = JavaVersion.VERSION_11
219919
- targetCompatibility = JavaVersion.VERSION_11
219947
+ sourceCompatibility = JavaVersion.VERSION_17
219948
+ targetCompatibility = JavaVersion.VERSION_17
219920
219949
  }
219921
219950
  }
219922
219951
 
219923
219952
  kotlin {
219924
219953
  compilerOptions {
219925
- jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
219954
+ jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
219926
219955
  }
219927
219956
  }
219928
219957
 
@@ -220036,7 +220065,22 @@ the init-time placeholder.
220036
220065
 
220037
220066
  ## Notes
220038
220067
 
220039
- - No native OTA on Android yet — app.json \`"update"\` is ignored for this shell (iOS-only).
220068
+
220069
+ ## Over-the-air updates
220070
+
220071
+ Native, before the JS engine: \`LeCodesShell.boot(engine, context)\` in \`AppViewModel\` asks the
220072
+ source for newer code (a conditional GET; offline is a no-op), stores it and runs it on the NEXT
220073
+ launch — the current launch runs the newest LOCAL bundle. Configured in \`../app.json\`:
220074
+ \`"update": true\` = the project's live bundle on the platform (a pushed project), \`"update":
220075
+ { "url": "https://…/app.js" }\` = your own hosting, \`false\` = off. Sync writes the URL into
220076
+ \`lecodes-runtime/src/main/AndroidManifest.xml\` (meta-data \`io.letary.lecodes.UPDATE_URL\`). A
220077
+ download runs only when it is of this shell's SDK major and not older than the bundle running; a
220078
+ store update always wins over a downloaded bundle. A shell generated before OTA existed reads
220079
+ \`assets/app.js\` itself — replace that with:
220080
+
220081
+ \`\`\`kotlin
220082
+ if (!LeCodesShell.boot(engine, getApplication<Application>())) Log.e("LeCodes", "bundled app.js is missing — run \`lecodes app sync\`")
220083
+ \`\`\`
220040
220084
  - Push: the notification service needs \`app/google-services.json\` from the le.codes Firebase
220041
220085
  project; \`lecodes app sync\` applies the google-services Gradle plugin only when that file
220042
220086
  exists (and warns otherwise).
@@ -220096,14 +220140,14 @@ android {
220096
220140
  }
220097
220141
  }
220098
220142
  compileOptions {
220099
- sourceCompatibility = JavaVersion.VERSION_11
220100
- targetCompatibility = JavaVersion.VERSION_11
220143
+ sourceCompatibility = JavaVersion.VERSION_17
220144
+ targetCompatibility = JavaVersion.VERSION_17
220101
220145
  }
220102
220146
  }
220103
220147
 
220104
220148
  kotlin {
220105
220149
  compilerOptions {
220106
- jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
220150
+ jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
220107
220151
  }
220108
220152
  }
220109
220153
 
@@ -220243,6 +220287,7 @@ package ${pkg}
220243
220287
  import android.app.Activity
220244
220288
  import android.app.Application
220245
220289
  import androidx.lifecycle.AndroidViewModel
220290
+ import io.letary.lecodes.LeCodesShell
220246
220291
  import io.letary.lecodes.LecodesEngine
220247
220292
 
220248
220293
  class AppViewModel(application: Application) : AndroidViewModel(application) {
@@ -220266,8 +220311,14 @@ class AppViewModel(application: Application) : AndroidViewModel(application) {
220266
220311
  fun onActivityCreate(activity: Activity) {
220267
220312
  engine.attach(activity)
220268
220313
  if (!isCreated) {
220269
- val code = getApplication<Application>().assets.open("app.js").bufferedReader().use { it.readText() }
220270
- engine.run(code)
220314
+ // OTA (native, no JS involved): LeCodesShell.boot starts the check FIRST — even a bundle
220315
+ // that crashes on boot can never block its own fix — then runs the newest local bundle
220316
+ // (a downloaded update, or the embedded assets/app.js). New code applies on the next
220317
+ // launch. Configured via app.json "update" (→ the runtime module's manifest meta-data);
220318
+ // offline: no-op.
220319
+ if (!LeCodesShell.boot(engine, getApplication<Application>())) {
220320
+ android.util.Log.e("LeCodes", "bundled app.js is missing — run \`lecodes app sync\`")
220321
+ }
220271
220322
  isCreated = true
220272
220323
  }
220273
220324
  }
@@ -220667,7 +220718,7 @@ if "%OS%"=="Windows_NT" endlocal
220667
220718
 
220668
220719
  // src/commands/app/android.ts
220669
220720
  var ANDROID_SDK_REPO = "letary/lecodes-android-sdk";
220670
- var ANDROID_SDK_MIN_VERSION = "1.2.0";
220721
+ var ANDROID_SDK_MIN_VERSION = "2.0.13";
220671
220722
  var ANDROID_SDK_VARIANTS_MIN = "1.3.0";
220672
220723
  var variantArtifact = (variant) => variant === "full" ? "lecodes-sdk" : `lecodes-sdk-${variant}`;
220673
220724
  var ANDROID_SDK_URL_DEFAULT = `https://github.com/${ANDROID_SDK_REPO}/releases/download`;
@@ -220931,6 +220982,10 @@ var syncAndroid = async (root, config, prior) => {
220931
220982
  const runtimeKt = join21(androidDir, "lecodes-runtime", "src", "main", "kotlin", "io", "letary", "lecodes", "runtime", "LeCodesRuntime.kt");
220932
220983
  mkdirSync11(dirname12(runtimeKt), { recursive: true });
220933
220984
  writeFileSync14(runtimeKt, runtimeKotlinTemplate(runtimePlugins, shellUuid));
220985
+ const updateUrl = resolveUpdateUrl(config.update, root, sdkVersion);
220986
+ writeFileSync14(join21(androidDir, "lecodes-runtime", "src", "main", "AndroidManifest.xml"), runtimeManifestTemplate(updateUrl));
220987
+ if (updateUrl)
220988
+ log(`${c.green("update")} OTA source ${updateUrl} — checked natively at launch (LeCodesShell.boot), new code applies next launch`);
220934
220989
  if (pluginSpecs.length > 0 || existsSync19(lockPath))
220935
220990
  writeJson(lockPath, { plugins: newLock });
220936
220991
  restampAppBuildGradle(androidDir, config, gradlePlugins);
@@ -220938,9 +220993,6 @@ var syncAndroid = async (root, config, prior) => {
220938
220993
  await writeAndroidIcons(androidDir, await loadIconMaster(root, config));
220939
220994
  restampManifestIcon(androidDir);
220940
220995
  }
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
220996
  log("");
220945
220997
  success(`Android shell synced: app.js + ${runtimePlugins.length} plugin(s), SDK ${sdkVersion}, v${config.version ?? "1.0.0"} (${config.android?.versionCode ?? 1}).`);
220946
220998
  note("Build & run from Android Studio (`lecodes app open android`) — user files in android/app/ were not touched.");
@@ -221467,6 +221519,14 @@ var doctorAndroidChecks = async (root, config, check) => {
221467
221519
  check(cmpVersion(linked, latest) >= 0, `SDK is the latest release (${latest})`, "run `lecodes app update`", false);
221468
221520
  } catch {}
221469
221521
  }
221522
+ if (config.update) {
221523
+ const manifest = join21(androidDir, "lecodes-runtime", "src", "main", "AndroidManifest.xml");
221524
+ check(existsSync19(manifest) && readFileSync18(manifest, "utf8").includes("io.letary.lecodes.UPDATE_URL"), "OTA update URL in the runtime manifest", "run `lecodes app sync`", false);
221525
+ const appSrc = join21(androidDir, "app", "src", "main", "java");
221526
+ const kt = existsSync19(appSrc) ? readdirSync5(appSrc, { recursive: true }).map(String).filter((f5) => f5.endsWith(".kt")) : [];
221527
+ const boots = kt.some((f5) => readFileSync18(join21(appSrc, f5), "utf8").includes("LeCodesShell.boot"));
221528
+ 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);
221529
+ }
221470
221530
  const signing = resolveAndroidSigning(androidDir);
221471
221531
  check(!!signing, "Release signing key", "`lecodes app keystore create` or `keystore use <file>` (release builds are refused without one)", false);
221472
221532
  if (signing) {
@@ -222779,26 +222839,6 @@ var init = defineCommand({
222779
222839
  throw new UsageError("Which platform? `lecodes app init <ios|android|desktop>`");
222780
222840
  }
222781
222841
  });
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
222842
  var runSync = async (platformArg) => {
222803
222843
  const root = projectRoot();
222804
222844
  const config = readJson2(join25(root, "app.json"));
@@ -223005,7 +223045,7 @@ var sync2 = defineCommand({
223005
223045
  usage: "[ios|android]",
223006
223046
  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
223047
 
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.`,
223048
+ 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
223049
  examples: ["lecodes app sync", "lecodes app sync android"],
223010
223050
  run: ({ args }) => runSync(args[0])
223011
223051
  });
@@ -223256,8 +223296,14 @@ var doctorIosChecks = async (root, config, check) => {
223256
223296
  const plist = readFileSync20(join25(root, "ios", "App", "Info.plist"), "utf8");
223257
223297
  check(plist.includes("<key>LeCodesUpdateURL</key>"), "OTA update URL in Info.plist", "run `lecodes app sync`", false);
223258
223298
  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);
223299
+ const swift = readdirSync9(appDir).filter((f5) => f5.endsWith(".swift")).map((f5) => readFileSync20(join25(appDir, f5), "utf8"));
223300
+ const launchesViaUpdater = swift.some((t2) => t2.includes("LeCodesAppViewController") || t2.includes("LeCodesUpdater.activeBundle"));
223301
+ 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);
223302
+ }
223303
+ {
223304
+ const vc = join25(root, "ios", "App", "ViewController.swift");
223305
+ const is2x = !existsSync23(vc) || readFileSync20(vc, "utf8").includes("LeCodesAppViewController");
223306
+ 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
223307
  }
223262
223308
  const asc = readJson2(join25(homedir10(), ".lecodes", "asc.json"));
223263
223309
  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);