lecodes-cli 0.14.2 → 0.16.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
@@ -864,6 +864,18 @@ var buildHeader = (name, code, opts = {}) => {
864
864
  header += `// icon: ${opts.icon}
865
865
  `;
866
866
  header += `// last-updated: ${new Date().toISOString()}
867
+ `;
868
+ if (!code.includes("_creator.createScene") && !code.includes("_creator.createEntity"))
869
+ header += `// gl: disable
870
+ `;
871
+ else if (code.includes("_creator.createEngine"))
872
+ header += `// gl: defer
873
+ `;
874
+ if (code.includes("_creator2d."))
875
+ header += `// 2d: enable
876
+ `;
877
+ if (code.includes("_creator.createARController"))
878
+ header += `// ar: enable
867
879
  `;
868
880
  const localAssets = new Set;
869
881
  for (const m of code.matchAll(/_creatorUtils\.fetchLocal\((.+?)\)/g)) {
@@ -882,17 +894,6 @@ var buildHeader = (name, code, opts = {}) => {
882
894
  if (opts.icons)
883
895
  header += `// icons: ${opts.icons}
884
896
  `;
885
- if (!code.includes("_creator.createScene") && !code.includes("_creator.createEntity"))
886
- header += `// gl: disable
887
- `;
888
- else if (code.includes("_creator.createEngine"))
889
- header += `// gl: defer
890
- `;
891
- if (code.includes("_creator2d."))
892
- header += `// 2d: enable
893
- `;
894
- if (code.includes("_creator.createARController"))
895
- header += "// ar: enable";
896
897
  header += `
897
898
  `;
898
899
  return header;
@@ -42820,65 +42821,65 @@ var init_esm30 = __esm(() => {
42820
42821
  });
42821
42822
 
42822
42823
  // src/types.ts
42823
- import { dirname as dirname12, join as join16, relative as relative3, sep } from "node:path";
42824
- import { existsSync as existsSync15, mkdirSync as mkdirSync10, readdirSync as readdirSync6, readFileSync as readFileSync15, rmSync as rmSync3, statSync as statSync4, writeFileSync as writeFileSync12 } from "node:fs";
42824
+ import { dirname as dirname13, join as join18, relative as relative3, sep } from "node:path";
42825
+ import { existsSync as existsSync17, mkdirSync as mkdirSync12, readdirSync as readdirSync8, readFileSync as readFileSync16, rmSync as rmSync5, statSync as statSync6, writeFileSync as writeFileSync14 } from "node:fs";
42825
42826
  var TSCONFIG, listBundleFiles = (dir, base = dir) => {
42826
- if (!existsSync15(dir))
42827
+ if (!existsSync17(dir))
42827
42828
  return [];
42828
42829
  const out = [];
42829
- for (const name of readdirSync6(dir)) {
42830
- const path2 = join16(dir, name);
42831
- if (statSync4(path2).isDirectory())
42830
+ for (const name of readdirSync8(dir)) {
42831
+ const path2 = join18(dir, name);
42832
+ if (statSync6(path2).isDirectory())
42832
42833
  out.push(...listBundleFiles(path2, base));
42833
42834
  else
42834
42835
  out.push(relative3(base, path2).split(sep).join("/"));
42835
42836
  }
42836
42837
  return out;
42837
42838
  }, pruneEmptyDirs = (dir, root) => {
42838
- if (!existsSync15(dir) || !statSync4(dir).isDirectory())
42839
+ if (!existsSync17(dir) || !statSync6(dir).isDirectory())
42839
42840
  return;
42840
- for (const name of readdirSync6(dir))
42841
- pruneEmptyDirs(join16(dir, name), root);
42842
- if (dir !== root && readdirSync6(dir).length === 0)
42843
- rmSync3(dir, { recursive: true });
42841
+ for (const name of readdirSync8(dir))
42842
+ pruneEmptyDirs(join18(dir, name), root);
42843
+ if (dir !== root && readdirSync8(dir).length === 0)
42844
+ rmSync5(dir, { recursive: true });
42844
42845
  }, writeTypeBundle = (root, bundle) => {
42845
- const typesDir = join16(root, LECODES_DIR, "types");
42846
+ const typesDir = join18(root, LECODES_DIR, "types");
42846
42847
  const before = new Set(listBundleFiles(typesDir));
42847
42848
  const stats = { added: 0, updated: 0, removed: 0, total: Object.keys(bundle).length };
42848
42849
  for (const [path2, text] of Object.entries(bundle)) {
42849
- const dest = join16(typesDir, path2);
42850
+ const dest = join18(typesDir, path2);
42850
42851
  if (!before.has(path2))
42851
42852
  stats.added++;
42852
- else if (readFileSync15(dest, "utf8") !== text)
42853
+ else if (readFileSync16(dest, "utf8") !== text)
42853
42854
  stats.updated++;
42854
42855
  else
42855
42856
  continue;
42856
- mkdirSync10(dirname12(dest), { recursive: true });
42857
- writeFileSync12(dest, text);
42857
+ mkdirSync12(dirname13(dest), { recursive: true });
42858
+ writeFileSync14(dest, text);
42858
42859
  }
42859
42860
  for (const path2 of before) {
42860
42861
  if (path2 in bundle)
42861
42862
  continue;
42862
- rmSync3(join16(typesDir, path2));
42863
+ rmSync5(join18(typesDir, path2));
42863
42864
  stats.removed++;
42864
42865
  }
42865
42866
  pruneEmptyDirs(typesDir, typesDir);
42866
42867
  return stats;
42867
42868
  }, bundleHash = (bundle) => sha256(Object.keys(bundle).sort().map((k) => `${k}\x00${bundle[k]}`).join("\x00")), readProjectTypeBundle = (root) => {
42868
- const typesDir = join16(root, LECODES_DIR, "types");
42869
+ const typesDir = join18(root, LECODES_DIR, "types");
42869
42870
  const bundle = {};
42870
42871
  for (const path2 of listBundleFiles(typesDir))
42871
- bundle[path2] = readFileSync15(join16(typesDir, path2), "utf8");
42872
+ bundle[path2] = readFileSync16(join18(typesDir, path2), "utf8");
42872
42873
  return bundle;
42873
42874
  }, materializeTypes = async (root, apiUrl) => writeTypeBundle(root, await getTypes(apiUrl)), readLocalTypeBundle = () => {
42874
42875
  const candidates = [
42875
- join16(distRoot, "runtime", "sdk-types.json"),
42876
- join16(distRoot, "..", "sdk", "dist", "types.json")
42876
+ join18(distRoot, "runtime", "sdk-types.json"),
42877
+ join18(distRoot, "..", "sdk", "dist", "types.json")
42877
42878
  ];
42878
42879
  for (const path2 of candidates) {
42879
- if (existsSync15(path2)) {
42880
+ if (existsSync17(path2)) {
42880
42881
  try {
42881
- return JSON.parse(readFileSync15(path2, "utf8"));
42882
+ return JSON.parse(readFileSync16(path2, "utf8"));
42882
42883
  } catch {}
42883
42884
  }
42884
42885
  }
@@ -42890,7 +42891,7 @@ var TSCONFIG, listBundleFiles = (dir, base = dir) => {
42890
42891
  writeTypeBundle(root, bundle);
42891
42892
  return true;
42892
42893
  }, writeTsconfig = (root) => {
42893
- writeFileSync12(join16(root, "tsconfig.json"), JSON.stringify(TSCONFIG, null, 2));
42894
+ writeFileSync14(join18(root, "tsconfig.json"), JSON.stringify(TSCONFIG, null, 2));
42894
42895
  };
42895
42896
  var init_types4 = __esm(() => {
42896
42897
  init_api();
@@ -42917,13 +42918,13 @@ var init_types4 = __esm(() => {
42917
42918
 
42918
42919
  // src/compile/designCompile.ts
42919
42920
  import { readFileSync as readFileSync17 } from "node:fs";
42920
- import { basename as basename6 } from "node:path";
42921
+ import { basename as basename7 } from "node:path";
42921
42922
  var LOCAL_ASSET_SCHEME = "lecodesfile://", compileDesignScreen = async (opts) => {
42922
42923
  const { root, manifest, designDir, designKey, screenId, state } = opts;
42923
42924
  const entries = [];
42924
42925
  const scan2 = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest?.files ?? {})) });
42925
42926
  for (const file of scan2.files) {
42926
- const type = manifest?.files[file.path]?.type ?? classifyKind(basename6(file.path));
42927
+ const type = manifest?.files[file.path]?.type ?? classifyKind(basename7(file.path));
42927
42928
  if (type === "resource") {
42928
42929
  entries.push({
42929
42930
  path: file.path,
@@ -42979,7 +42980,7 @@ var init_designCompile = __esm(() => {
42979
42980
 
42980
42981
  // src/browserAuth.ts
42981
42982
  import { createServer } from "node:http";
42982
- import { spawn as spawn3 } from "node:child_process";
42983
+ import { spawn as spawn4 } from "node:child_process";
42983
42984
  import { randomBytes } from "node:crypto";
42984
42985
  import { hostname } from "node:os";
42985
42986
  var SUCCESS_HTML = `<!doctype html><meta charset="utf-8"><title>lecodes</title>
@@ -43004,16 +43005,16 @@ var SUCCESS_HTML = `<!doctype html><meta charset="utf-8"><title>lecodes</title>
43004
43005
  args = [url];
43005
43006
  }
43006
43007
  try {
43007
- const child = spawn3(cmd, args, { stdio: "ignore", detached: true });
43008
+ const child = spawn4(cmd, args, { stdio: "ignore", detached: true });
43008
43009
  child.on("error", () => {});
43009
43010
  child.unref();
43010
43011
  } catch {}
43011
43012
  }, browserLogin = async (webBase, timeoutMs = 5 * 60 * 1000, open = openBrowser) => {
43012
43013
  const state = randomBytes(16).toString("hex");
43013
- let resolve8;
43014
+ let resolve9;
43014
43015
  let reject;
43015
43016
  const tokenPromise = new Promise((res, rej) => {
43016
- resolve8 = res;
43017
+ resolve9 = res;
43017
43018
  reject = rej;
43018
43019
  });
43019
43020
  const server = createServer((req, res) => {
@@ -43031,7 +43032,7 @@ var SUCCESS_HTML = `<!doctype html><meta charset="utf-8"><title>lecodes</title>
43031
43032
  }
43032
43033
  res.writeHead(200, { "content-type": "text/html" });
43033
43034
  res.end(SUCCESS_HTML);
43034
- resolve8(token);
43035
+ resolve9(token);
43035
43036
  });
43036
43037
  await new Promise((res, rej) => {
43037
43038
  server.once("error", rej);
@@ -43532,8 +43533,8 @@ __export(exports_design, {
43532
43533
  flushDesignComments: () => flushDesignComments,
43533
43534
  design: () => design
43534
43535
  });
43535
- import { existsSync as existsSync19, mkdirSync as mkdirSync14, readFileSync as readFileSync18, readdirSync as readdirSync10, writeFileSync as writeFileSync15, appendFileSync } from "node:fs";
43536
- import { basename as basename7, join as join19 } from "node:path";
43536
+ import { existsSync as existsSync20, mkdirSync as mkdirSync15, readFileSync as readFileSync18, readdirSync as readdirSync11, writeFileSync as writeFileSync16, appendFileSync } from "node:fs";
43537
+ import { basename as basename8, join as join20 } from "node:path";
43537
43538
  var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43538
43539
  let root;
43539
43540
  let manifest;
@@ -43548,13 +43549,13 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43548
43549
  return {
43549
43550
  root,
43550
43551
  manifest,
43551
- name: manifest?.name ?? basename7(root),
43552
+ name: manifest?.name ?? basename8(root),
43552
43553
  dirName,
43553
- designDir: join19(root, dirName),
43554
+ designDir: join20(root, dirName),
43554
43555
  designKey: "/" + dirName.replace(/\\/g, "/")
43555
43556
  };
43556
43557
  }, loadDesignServer = async (ctx, needs = []) => {
43557
- if (!existsSync19(ctx.designDir)) {
43558
+ if (!existsSync20(ctx.designDir)) {
43558
43559
  throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
43559
43560
  }
43560
43561
  let mod;
@@ -43586,13 +43587,13 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43586
43587
  `).find((l2) => l2.trim())?.trim() ?? "";
43587
43588
  return line.length > max ? line.slice(0, max - 1) + "…" : line;
43588
43589
  }, listScreens = (designDir) => {
43589
- const dir = join19(designDir, "screens");
43590
- if (!existsSync19(dir))
43590
+ const dir = join20(designDir, "screens");
43591
+ if (!existsSync20(dir))
43591
43592
  return [];
43592
- return readdirSync10(dir).filter((f3) => f3.endsWith(".ts") && !f3.startsWith(".")).map((f3) => f3.slice(0, -3)).sort();
43593
+ return readdirSync11(dir).filter((f3) => f3.endsWith(".ts") && !f3.startsWith(".")).map((f3) => f3.slice(0, -3)).sort();
43593
43594
  }, PHONE_SIZE, readMetaFrames = (designDir) => {
43594
43595
  try {
43595
- const meta = JSON.parse(readFileSync18(join19(designDir, "meta.json"), "utf8"));
43596
+ const meta = JSON.parse(readFileSync18(join20(designDir, "meta.json"), "utf8"));
43596
43597
  const d = meta?.defaultSize;
43597
43598
  const valid = Array.isArray(d) && d.length === 2 && d.every((n2) => typeof n2 === "number" && Number.isFinite(n2) && n2 >= 64 && n2 <= 8192);
43598
43599
  return {
@@ -43603,10 +43604,10 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43603
43604
  return { screens: {}, defaultSize: PHONE_SIZE };
43604
43605
  }
43605
43606
  }, writeMcpConfig = (ctx, port) => {
43606
- const path2 = join19(ctx.root, ".mcp.json");
43607
+ const path2 = join20(ctx.root, ".mcp.json");
43607
43608
  const entry = { type: "http", url: `http://localhost:${port}/mcp` };
43608
43609
  let config = {};
43609
- if (existsSync19(path2)) {
43610
+ if (existsSync20(path2)) {
43610
43611
  try {
43611
43612
  config = JSON.parse(readFileSync18(path2, "utf8"));
43612
43613
  } catch {
@@ -43621,7 +43622,7 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43621
43622
  return;
43622
43623
  }
43623
43624
  config.mcpServers["lecodes-design"] = entry;
43624
- writeFileSync15(path2, JSON.stringify(config, null, 2) + `
43625
+ writeFileSync16(path2, JSON.stringify(config, null, 2) + `
43625
43626
  `);
43626
43627
  log(`${c.green(existing ? "updated" : "created")} .mcp.json (lecodes-design MCP → ${entry.url})`);
43627
43628
  }, init = async (ctx, args) => {
@@ -43630,26 +43631,26 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43630
43631
  ["spec.md", SPEC_MD],
43631
43632
  ["README.md", README_HINT],
43632
43633
  ["CLAUDE.md", CLAUDE_MD],
43633
- [join19("shared", "tokens.ts"), TOKENS_TS],
43634
- [join19("shared", "tabs.ts"), TABS_TS],
43635
- [join19("screens", "home.ts"), HOME_SCREEN_TS]
43634
+ [join20("shared", "tokens.ts"), TOKENS_TS],
43635
+ [join20("shared", "tabs.ts"), TABS_TS],
43636
+ [join20("screens", "home.ts"), HOME_SCREEN_TS]
43636
43637
  ];
43637
- mkdirSync14(join19(ctx.designDir, "screens"), { recursive: true });
43638
- mkdirSync14(join19(ctx.designDir, "shared"), { recursive: true });
43638
+ mkdirSync15(join20(ctx.designDir, "screens"), { recursive: true });
43639
+ mkdirSync15(join20(ctx.designDir, "shared"), { recursive: true });
43639
43640
  let created = 0;
43640
43641
  for (const [rel, content] of files) {
43641
- const abs = join19(ctx.designDir, rel);
43642
- if (existsSync19(abs)) {
43642
+ const abs = join20(ctx.designDir, rel);
43643
+ if (existsSync20(abs)) {
43643
43644
  note(`kept ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
43644
43645
  continue;
43645
43646
  }
43646
- writeFileSync15(abs, content);
43647
+ writeFileSync16(abs, content);
43647
43648
  log(`${c.green("created")} ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
43648
43649
  created++;
43649
43650
  }
43650
- const ignorePath = join19(ctx.root, ".lecodesignore");
43651
+ const ignorePath = join20(ctx.root, ".lecodesignore");
43651
43652
  const ignoreLines = [`${ctx.dirName}/.snapshots/`, `${ctx.dirName}/comments/`];
43652
- const current = existsSync19(ignorePath) ? readFileSync18(ignorePath, "utf8") : "";
43653
+ const current = existsSync20(ignorePath) ? readFileSync18(ignorePath, "utf8") : "";
43653
43654
  const present = new Set(current.split(/\r?\n/));
43654
43655
  const missing = ignoreLines.filter((line) => !present.has(line));
43655
43656
  if (missing.length) {
@@ -43664,8 +43665,8 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43664
43665
  writeMcpConfig(ctx, Number(flagStr(args, "port")) || DEFAULT_PORT);
43665
43666
  const wroteTypes = materializeTypesLocal(ctx.designDir);
43666
43667
  writeTsconfig(ctx.designDir);
43667
- mkdirSync14(join19(ctx.designDir, ".lecodes", "types"), { recursive: true });
43668
- writeFileSync15(join19(ctx.designDir, ".lecodes", "types", "design-macros.d.ts"), DESIGN_MACROS_DTS);
43668
+ mkdirSync15(join20(ctx.designDir, ".lecodes", "types"), { recursive: true });
43669
+ writeFileSync16(join20(ctx.designDir, ".lecodes", "types", "design-macros.d.ts"), DESIGN_MACROS_DTS);
43669
43670
  log(`${c.green("created")} ${ctx.dirName}/tsconfig.json${wroteTypes ? ` + ${ctx.dirName}/.lecodes/types/` : ""} (IDE types)`);
43670
43671
  if (!wroteTypes)
43671
43672
  warnErr("Bundled SDK types not found — tsconfig written, but globals may show unresolved until the CLI is rebuilt with vendored types.");
@@ -43713,16 +43714,16 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43713
43714
  killer.unref?.();
43714
43715
  const png2 = flagBool(args, "png") || flagStr(args, "png") !== undefined;
43715
43716
  const frames = readMetaFrames(ctx.designDir);
43716
- const outDir = flagStr(args, "out-dir") ?? join19(ctx.designDir, ".snapshots");
43717
+ const outDir = flagStr(args, "out-dir") ?? join20(ctx.designDir, ".snapshots");
43717
43718
  const single = targets.length === 1 && requested.length === 1 && !png2 && !allStates && !flagStr(args, "out-dir");
43718
43719
  const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
43719
43720
  if (!single)
43720
- mkdirSync14(outDir, { recursive: true });
43721
+ mkdirSync15(outDir, { recursive: true });
43721
43722
  for (const id of targets) {
43722
43723
  const [width, height] = frames.screens[id]?.size ?? frames.defaultSize;
43723
43724
  let plan = [{ state: undefined, bases: [id] }];
43724
43725
  if (discover) {
43725
- const { states, canonical } = discover(readFileSync18(join19(ctx.designDir, "screens", `${id}.ts`), "utf8"));
43726
+ const { states, canonical } = discover(readFileSync18(join20(ctx.designDir, "screens", `${id}.ts`), "utf8"));
43726
43727
  if (stateFlag !== undefined) {
43727
43728
  if (!states.includes(stateFlag)) {
43728
43729
  if (requested.length === 1)
@@ -43763,11 +43764,11 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43763
43764
  for (const w of pngResult.warnings)
43764
43765
  warnErr(`${tag}: ${w}`);
43765
43766
  for (const base of bases) {
43766
- writeFileSync15(join19(outDir, `${base}.json`), result.text);
43767
- log(`${c.green("✓")} ${tag} → ${join19(outDir, `${base}.json`)}`);
43767
+ writeFileSync16(join20(outDir, `${base}.json`), result.text);
43768
+ log(`${c.green("✓")} ${tag} → ${join20(outDir, `${base}.json`)}`);
43768
43769
  if (pngResult) {
43769
- writeFileSync15(join19(outDir, `${base}.png`), pngResult.png);
43770
- log(`${c.green("✓")} ${tag} → ${join19(outDir, `${base}.png`)}`);
43770
+ writeFileSync16(join20(outDir, `${base}.png`), pngResult.png);
43771
+ log(`${c.green("✓")} ${tag} → ${join20(outDir, `${base}.png`)}`);
43771
43772
  }
43772
43773
  }
43773
43774
  }
@@ -44069,7 +44070,7 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
44069
44070
  note("Read them with: lecodes design comments");
44070
44071
  }, flushDesignComments = async (args) => {
44071
44072
  const ctx = getContext(args);
44072
- if (!existsSync19(ctx.designDir))
44073
+ if (!existsSync20(ctx.designDir))
44073
44074
  return null;
44074
44075
  const platform = optionalPlatform(ctx);
44075
44076
  if (!platform)
@@ -45211,7 +45212,7 @@ var require_main = __commonJS((exports, module) => {
45211
45212
  // package.json
45212
45213
  var package_default = {
45213
45214
  name: "lecodes-cli",
45214
- version: "0.14.2",
45215
+ version: "0.16.0",
45215
45216
  dependencies: {
45216
45217
  "@letary/chisel": "^0.6.0",
45217
45218
  jimp: "^1.6.1"
@@ -45224,9 +45225,9 @@ var package_default = {
45224
45225
  sdk: "workspace:*"
45225
45226
  },
45226
45227
  peerDependencies: {
45227
- "lecodes-design": "^0.7.0",
45228
- "lecodes-renderer": "^0.8.2",
45229
- "lecodes-3d-editor": "^0.2.0"
45228
+ "lecodes-design": "^0.8.0",
45229
+ "lecodes-renderer": "^0.8.3",
45230
+ "lecodes-3d-editor": "^0.3.0"
45230
45231
  },
45231
45232
  bin: {
45232
45233
  lecodes: "dist/index.js"
@@ -45272,10 +45273,10 @@ var package_default = {
45272
45273
  // src/commands/app.ts
45273
45274
  init_manifest();
45274
45275
  init_config();
45275
- import { execFileSync as execFileSync3 } from "node:child_process";
45276
- import { cpSync as cpSync2, existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync14, readdirSync as readdirSync5, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync11 } from "node:fs";
45277
- import { homedir as homedir5 } from "node:os";
45278
- import { basename as basename5, dirname as dirname11, join as join15, resolve as resolve4 } from "node:path";
45276
+ import { execFileSync as execFileSync4 } from "node:child_process";
45277
+ import { cpSync as cpSync3, existsSync as existsSync16, mkdirSync as mkdirSync11, readFileSync as readFileSync15, readdirSync as readdirSync7, rmSync as rmSync4, statSync as statSync5, writeFileSync as writeFileSync13 } from "node:fs";
45278
+ import { homedir as homedir6 } from "node:os";
45279
+ import { basename as basename6, dirname as dirname12, join as join17, resolve as resolve6 } from "node:path";
45279
45280
 
45280
45281
  // src/commands/compile.ts
45281
45282
  init_api();
@@ -46323,6 +46324,23 @@ NEXT launch. Offline it's a silent no-op — the app always launches from a loca
46323
46324
  An App Store update always beats a cached OTA bundle. JS only: adding/removing native
46324
46325
  plugins still needs a store release.
46325
46326
 
46327
+ Shipping an update is just \`lecodes push --publish\` — no rebuild, no store review:
46328
+ running apps download the new code on their next launch and run it on the launch after.
46329
+
46330
+ Shells generated before OTA support have a \`ViewController.swift\` that reads app.js from
46331
+ \`Bundle.main\` directly, so downloaded updates would never run — and since \`App/*.swift\`
46332
+ is yours, sync won't rewrite it (\`lecodes app doctor\` flags this). Replace the
46333
+ \`Bundle.main\` load with:
46334
+
46335
+ \`\`\`swift
46336
+ LeCodesUpdater.checkForUpdate()
46337
+ guard let code = LeCodesUpdater.activeBundle() else {
46338
+ print("LeCodes: bundled app.js is missing — run \`lecodes app sync\`")
46339
+ return
46340
+ }
46341
+ engine.run(code)
46342
+ \`\`\`
46343
+
46326
46344
  ## Config
46327
46345
 
46328
46346
  Identity lives in \`../app.json\` (name, version, plugins, ios.bundleId / teamId / variant /
@@ -46359,7 +46377,7 @@ var readJson = (path) => {
46359
46377
  if (!existsSync10(path))
46360
46378
  return null;
46361
46379
  try {
46362
- return JSON.parse(readFileSync11(path, "utf8"));
46380
+ return JSON.parse(readFileSync11(path, "utf8").replace(/^\uFEFF/, ""));
46363
46381
  } catch {
46364
46382
  throw new CliError(`Invalid JSON in ${path}`);
46365
46383
  }
@@ -46400,6 +46418,34 @@ var ensureShellIgnored = (root, dir) => {
46400
46418
  `);
46401
46419
  log(`${c.green("updated")} .lecodesignore (+ ${dir}/)`);
46402
46420
  };
46421
+ var SDK_VARIANTS = ["core", "2d", "3d", "full"];
46422
+ var detectEngines = (bundleJs) => {
46423
+ const lines = [];
46424
+ for (const line of bundleJs.split(`
46425
+ `)) {
46426
+ if (!line.startsWith("//"))
46427
+ break;
46428
+ lines.push(line);
46429
+ }
46430
+ const header = lines.join(`
46431
+ `);
46432
+ return { gl: !/^\/\/ gl: disable$/m.test(header), g2: /^\/\/ 2d: enable$/m.test(header) };
46433
+ };
46434
+ var resolveVariant = (requested, bundleJs, configKey = "ios.variant") => {
46435
+ const uses = detectEngines(bundleJs);
46436
+ const detected = uses.gl && uses.g2 ? "full" : uses.gl ? "3d" : uses.g2 ? "2d" : "core";
46437
+ if (requested === "auto")
46438
+ return { variant: detected, detected };
46439
+ const variant = requested;
46440
+ const missing = [
46441
+ uses.gl && variant !== "3d" && variant !== "full" ? "3D" : null,
46442
+ uses.g2 && variant !== "2d" && variant !== "full" ? "2D" : null
46443
+ ].filter(Boolean);
46444
+ if (missing.length > 0) {
46445
+ warn(`project uses the ${missing.join(" + ")} engine but ${configKey} forces "${variant}" — those scenes won't run on device; remove ${configKey} to auto-select "${detected}".`);
46446
+ }
46447
+ return { variant, detected };
46448
+ };
46403
46449
  var VERSION_RE = /^\d+\.\d+\.\d+$/;
46404
46450
  var cmpVersion = (a, b) => {
46405
46451
  const pa = a.split(".").map(Number);
@@ -46747,7 +46793,7 @@ validateDistributionUrl=true
46747
46793
  zipStoreBase=GRADLE_USER_HOME
46748
46794
  zipStorePath=wrapper/dists
46749
46795
  `;
46750
- var runtimeBuildGradleTemplate = (sdkVersion, pluginModules) => {
46796
+ var runtimeBuildGradleTemplate = (sdkVersion, pluginModules, sdkArtifact = "lecodes-sdk") => {
46751
46797
  const deps = SDK_RUNTIME_DEPS.map((d) => `
46752
46798
  api("${d}")`).join("");
46753
46799
  const plugins = pluginModules.map((m) => `
@@ -46779,8 +46825,10 @@ kotlin {
46779
46825
  }
46780
46826
 
46781
46827
  dependencies {
46782
- // Exact pin — the release asset URL is derived from this version (see settings.gradle.kts).
46783
- api("io.letary:lecodes-sdk:${sdkVersion}@aar")
46828
+ // Exact pin — the release asset URL is derived from this artifact + version (see
46829
+ // settings.gradle.kts). The artifact is the SDK variant sync picked (lecodes-sdk = full,
46830
+ // -3d / -2d / -core = smaller engine builds); every variant vends the same Kotlin API.
46831
+ api("io.letary:${sdkArtifact}:${sdkVersion}@aar")
46784
46832
  // The fat AAR carries no POM, so its own dependencies ride here explicitly.${deps}${plugins}
46785
46833
  }
46786
46834
  `;
@@ -46830,7 +46878,7 @@ ${push ? ` ${pushNs}.PushPlugin.setForeground(foreground)
46830
46878
  }
46831
46879
  `;
46832
46880
  };
46833
- var pluginBuildGradleTemplate = (id, module, namespace, sdkVersion, dependencies, aarNames, hasConsumerRules) => {
46881
+ var pluginBuildGradleTemplate = (id, module, namespace, sdkVersion, dependencies, aarNames, hasConsumerRules, sdkArtifact = "lecodes-sdk") => {
46834
46882
  const consumer = hasConsumerRules ? `
46835
46883
  consumerProguardFiles("consumer-rules.pro")` : "";
46836
46884
  const aars = aarNames.map((n2) => `
@@ -46865,7 +46913,9 @@ kotlin {
46865
46913
  }
46866
46914
 
46867
46915
  dependencies {
46868
- implementation("io.letary:lecodes-sdk:${sdkVersion}@aar")${sdkDeps}${aars}${manifestDeps}
46916
+ // Same artifact (SDK variant) as lecodes-runtime — two different variant coordinates in one
46917
+ // shell would put two copies of the SDK classes on the runtime classpath.
46918
+ implementation("io.letary:${sdkArtifact}:${sdkVersion}@aar")${sdkDeps}${aars}${manifestDeps}
46869
46919
  }
46870
46920
  `;
46871
46921
  };
@@ -46897,6 +46947,14 @@ in \`settings.gradle.kts\`. \`lecodes app update\` moves the pin (or set \`app.j
46897
46947
  \`android.sdk\`). The SDK AAR ships no POM, so its own dependencies are declared explicitly in
46898
46948
  the generated runtime build file.
46899
46949
 
46950
+ ## SDK variant
46951
+
46952
+ Sync links the smallest SDK artifact covering the engines the compiled bundle actually uses —
46953
+ \`lecodes-sdk\` (full: 3D+2D), \`lecodes-sdk-3d\`, \`lecodes-sdk-2d\` or \`lecodes-sdk-core\`
46954
+ (UI only) — so an app without 3D scenes doesn't ship Filament. If the project starts using an
46955
+ engine, sync switches automatically on the next run; set \`android.variant\` in \`../app.json\`
46956
+ to force one (needs lecodes-android-sdk >= 1.3.0; older releases ship only the full AAR).
46957
+
46900
46958
  ## App-local native code
46901
46959
 
46902
46960
  Drop Kotlin files into \`app/src/main/java/\` and register them in \`MainActivity\` after the
@@ -47560,6 +47618,8 @@ if "%OS%"=="Windows_NT" endlocal
47560
47618
  // src/commands/appAndroid.ts
47561
47619
  var ANDROID_SDK_REPO = "letary/lecodes-android-sdk";
47562
47620
  var ANDROID_SDK_MIN_VERSION = "1.2.0";
47621
+ var ANDROID_SDK_VARIANTS_MIN = "1.3.0";
47622
+ var variantArtifact = (variant) => variant === "full" ? "lecodes-sdk" : `lecodes-sdk-${variant}`;
47563
47623
  var ANDROID_SDK_URL_DEFAULT = `https://github.com/${ANDROID_SDK_REPO}/releases/download`;
47564
47624
  var sdkRepoUrl = () => process.env.LECODES_ANDROID_SDK_URL ?? ANDROID_SDK_URL_DEFAULT;
47565
47625
  var ANDROID_SDK_OPTS = { repo: ANDROID_SDK_REPO, minVersion: ANDROID_SDK_MIN_VERSION, configKey: "android.sdk" };
@@ -47567,7 +47627,7 @@ var shellSdkVersionAndroid = (androidDir) => {
47567
47627
  const path2 = join14(androidDir, "lecodes-runtime", "build.gradle.kts");
47568
47628
  if (!existsSync13(path2))
47569
47629
  return null;
47570
- const m = /io\.letary:lecodes-sdk:(\d+\.\d+\.\d+)@aar/.exec(readFileSync13(path2, "utf8"));
47630
+ const m = /io\.letary:lecodes-sdk(?:-(?:core|2d|3d))?:(\d+\.\d+\.\d+)@aar/.exec(readFileSync13(path2, "utf8"));
47571
47631
  return m ? m[1] : null;
47572
47632
  };
47573
47633
  var GRADLE_WRAPPER_JAR_URL = "https://raw.githubusercontent.com/gradle/gradle/v8.13.0/gradle/wrapper/gradle-wrapper.jar";
@@ -47600,6 +47660,10 @@ var initAndroid = async (args) => {
47600
47660
  throw new CliError("android/ already exists — run `lecodes app sync` to update it.");
47601
47661
  }
47602
47662
  const existing = readJson(join14(root, "app.json")) ?? {};
47663
+ const variant = flagStr(args, "variant") ?? existing.android?.variant ?? "auto";
47664
+ if (variant !== "auto" && !SDK_VARIANTS.includes(variant)) {
47665
+ throw new CliError(`--variant must be auto, ${SDK_VARIANTS.join(", ")} (got "${variant}")`);
47666
+ }
47603
47667
  const sdkFlag = flagStr(args, "sdk") ?? existing.android?.sdk;
47604
47668
  const sdkVersion = await resolveSdkVersion(process.env.LECODES_ANDROID_SDK ?? sdkFlag ?? "latest", null, ANDROID_SDK_OPTS);
47605
47669
  const sdk = sdkFlag === "latest" ? "latest" : sdkVersion;
@@ -47614,6 +47678,7 @@ var initAndroid = async (args) => {
47614
47678
  applicationId: flagStr(args, "app-id") ?? existing.android?.applicationId ?? defaultAppId,
47615
47679
  versionCode: existing.android?.versionCode ?? 1,
47616
47680
  sdk,
47681
+ variant,
47617
47682
  ...existing.android?.signing ? { signing: existing.android.signing } : {}
47618
47683
  }
47619
47684
  };
@@ -47655,7 +47720,9 @@ var initAndroid = async (args) => {
47655
47720
  write("app/src/main/res/values/colors.xml", COLORS_XML);
47656
47721
  write("app/src/main/res/values/strings.xml", stringsXmlTemplate(name));
47657
47722
  write("app/src/main/res/drawable/ic_launcher.xml", IC_LAUNCHER_XML);
47658
- write("lecodes-runtime/build.gradle.kts", runtimeBuildGradleTemplate(sdkVersion, []));
47723
+ const initVariant = variant === "auto" ? "core" : variant;
47724
+ const initArtifact = cmpVersion(sdkVersion, ANDROID_SDK_VARIANTS_MIN) >= 0 ? variantArtifact(initVariant) : "lecodes-sdk";
47725
+ write("lecodes-runtime/build.gradle.kts", runtimeBuildGradleTemplate(sdkVersion, [], initArtifact));
47659
47726
  write("lecodes-runtime/src/main/kotlin/io/letary/lecodes/runtime/LeCodesRuntime.kt", runtimeKotlinTemplate([]));
47660
47727
  log("");
47661
47728
  log(`${c.green("sdk")} lecodes-android-sdk ${sdkVersion}${sdk === "latest" ? c.dim(" (android.sdk: latest — re-resolved on every sync)") : c.dim(" (pinned in app.json android.sdk — `lecodes app update` moves it)")}`);
@@ -47680,6 +47747,40 @@ var syncAndroid = async (root, config, prior) => {
47680
47747
  const linkedSdk = shellSdkVersionAndroid(androidDir);
47681
47748
  const sdkPin = process.env.LECODES_ANDROID_SDK ?? config.android?.sdk ?? linkedSdk ?? "latest";
47682
47749
  const sdkVersion = await resolveSdkVersion(sdkPin, linkedSdk, ANDROID_SDK_OPTS);
47750
+ const remotePatterns = config.resources?.remote ?? [];
47751
+ if (!Array.isArray(remotePatterns) || remotePatterns.some((p2) => typeof p2 !== "string")) {
47752
+ throw new CliError('app.json "resources.remote" must be an array of glob strings (e.g. ["videos/**"]).');
47753
+ }
47754
+ const assetsDir = join14(androidDir, "app", "src", "main", "assets");
47755
+ rmrf(assetsDir);
47756
+ mkdirSync8(assetsDir, { recursive: true });
47757
+ let bundleJs;
47758
+ let resources;
47759
+ if (prior) {
47760
+ writeFileSync10(join14(assetsDir, "app.js"), prior.bundleJs);
47761
+ bundleJs = prior.bundleJs;
47762
+ resources = prior.resources;
47763
+ } else {
47764
+ resources = await compileShell({ _: [], flags: { o: join14(assetsDir, "app.js") } }, { remotePatterns });
47765
+ bundleJs = readFileSync13(join14(assetsDir, "app.js"), "utf8");
47766
+ }
47767
+ const requestedVariant = config.android?.variant ?? "auto";
47768
+ if (requestedVariant !== "auto" && !SDK_VARIANTS.includes(requestedVariant)) {
47769
+ throw new CliError(`app.json android.variant must be "auto" or one of ${SDK_VARIANTS.join(", ")} (got "${requestedVariant}").`);
47770
+ }
47771
+ const { variant, detected } = resolveVariant(requestedVariant, bundleJs, "android.variant");
47772
+ let sdkArtifact = variantArtifact(variant);
47773
+ if (sdkArtifact !== "lecodes-sdk" && cmpVersion(sdkVersion, ANDROID_SDK_VARIANTS_MIN) < 0) {
47774
+ 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.`);
47775
+ sdkArtifact = "lecodes-sdk";
47776
+ }
47777
+ const WHY = {
47778
+ full: "3D + 2D engines in use",
47779
+ "3d": "3D in use, no 2D",
47780
+ "2d": "2D in use, no 3D",
47781
+ core: "no 3D/2D engine use detected"
47782
+ };
47783
+ log(`${c.green("variant")} ${variant} → ${sdkArtifact}${requestedVariant === "auto" ? c.dim(` (auto: ${WHY[detected]})`) : c.dim(" (forced by android.variant)")}`);
47683
47784
  const lockPath = join14(root, "plugins.lock.json");
47684
47785
  const oldLock = readJson(lockPath)?.plugins ?? {};
47685
47786
  const pluginSpecs = Object.entries(config.plugins ?? {});
@@ -47720,7 +47821,7 @@ var syncAndroid = async (root, config, prior) => {
47720
47821
  aarWrapperPaths.push(`:plugins:${module}:libs:${base}`);
47721
47822
  }
47722
47823
  const hasConsumerRules = existsSync13(join14(dest, "consumer-rules.pro"));
47723
- writeFileSync10(join14(dest, "build.gradle.kts"), pluginBuildGradleTemplate(id, module, namespace, sdkVersion, android.dependencies ?? [], aarNames, hasConsumerRules));
47824
+ writeFileSync10(join14(dest, "build.gradle.kts"), pluginBuildGradleTemplate(id, module, namespace, sdkVersion, android.dependencies ?? [], aarNames, hasConsumerRules, sdkArtifact));
47724
47825
  runtimePlugins.push({ id, module, register });
47725
47826
  newLock[id] = plugin.lock;
47726
47827
  log(`${c.green("plugin")} ${id}@${plugin.lock.version} (${plugin.lock.tag} → plugins/${module})`);
@@ -47732,23 +47833,6 @@ var syncAndroid = async (root, config, prior) => {
47732
47833
  log(`${c.yellow("removed")} android/plugins/${entry} (plugin uninstalled)`);
47733
47834
  }
47734
47835
  }
47735
- const remotePatterns = config.resources?.remote ?? [];
47736
- if (!Array.isArray(remotePatterns) || remotePatterns.some((p2) => typeof p2 !== "string")) {
47737
- throw new CliError('app.json "resources.remote" must be an array of glob strings (e.g. ["videos/**"]).');
47738
- }
47739
- const assetsDir = join14(androidDir, "app", "src", "main", "assets");
47740
- rmrf(assetsDir);
47741
- mkdirSync8(assetsDir, { recursive: true });
47742
- let bundleJs;
47743
- let resources;
47744
- if (prior) {
47745
- writeFileSync10(join14(assetsDir, "app.js"), prior.bundleJs);
47746
- bundleJs = prior.bundleJs;
47747
- resources = prior.resources;
47748
- } else {
47749
- resources = await compileShell({ _: [], flags: { o: join14(assetsDir, "app.js") } }, { remotePatterns });
47750
- bundleJs = readFileSync13(join14(assetsDir, "app.js"), "utf8");
47751
- }
47752
47836
  const sdkWhy = sdkPin === "latest" ? "latest" : config.android?.sdk ? "pinned by android.sdk" : "from the shell — set android.sdk or run `lecodes app update`";
47753
47837
  log(`${c.green("sdk")} lecodes-android-sdk ${sdkVersion}${c.dim(` (${sdkWhy})`)}`);
47754
47838
  const preload = /^\/\/ preload: (.*)$/m.exec(bundleJs)?.[1].split(";").map((s2) => s2.trim()).filter(Boolean) ?? [];
@@ -47791,7 +47875,7 @@ var syncAndroid = async (root, config, prior) => {
47791
47875
  writeFileSync10(join14(androidDir, "settings.gradle.kts"), settingsGradleTemplate(slugify(config.name ?? basename4(root)), modules, aarWrapperPaths, sdkRepoUrl()));
47792
47876
  writeFileSync10(join14(androidDir, "build.gradle.kts"), rootBuildGradleTemplate([...gradlePlugins].map(([id, version]) => ({ id, version }))));
47793
47877
  writeFileSync10(join14(androidDir, "gradle.properties"), GRADLE_PROPERTIES);
47794
- writeFileSync10(join14(androidDir, "lecodes-runtime", "build.gradle.kts"), runtimeBuildGradleTemplate(sdkVersion, modules));
47878
+ writeFileSync10(join14(androidDir, "lecodes-runtime", "build.gradle.kts"), runtimeBuildGradleTemplate(sdkVersion, modules, sdkArtifact));
47795
47879
  const runtimeKt = join14(androidDir, "lecodes-runtime", "src", "main", "kotlin", "io", "letary", "lecodes", "runtime", "LeCodesRuntime.kt");
47796
47880
  mkdirSync8(dirname10(runtimeKt), { recursive: true });
47797
47881
  writeFileSync10(runtimeKt, runtimeKotlinTemplate(runtimePlugins, shellUuid));
@@ -47962,6 +48046,9 @@ var doctorAndroidChecks = async (root, config, check) => {
47962
48046
  check(javaMajor >= 17, `JDK ${javaMajor || "not found"}`, "install JDK 17+ (Android Studio ships one)");
47963
48047
  const hasSdkHome = !!(process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT) || existsSync13(join14(androidDir, "local.properties"));
47964
48048
  check(hasSdkHome, "Android SDK location", "set ANDROID_HOME or create android/local.properties");
48049
+ const requestedVariant = config.android?.variant ?? "auto";
48050
+ const validVariant = requestedVariant === "auto" || SDK_VARIANTS.includes(requestedVariant);
48051
+ check(validVariant, `android.variant "${requestedVariant}"`, `use "auto" or one of ${SDK_VARIANTS.join(", ")}`);
47965
48052
  const wrapperJar = join14(androidDir, "gradle", "wrapper", "gradle-wrapper.jar");
47966
48053
  check(existsSync13(wrapperJar) && statSync2(wrapperJar).size > 0, "Gradle wrapper jar", `run \`gradle wrapper --gradle-version ${GRADLE_VERSION}\` in android/ (or let Android Studio repair it)`);
47967
48054
  check(existsSync13(join14(androidDir, "app", "src", "main", "assets", "app.js")), "Bundled app.js", "run `lecodes app sync`");
@@ -47984,16 +48071,491 @@ var doctorAndroidChecks = async (root, config, check) => {
47984
48071
  }
47985
48072
  };
47986
48073
 
48074
+ // src/commands/appDesktop.ts
48075
+ import { execFileSync as execFileSync3, spawn as spawn3 } from "node:child_process";
48076
+ import { cpSync as cpSync2, existsSync as existsSync15, mkdirSync as mkdirSync10, readdirSync as readdirSync6, rmSync as rmSync3, statSync as statSync4, writeFileSync as writeFileSync12 } from "node:fs";
48077
+ import { basename as basename5, dirname as dirname11, join as join16, resolve as resolve5 } from "node:path";
48078
+
48079
+ // src/desktopRenderer.ts
48080
+ init_util();
48081
+ import { createHash as createHash5 } from "node:crypto";
48082
+ import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
48083
+ import { copyFileSync, existsSync as existsSync14, mkdirSync as mkdirSync9, mkdtempSync, readdirSync as readdirSync5, readFileSync as readFileSync14, renameSync as renameSync2, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync11 } from "node:fs";
48084
+ import { homedir as homedir5, tmpdir } from "node:os";
48085
+ import { join as join15, resolve as resolve4 } from "node:path";
48086
+ var MANIFEST_URL = "https://storage.yandexcloud.net/le-codes/releases/versions.json";
48087
+ var DESKTOP_RENDERER_VERSION = "1.1.0";
48088
+ var semverNewer = (a2, b) => {
48089
+ const pa = a2.split(/[.-]/).map(Number), pb = b.split(/[.-]/).map(Number);
48090
+ for (let i2 = 0;i2 < 3; i2++)
48091
+ if ((pb[i2] ?? 0) !== (pa[i2] ?? 0))
48092
+ return (pb[i2] ?? 0) - (pa[i2] ?? 0);
48093
+ return 0;
48094
+ };
48095
+ var platformKey = () => {
48096
+ if (process.platform === "win32")
48097
+ return "windows-x64";
48098
+ if (process.platform === "linux")
48099
+ return "ubuntu-linux-x64";
48100
+ throw new CliError(process.platform === "darwin" ? "`lecodes render --desktop` isn't available on macOS yet (the macOS host ships separately)." : `\`lecodes render --desktop\` isn't available on ${process.platform}.`);
48101
+ };
48102
+ var exeName = () => process.platform === "win32" ? "lecodes-desktop.exe" : "lecodes-desktop";
48103
+ var findExe = (dir) => {
48104
+ const name = exeName();
48105
+ for (const entry of readdirSync5(dir, { withFileTypes: true })) {
48106
+ const p2 = join15(dir, entry.name);
48107
+ if (entry.isFile() && entry.name === name)
48108
+ return p2;
48109
+ if (entry.isDirectory()) {
48110
+ const found = findExe(p2);
48111
+ if (found)
48112
+ return found;
48113
+ }
48114
+ }
48115
+ return null;
48116
+ };
48117
+ var cacheRoot = () => join15(homedir5(), ".lecodes", "renderer");
48118
+ var currentFile = () => join15(cacheRoot(), "current.json");
48119
+ var installedDesktopVersions = () => {
48120
+ try {
48121
+ return readdirSync5(cacheRoot(), { withFileTypes: true }).filter((e2) => e2.isDirectory() && e2.name.startsWith("desktop-")).map((e2) => e2.name.slice("desktop-".length)).filter((v) => findExe(join15(cacheRoot(), `desktop-${v}`)) !== null).sort(semverNewer);
48122
+ } catch {
48123
+ return [];
48124
+ }
48125
+ };
48126
+ var selectedDesktopVersion = () => {
48127
+ try {
48128
+ const saved = JSON.parse(readFileSync14(currentFile(), "utf8")).version;
48129
+ if (saved && /^\d+\.\d+\.\d+/.test(saved) && semverNewer(DESKTOP_RENDERER_VERSION, saved) > 0)
48130
+ return saved;
48131
+ } catch {}
48132
+ return DESKTOP_RENDERER_VERSION;
48133
+ };
48134
+ var fetchDesktopManifest = async () => {
48135
+ let manifest;
48136
+ try {
48137
+ const res = await fetch(MANIFEST_URL);
48138
+ if (!res.ok)
48139
+ throw new Error(`HTTP ${res.status}`);
48140
+ manifest = await res.json();
48141
+ } catch (e2) {
48142
+ throw new CliError(`Can't fetch the release manifest (${MANIFEST_URL}): ${e2 instanceof Error ? e2.message : String(e2)}`);
48143
+ }
48144
+ const desktop = manifest.products?.desktop;
48145
+ if (!desktop)
48146
+ throw new CliError("The release manifest has no desktop product.");
48147
+ return desktop;
48148
+ };
48149
+ var downloadDesktop = async (version, dest) => {
48150
+ const key = platformKey();
48151
+ const entry = (await fetchDesktopManifest()).versions.find((v) => v.version === version);
48152
+ const file = entry?.files[key];
48153
+ if (!file) {
48154
+ throw new CliError(`LeCodes Desktop ${version} [${key}] isn't published yet — \`--desktop\` needs it for the ` + `headless render contract.
48155
+ Set LECODES_DESKTOP_EXE to a locally built host, or update lecodes-cli.`);
48156
+ }
48157
+ note(`Downloading the native renderer (LeCodes Desktop ${version}, ${(file.size / 1024 / 1024).toFixed(1)} MB)…`);
48158
+ let bytes;
48159
+ try {
48160
+ const res = await fetch(file.url);
48161
+ if (!res.ok)
48162
+ throw new Error(`HTTP ${res.status}`);
48163
+ bytes = Buffer.from(await res.arrayBuffer());
48164
+ } catch (e2) {
48165
+ throw new CliError(`Download failed (${file.url}): ${e2 instanceof Error ? e2.message : String(e2)}`);
48166
+ }
48167
+ const sha = createHash5("sha256").update(bytes).digest("hex");
48168
+ if (sha !== file.sha256)
48169
+ throw new CliError(`Downloaded archive is corrupt (sha256 mismatch) — try again.`);
48170
+ mkdirSync9(cacheRoot(), { recursive: true });
48171
+ const stage = mkdtempSync(join15(cacheRoot(), "dl-"));
48172
+ try {
48173
+ const archive = join15(stage, file.url.split("/").pop() ?? "archive");
48174
+ writeFileSync11(archive, bytes);
48175
+ const tar = spawnSync3("tar", ["-xf", archive, "-C", stage], { stdio: ["ignore", "ignore", "pipe"] });
48176
+ if (tar.status !== 0)
48177
+ throw new CliError(`Couldn't extract the renderer archive: ${tar.stderr?.toString() ?? "tar failed"}`);
48178
+ rmSync2(archive);
48179
+ if (!findExe(stage))
48180
+ throw new CliError("The downloaded archive doesn't contain the desktop host binary.");
48181
+ try {
48182
+ renameSync2(stage, dest);
48183
+ } catch {
48184
+ if (!findExe(dest))
48185
+ throw new CliError(`Couldn't move the renderer into ${dest}.`);
48186
+ }
48187
+ } finally {
48188
+ rmSync2(stage, { recursive: true, force: true });
48189
+ }
48190
+ };
48191
+ var resolveDesktopExe = async () => {
48192
+ const override = process.env.LECODES_DESKTOP_EXE;
48193
+ if (override) {
48194
+ if (!existsSync14(override))
48195
+ throw new CliError(`LECODES_DESKTOP_EXE points at "${override}" which doesn't exist.`);
48196
+ return resolve4(override);
48197
+ }
48198
+ const version = selectedDesktopVersion();
48199
+ const dir = join15(cacheRoot(), `desktop-${version}`);
48200
+ let exe = existsSync14(dir) ? findExe(dir) : null;
48201
+ if (!exe) {
48202
+ await downloadDesktop(version, dir);
48203
+ exe = findExe(dir);
48204
+ if (!exe)
48205
+ throw new CliError(`Renderer cache at ${dir} is broken — delete the folder and retry.`);
48206
+ }
48207
+ return exe;
48208
+ };
48209
+ var updateDesktop = async () => {
48210
+ const { latest } = await fetchDesktopManifest();
48211
+ if (semverNewer(latest, DESKTOP_RENDERER_VERSION) > 0) {
48212
+ throw new CliError(`The newest published LeCodes Desktop is ${latest}, but this CLI needs ≥ ${DESKTOP_RENDERER_VERSION} ` + `for the render contract. Publish the desktop release first.`);
48213
+ }
48214
+ const before = selectedDesktopVersion();
48215
+ const dir = join15(cacheRoot(), `desktop-${latest}`);
48216
+ const hadIt = existsSync14(dir) && findExe(dir) !== null;
48217
+ if (!hadIt)
48218
+ await downloadDesktop(latest, dir);
48219
+ mkdirSync9(cacheRoot(), { recursive: true });
48220
+ writeFileSync11(currentFile(), JSON.stringify({ version: latest }, null, 2));
48221
+ if (latest === before && hadIt)
48222
+ note(`Already up to date: LeCodes Desktop ${latest}.`);
48223
+ else if (latest === before)
48224
+ note(`Re-downloaded LeCodes Desktop ${latest}.`);
48225
+ else
48226
+ note(`Desktop renderer set to ${latest} (was ${before}).`);
48227
+ };
48228
+ var windowEnv = (opts) => {
48229
+ const env = {};
48230
+ const width = opts.width ?? (opts.height !== undefined ? 1280 : undefined);
48231
+ const height = opts.height ?? (opts.width !== undefined ? 720 : undefined);
48232
+ if (width && height)
48233
+ env.CREATOR_WINDOW_SIZE = `${width}x${height}`;
48234
+ if (opts.minWidth && opts.minHeight)
48235
+ env.CREATOR_MIN_WINDOW_SIZE = `${opts.minWidth}x${opts.minHeight}`;
48236
+ if (opts.title)
48237
+ env.CREATOR_WINDOW_TITLE = opts.title;
48238
+ if (opts.resizable === false)
48239
+ env.CREATOR_WINDOW_FIXED = "1";
48240
+ if (opts.fullscreen !== undefined)
48241
+ env.CREATOR_WINDOW_FULLSCREEN = opts.fullscreen ? "1" : "0";
48242
+ if (opts.scale === "fit")
48243
+ env.CREATOR_UI_SCALE = "fit";
48244
+ else if (typeof opts.scale === "number")
48245
+ env.CREATOR_UI_SCALE = String(opts.scale);
48246
+ return env;
48247
+ };
48248
+ var stageBundle = (js, resources) => {
48249
+ const runDir = mkdtempSync(join15(tmpdir(), "lecodes-render-"));
48250
+ writeFileSync11(join15(runDir, "app.js"), js);
48251
+ for (const r2 of resources) {
48252
+ if (r2.absPath && existsSync14(r2.absPath) && statSync3(r2.absPath).isFile()) {
48253
+ copyFileSync(r2.absPath, join15(runDir, r2.name));
48254
+ }
48255
+ }
48256
+ return runDir;
48257
+ };
48258
+ var runDesktopApp = async (js, resources, opts = {}) => {
48259
+ const exe = await resolveDesktopExe();
48260
+ const runDir = stageBundle(js, resources);
48261
+ try {
48262
+ const env = { ...process.env, ...windowEnv(opts) };
48263
+ await new Promise((done, fail) => {
48264
+ const child = spawn2(exe, [join15(runDir, "app.js")], { cwd: runDir, env, stdio: ["ignore", "inherit", "inherit"] });
48265
+ child.on("error", (e2) => fail(new CliError(`Can't run the desktop host: ${e2.message}`)));
48266
+ child.on("exit", (code) => {
48267
+ if (code === 0 || code === null)
48268
+ done();
48269
+ else
48270
+ fail(new CliError(`The desktop host exited with code ${code}.`));
48271
+ });
48272
+ });
48273
+ } finally {
48274
+ rmSync2(runDir, { recursive: true, force: true });
48275
+ }
48276
+ };
48277
+ var runDesktopDev = async (url, opts = {}) => {
48278
+ const exe = await resolveDesktopExe();
48279
+ const env = { ...process.env, ...windowEnv(opts) };
48280
+ await new Promise((done, fail) => {
48281
+ const child = spawn2(exe, [url], { env, stdio: ["ignore", "ignore", "inherit"] });
48282
+ child.on("error", (e2) => fail(new CliError(`Can't run the desktop host: ${e2.message}`)));
48283
+ child.on("exit", () => done());
48284
+ });
48285
+ };
48286
+ var runDesktopRender = async (js, resources, opts) => {
48287
+ const exe = await resolveDesktopExe();
48288
+ const runDir = stageBundle(js, resources);
48289
+ try {
48290
+ const appJs = join15(runDir, "app.js");
48291
+ const outAbs = resolve4(opts.outPng);
48292
+ await new Promise((done, fail) => {
48293
+ const child = spawn2(exe, [appJs], {
48294
+ cwd: runDir,
48295
+ env: {
48296
+ ...process.env,
48297
+ CREATOR_DUMP_PNG: outAbs,
48298
+ CREATOR_MAX_FRAMES: String(opts.frames),
48299
+ CREATOR_WINDOW_SIZE: `${opts.width}x${opts.height}`,
48300
+ CREATOR_HIDDEN: "1"
48301
+ },
48302
+ stdio: ["ignore", "pipe", "pipe"]
48303
+ });
48304
+ const tail = [];
48305
+ const sink = (d) => {
48306
+ const text = String(d);
48307
+ tail.push(text);
48308
+ while (tail.length > 80)
48309
+ tail.shift();
48310
+ if (opts.logs)
48311
+ process.stderr.write(text);
48312
+ };
48313
+ child.stdout.on("data", sink);
48314
+ child.stderr.on("data", sink);
48315
+ const killer = setTimeout(() => {
48316
+ child.kill();
48317
+ fail(new CliError(`The native render timed out after ${opts.timeoutMs}ms (raise --timeout, or lower --frames).`));
48318
+ }, opts.timeoutMs);
48319
+ child.on("error", (e2) => {
48320
+ clearTimeout(killer);
48321
+ fail(new CliError(`Can't run the desktop renderer: ${e2.message}`));
48322
+ });
48323
+ child.on("exit", (code) => {
48324
+ clearTimeout(killer);
48325
+ if (code === 0 && existsSync14(outAbs))
48326
+ return done();
48327
+ const detail = tail.length > 0 ? `
48328
+ ${tail.join("").trimEnd()}` : "";
48329
+ fail(new CliError(code === 0 ? `The renderer exited cleanly but produced no PNG — did the app open a screen?${detail}` : `The renderer exited with code ${code}.${detail}`));
48330
+ });
48331
+ });
48332
+ } finally {
48333
+ rmSync2(runDir, { recursive: true, force: true });
48334
+ }
48335
+ };
48336
+
48337
+ // src/commands/appDesktop.ts
48338
+ init_util();
48339
+ var mb = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)} MB`;
48340
+ var dirSize = (dir) => readdirSync6(dir, { withFileTypes: true }).reduce((sum, e2) => {
48341
+ const p2 = join16(dir, e2.name);
48342
+ return sum + (e2.isDirectory() ? dirSize(p2) : statSync4(p2).size);
48343
+ }, 0);
48344
+ var initDesktop = async (args) => {
48345
+ const root = projectRoot();
48346
+ const existing = readJson(join16(root, "app.json")) ?? {};
48347
+ const name = flagStr(args, "name") ?? existing.name ?? basename5(root);
48348
+ const window2 = {
48349
+ width: 1280,
48350
+ height: 800,
48351
+ minWidth: 640,
48352
+ minHeight: 480,
48353
+ ...existing.desktop?.window,
48354
+ ...flagBool(args, "fullscreen") ? { fullscreen: true } : {}
48355
+ };
48356
+ const config = { ...existing, name, desktop: { ...existing.desktop, window: window2 } };
48357
+ writeJson(join16(root, "app.json"), config);
48358
+ log(`${c.green("written")} app.json (desktop.window ${window2.width}×${window2.height}${window2.fullscreen ? ", fullscreen" : ""})`);
48359
+ log(`Next: ${c.bold("lecodes dev --desktop")} to develop, ${c.bold("lecodes app build desktop")} to package.`);
48360
+ };
48361
+ var cleanFileName = (name) => name.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "").replace(/[. ]+$/, "").trim() || "app";
48362
+ var buildDesktop = async (args) => {
48363
+ const root = projectRoot();
48364
+ const config = readJson(join16(root, "app.json")) ?? {};
48365
+ const name = (config.name ?? basename5(root)).trim() || "app";
48366
+ const slug = flagStr(args, "slug") ?? cleanFileName(name);
48367
+ const out = resolve5(root, flagStr(args, "o", "out") ?? join16(root, "dist", slug));
48368
+ if (existsSync15(out)) {
48369
+ const entries = readdirSync6(out);
48370
+ const looksLikeBuild = entries.length === 0 || entries.includes("app.js");
48371
+ if (!looksLikeBuild) {
48372
+ throw new CliError(`${out} exists and doesn't look like a previous desktop build — pick another -o or clear it yourself.`);
48373
+ }
48374
+ }
48375
+ if (config.desktop?.entry && flagStr(args, "entry") === undefined)
48376
+ args.flags.entry = config.desktop.entry;
48377
+ note(`Building "${name}" for desktop…`);
48378
+ const { js, resources } = await buildBundle(args, { remotePatterns: config.resources?.remote ?? [] });
48379
+ const wanted = new Set([...js.matchAll(/fetchLocal\("([^"]+)"\)/g)].map((m) => m[1]));
48380
+ const shipped = resources.filter((r2) => !r2.remote && wanted.has(r2.name));
48381
+ const byName = new Map;
48382
+ for (const r2 of resources) {
48383
+ if (r2.remote || !wanted.has(r2.name))
48384
+ continue;
48385
+ const set = byName.get(r2.name) ?? new Set;
48386
+ set.add(r2.path);
48387
+ byName.set(r2.name, set);
48388
+ }
48389
+ const collisions = [...byName.entries()].filter(([, paths]) => paths.size > 1);
48390
+ if (collisions.length > 0) {
48391
+ throw new CliError("Asset file names must be unique across the whole project — the bundle refers to assets " + `by BASENAME, so same-named files silently replace each other in the built app:
48392
+ ` + collisions.map(([n2, paths]) => ` ${n2}
48393
+ ` + [...paths].map((p2) => ` ${p2}`).join(`
48394
+ `)).join(`
48395
+ `));
48396
+ }
48397
+ const shippedNames = new Set(shipped.map((r2) => r2.name));
48398
+ const missing = [...wanted].filter((n2) => !shippedNames.has(n2));
48399
+ if (missing.length > 0) {
48400
+ throw new CliError(`The bundle references assets with no local file: ${missing.join(", ")}`);
48401
+ }
48402
+ const exe = await resolveDesktopExe();
48403
+ const hostDir = dirname11(exe);
48404
+ const version = process.env.LECODES_DESKTOP_EXE ? "local build" : selectedDesktopVersion();
48405
+ rmSync3(out, { recursive: true, force: true });
48406
+ mkdirSync10(out, { recursive: true });
48407
+ for (const entry of readdirSync6(hostDir, { withFileTypes: true })) {
48408
+ const keep = entry.isFile() && entry.name.toLowerCase().endsWith(".dll") || entry.isDirectory() && (entry.name === "assets" || entry.name === "fonts");
48409
+ if (keep)
48410
+ cpSync2(join16(hostDir, entry.name), join16(out, entry.name), { recursive: true });
48411
+ }
48412
+ if (!existsSync15(join16(out, "assets", "initial.js"))) {
48413
+ throw new CliError(`The host at ${hostDir} has no assets/initial.js next to it — a staged build can't boot. ` + `(LECODES_DESKTOP_EXE must point at an exe with its runtime assets/ beside it.)`);
48414
+ }
48415
+ const exeName2 = process.platform === "win32" ? `${cleanFileName(name)}.exe` : cleanFileName(name);
48416
+ cpSync2(exe, join16(out, exeName2));
48417
+ note(` host: LeCodes Desktop ${version}`);
48418
+ writeFileSync12(join16(out, "app.js"), js);
48419
+ let assetBytes = 0, assetCount = 0;
48420
+ for (const r2 of shipped) {
48421
+ if (!r2.absPath || !existsSync15(r2.absPath) || !statSync4(r2.absPath).isFile()) {
48422
+ warn(`asset "${r2.name}" (${r2.path}) has no local file — skipped`);
48423
+ continue;
48424
+ }
48425
+ cpSync2(r2.absPath, join16(out, r2.name));
48426
+ assetBytes += statSync4(r2.absPath).size;
48427
+ assetCount++;
48428
+ }
48429
+ note(` assets: ${assetCount} files, ${mb(assetBytes)}`);
48430
+ writeJson(join16(out, "app.json"), { name, ...config.desktop ? { desktop: { window: config.desktop.window } } : {} });
48431
+ if (config.icon) {
48432
+ await writeIco(root, config, join16(out, "app.ico"));
48433
+ note(" app.ico rendered from " + config.icon);
48434
+ }
48435
+ writeFileSync12(join16(out, "README.txt"), "\uFEFF" + readmeText(name, exeName2, version));
48436
+ success(`Portable build: ${out} (${mb(dirSize(out))})`);
48437
+ if (flagBool(args, "probe"))
48438
+ await probeBuild(out, exeName2, config, args);
48439
+ if (flagBool(args, "zip"))
48440
+ zipBuild(out);
48441
+ if (!flagBool(args, "probe"))
48442
+ note(`Tip: --probe renders the staged folder headless to verify exactly what ships.`);
48443
+ };
48444
+ var writeIco = async (root, config, dest) => {
48445
+ const { image: image2 } = await loadIconMaster(root, config);
48446
+ const sizes = [16, 24, 32, 48, 64, 256];
48447
+ const pngs = [];
48448
+ for (const s2 of sizes) {
48449
+ const frame = image2.clone();
48450
+ frame.resize({ w: s2, h: s2 });
48451
+ pngs.push(Buffer.from(await frame.getBuffer("image/png")));
48452
+ }
48453
+ const header = Buffer.alloc(6 + sizes.length * 16);
48454
+ header.writeUInt16LE(0, 0);
48455
+ header.writeUInt16LE(1, 2);
48456
+ header.writeUInt16LE(sizes.length, 4);
48457
+ let offset = header.length;
48458
+ sizes.forEach((s2, i2) => {
48459
+ const e2 = 6 + i2 * 16;
48460
+ header.writeUInt8(s2 === 256 ? 0 : s2, e2);
48461
+ header.writeUInt8(s2 === 256 ? 0 : s2, e2 + 1);
48462
+ header.writeUInt16LE(1, e2 + 4);
48463
+ header.writeUInt16LE(32, e2 + 6);
48464
+ header.writeUInt32LE(pngs[i2].length, e2 + 8);
48465
+ header.writeUInt32LE(offset, e2 + 12);
48466
+ offset += pngs[i2].length;
48467
+ });
48468
+ writeFileSync12(dest, Buffer.concat([header, ...pngs]));
48469
+ };
48470
+ var probeBuild = async (out, exeName2, config, args) => {
48471
+ const win = config.desktop?.window;
48472
+ const frames = Number(flagStr(args, "frames")) || 90;
48473
+ const shot = join16(dirname11(out), `${basename5(out)}-probe.png`);
48474
+ note(`Probe: hidden render of the staged folder (${frames} frames)…`);
48475
+ await new Promise((done, fail) => {
48476
+ const child = spawn3(join16(out, exeName2), [], {
48477
+ cwd: out,
48478
+ env: {
48479
+ ...process.env,
48480
+ CREATOR_HIDDEN: "1",
48481
+ CREATOR_MAX_FRAMES: String(frames),
48482
+ CREATOR_DUMP_PNG: shot,
48483
+ CREATOR_WINDOW_SIZE: `${win?.width ?? 1280}x${win?.height ?? 720}`
48484
+ },
48485
+ stdio: ["ignore", "pipe", "pipe"]
48486
+ });
48487
+ const tail = [];
48488
+ const sink = (d) => {
48489
+ tail.push(String(d));
48490
+ while (tail.length > 60)
48491
+ tail.shift();
48492
+ };
48493
+ child.stdout.on("data", sink);
48494
+ child.stderr.on("data", sink);
48495
+ const killer = setTimeout(() => {
48496
+ child.kill();
48497
+ fail(new CliError("The probe timed out after 120s — raise --frames headroom or check the app."));
48498
+ }, 120000);
48499
+ child.on("error", (e2) => {
48500
+ clearTimeout(killer);
48501
+ fail(new CliError(`Probe couldn't run: ${e2.message}`));
48502
+ });
48503
+ child.on("exit", (code) => {
48504
+ clearTimeout(killer);
48505
+ if (code === 0 && existsSync15(shot)) {
48506
+ success(`Probe: ${shot}`);
48507
+ done();
48508
+ } else {
48509
+ const detail = tail.length > 0 ? `
48510
+ ${tail.join("").trimEnd()}` : "";
48511
+ fail(new CliError(`Probe failed (exit ${code}).${detail}`));
48512
+ }
48513
+ });
48514
+ });
48515
+ };
48516
+ var zipBuild = (out) => {
48517
+ const zip = `${out}.zip`;
48518
+ rmSync3(zip, { force: true });
48519
+ note("Zipping…");
48520
+ if (process.platform === "win32") {
48521
+ const q = (s2) => s2.replaceAll("'", "''");
48522
+ execFileSync3("powershell", ["-NoProfile", "-Command", [
48523
+ "Add-Type -AssemblyName System.IO.Compression.FileSystem;",
48524
+ `$src='${q(out)}'; $base=Split-Path $src -Parent;`,
48525
+ `$zip=[System.IO.Compression.ZipFile]::Open('${q(zip)}','Create');`,
48526
+ "Get-ChildItem $src -Recurse -File | ForEach-Object {",
48527
+ " $rel=$_.FullName.Substring($base.Length+1).Replace('\\','/');",
48528
+ " [void][System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip,$_.FullName,$rel,'Optimal') };",
48529
+ "$zip.Dispose()"
48530
+ ].join(" ")], { stdio: ["ignore", "ignore", "pipe"] });
48531
+ } else {
48532
+ execFileSync3("zip", ["-rq", zip, basename5(out)], { cwd: dirname11(out) });
48533
+ }
48534
+ success(`Zip: ${zip} (${mb(statSync4(zip).size)})`);
48535
+ };
48536
+ var readmeText = (name, exeName2, version) => `${name}
48537
+ ${"=".repeat(Math.max(name.length, 4))}
48538
+
48539
+ A LeCodes desktop app. Nothing to install: unpack the WHOLE folder anywhere and run ${exeName2}.
48540
+ Keep the files together — the app reads its assets from this folder.
48541
+
48542
+ On first run Windows may show a SmartScreen prompt (any unsigned app gets one):
48543
+ choose "More info", then "Run anyway".
48544
+
48545
+ Requires 64-bit Windows 10/11.
48546
+
48547
+ Built with LeCodes Desktop ${version}.
48548
+ `;
48549
+
47987
48550
  // src/commands/app.ts
47988
48551
  var SDK_REPO = "letary/lecodes-ios-sdk";
47989
48552
  var SDK_MIN_VERSION = "1.2.0";
47990
- var SDK_VARIANTS = ["core", "2d", "3d", "full"];
47991
48553
  var SDK_IDENTITY_VERSION = "1.6.0";
47992
48554
  var shellSdkVersion = (iosDir) => {
47993
- const path2 = join15(iosDir, "LeCodesRuntime", "Package.swift");
47994
- if (!existsSync14(path2))
48555
+ const path2 = join17(iosDir, "LeCodesRuntime", "Package.swift");
48556
+ if (!existsSync16(path2))
47995
48557
  return null;
47996
- const m = /lecodes-ios-sdk\.git",\s*(?:exact|from):\s*"([^"]+)"/.exec(readFileSync14(path2, "utf8"));
48558
+ const m = /lecodes-ios-sdk\.git",\s*(?:exact|from):\s*"([^"]+)"/.exec(readFileSync15(path2, "utf8"));
47997
48559
  return m && VERSION_RE.test(m[1]) ? m[1] : null;
47998
48560
  };
47999
48561
  var IOS_SDK_OPTS = { repo: SDK_REPO, minVersion: SDK_MIN_VERSION, configKey: "ios.sdk" };
@@ -48001,15 +48563,17 @@ var appInit = async (args) => {
48001
48563
  const platform = args._[1];
48002
48564
  if (platform === "android")
48003
48565
  return initAndroid(args);
48566
+ if (platform === "desktop")
48567
+ return initDesktop(args);
48004
48568
  if (platform !== "ios") {
48005
- throw new CliError(platform ? `Unsupported platform "${platform}" — use ios or android.` : "Usage: lecodes app init <ios|android> [--name <display name>] [--bundle-id <id>] [--team <team id>] [--variant auto|core|2d|3d|full] [--app-id <application id>] [--sdk <version|latest>]");
48569
+ throw new CliError(platform ? `Unsupported platform "${platform}" — use ios, android or desktop.` : "Usage: lecodes app init <ios|android|desktop> [--name <display name>] [--bundle-id <id>] [--team <team id>] [--variant auto|core|2d|3d|full] [--app-id <application id>] [--sdk <version|latest>]");
48006
48570
  }
48007
48571
  const root = projectRoot();
48008
- const iosDir = join15(root, "ios");
48009
- if (existsSync14(iosDir)) {
48572
+ const iosDir = join17(root, "ios");
48573
+ if (existsSync16(iosDir)) {
48010
48574
  throw new CliError("ios/ already exists — run `lecodes app sync` to update it.");
48011
48575
  }
48012
- const existing = readJson(join15(root, "app.json")) ?? {};
48576
+ const existing = readJson(join17(root, "app.json")) ?? {};
48013
48577
  const variant = flagStr(args, "variant") ?? existing.ios?.variant ?? "auto";
48014
48578
  if (variant !== "auto" && !SDK_VARIANTS.includes(variant)) {
48015
48579
  throw new CliError(`--variant must be auto, ${SDK_VARIANTS.join(", ")} (got "${variant}")`);
@@ -48017,7 +48581,7 @@ var appInit = async (args) => {
48017
48581
  const sdkFlag = flagStr(args, "sdk") ?? existing.ios?.sdk;
48018
48582
  const sdkVersion = await resolveSdkVersion(process.env.LECODES_IOS_SDK ?? sdkFlag ?? "latest", null, IOS_SDK_OPTS);
48019
48583
  const sdk = sdkFlag === "latest" ? "latest" : sdkVersion;
48020
- const name = flagStr(args, "name") ?? existing.name ?? basename5(root);
48584
+ const name = flagStr(args, "name") ?? existing.name ?? basename6(root);
48021
48585
  const config = {
48022
48586
  ...existing,
48023
48587
  name,
@@ -48031,7 +48595,7 @@ var appInit = async (args) => {
48031
48595
  buildNumber: existing.ios?.buildNumber ?? 1
48032
48596
  }
48033
48597
  };
48034
- writeJson(join15(root, "app.json"), config);
48598
+ writeJson(join17(root, "app.json"), config);
48035
48599
  log(`${c.green("written")} app.json`);
48036
48600
  ensureShellIgnored(root, "ios");
48037
48601
  const identity = {
@@ -48042,9 +48606,9 @@ var appInit = async (args) => {
48042
48606
  buildNumber: config.ios.buildNumber
48043
48607
  };
48044
48608
  const write = (rel, content) => {
48045
- const abs = join15(iosDir, rel);
48046
- mkdirSync9(dirname11(abs), { recursive: true });
48047
- writeFileSync11(abs, content);
48609
+ const abs = join17(iosDir, rel);
48610
+ mkdirSync11(dirname12(abs), { recursive: true });
48611
+ writeFileSync13(abs, content);
48048
48612
  log(`${c.green("created")} ios/${rel}`);
48049
48613
  };
48050
48614
  write("README.md", IOS_README);
@@ -48068,54 +48632,27 @@ var appInit = async (args) => {
48068
48632
  log(` ${c.bold("open ios/App.xcodeproj")} then run on your device from Xcode`);
48069
48633
  log(` App icon: set app.json "icon" to a square ≥1024px image — sync renders every size (or drop a PNG into App/Assets.xcassets/AppIcon.appiconset/)`);
48070
48634
  };
48071
- var detectEngines = (bundleJs) => {
48072
- const lines = [];
48073
- for (const line of bundleJs.split(`
48074
- `)) {
48075
- if (!line.startsWith("//"))
48076
- break;
48077
- lines.push(line);
48078
- }
48079
- const header = lines.join(`
48080
- `);
48081
- return { gl: !/^\/\/ gl: disable$/m.test(header), g2: /^\/\/ 2d: enable$/m.test(header) };
48082
- };
48083
- var resolveVariant = (requested, bundleJs) => {
48084
- const uses = detectEngines(bundleJs);
48085
- const detected = uses.gl && uses.g2 ? "full" : uses.gl ? "3d" : uses.g2 ? "2d" : "core";
48086
- if (requested === "auto")
48087
- return { variant: detected, detected };
48088
- const variant = requested;
48089
- const missing = [
48090
- uses.gl && variant !== "3d" && variant !== "full" ? "3D" : null,
48091
- uses.g2 && variant !== "2d" && variant !== "full" ? "2D" : null
48092
- ].filter(Boolean);
48093
- if (missing.length > 0) {
48094
- warn(`project uses the ${missing.join(" + ")} engine but ios.variant forces "${variant}" — those scenes won't run on device; remove ios.variant to auto-select "${detected}".`);
48095
- }
48096
- return { variant, detected };
48097
- };
48098
48635
  var ensureSdkResourcesBundle = async (sdkVersion) => {
48099
48636
  const override = process.env.LECODES_SDK_RESOURCES;
48100
48637
  if (override) {
48101
48638
  note(`Using local resources bundle ${override} (LECODES_SDK_RESOURCES)`);
48102
- return resolve4(override);
48639
+ return resolve6(override);
48103
48640
  }
48104
- const dir = join15(homedir5(), ".lecodes", "sdk-resources", sdkVersion);
48105
- const bundle = join15(dir, "LeCodesSDKResources.bundle");
48106
- if (existsSync14(bundle))
48641
+ const dir = join17(homedir6(), ".lecodes", "sdk-resources", sdkVersion);
48642
+ const bundle = join17(dir, "LeCodesSDKResources.bundle");
48643
+ if (existsSync16(bundle))
48107
48644
  return bundle;
48108
48645
  const url = `https://github.com/${SDK_REPO}/releases/download/${sdkVersion}/LeCodesSDKResources.bundle.zip`;
48109
48646
  note(`Downloading LeCodesSDKResources.bundle (SDK ${sdkVersion})…`);
48110
48647
  const res = await fetch(url);
48111
48648
  if (!res.ok)
48112
48649
  throw new CliError(`Resources bundle download failed (HTTP ${res.status}) — ${url}`);
48113
- mkdirSync9(dir, { recursive: true });
48114
- const zip = join15(dir, "LeCodesSDKResources.bundle.zip");
48115
- writeFileSync11(zip, new Uint8Array(await res.arrayBuffer()));
48116
- execFileSync3("ditto", ["-x", "-k", zip, dir]);
48117
- rmSync2(zip);
48118
- if (!existsSync14(bundle))
48650
+ mkdirSync11(dir, { recursive: true });
48651
+ const zip = join17(dir, "LeCodesSDKResources.bundle.zip");
48652
+ writeFileSync13(zip, new Uint8Array(await res.arrayBuffer()));
48653
+ execFileSync4("ditto", ["-x", "-k", zip, dir]);
48654
+ rmSync4(zip);
48655
+ if (!existsSync16(bundle))
48119
48656
  throw new CliError(`No LeCodesSDKResources.bundle inside the release asset ${url}`);
48120
48657
  return bundle;
48121
48658
  };
@@ -48140,13 +48677,13 @@ var resolveUpdateUrl = (update, root) => {
48140
48677
  };
48141
48678
  var appSync = async (args) => {
48142
48679
  const root = projectRoot();
48143
- const config = readJson(join15(root, "app.json"));
48680
+ const config = readJson(join17(root, "app.json"));
48144
48681
  const platform = args._[1];
48145
48682
  if (platform !== undefined && platform !== "ios" && platform !== "android") {
48146
48683
  throw new CliError(`Unknown platform "${platform}" — use ios or android.`);
48147
48684
  }
48148
- const hasIos = !!config?.ios && existsSync14(join15(root, "ios"));
48149
- const hasAndroid = !!config?.android && existsSync14(join15(root, "android"));
48685
+ const hasIos = !!config?.ios && existsSync16(join17(root, "ios"));
48686
+ const hasAndroid = !!config?.android && existsSync16(join17(root, "android"));
48150
48687
  if (platform === "ios" && !hasIos)
48151
48688
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
48152
48689
  if (platform === "android" && !hasAndroid)
@@ -48161,7 +48698,7 @@ var appSync = async (args) => {
48161
48698
  await syncAndroid(root, config, iosArtifacts);
48162
48699
  };
48163
48700
  var syncIos = async (root, config) => {
48164
- const iosDir = join15(root, "ios");
48701
+ const iosDir = join17(root, "ios");
48165
48702
  if (!config.ios)
48166
48703
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
48167
48704
  const requestedVariant = config.ios.variant ?? "auto";
@@ -48171,14 +48708,14 @@ var syncIos = async (root, config) => {
48171
48708
  const linkedSdk = shellSdkVersion(iosDir);
48172
48709
  const sdkPin = process.env.LECODES_IOS_SDK ?? config.ios.sdk ?? linkedSdk ?? "latest";
48173
48710
  const sdkVersion = await resolveSdkVersion(sdkPin, linkedSdk, IOS_SDK_OPTS);
48174
- const lockPath = join15(root, "plugins.lock.json");
48711
+ const lockPath = join17(root, "plugins.lock.json");
48175
48712
  const oldLock = readJson(lockPath)?.plugins ?? {};
48176
48713
  const pluginSpecs = Object.entries(config.plugins ?? {});
48177
48714
  const runtimePlugins = [];
48178
48715
  const infoValues = {};
48179
48716
  const entitlementValues = {};
48180
48717
  const newLock = {};
48181
- const sourcesDir = join15(iosDir, "LeCodesRuntime", "Sources");
48718
+ const sourcesDir = join17(iosDir, "LeCodesRuntime", "Sources");
48182
48719
  for (const [id, spec] of pluginSpecs) {
48183
48720
  const plugin = resolvePlugin(id, spec, oldLock[id], "ios");
48184
48721
  const ios = plugin.manifest.ios;
@@ -48186,12 +48723,12 @@ var syncIos = async (root, config) => {
48186
48723
  const register = ios.register;
48187
48724
  if (!module || !register)
48188
48725
  throw new CliError(`Plugin "${id}": manifest ios.module / ios.register missing`);
48189
- const src = join15(plugin.dir, ios.sources ?? "ios");
48190
- if (!existsSync14(src))
48726
+ const src = join17(plugin.dir, ios.sources ?? "ios");
48727
+ if (!existsSync16(src))
48191
48728
  throw new CliError(`Plugin "${id}": sources dir ${src} missing`);
48192
- const dest = join15(sourcesDir, module);
48193
- rmSync2(dest, { recursive: true, force: true });
48194
- cpSync2(src, dest, { recursive: true });
48729
+ const dest = join17(sourcesDir, module);
48730
+ rmSync4(dest, { recursive: true, force: true });
48731
+ cpSync3(src, dest, { recursive: true });
48195
48732
  if (ios.minSdk && VERSION_RE.test(ios.minSdk) && cmpVersion(sdkVersion, ios.minSdk) < 0) {
48196
48733
  throw new CliError(`Plugin "${id}" needs lecodes-ios-sdk ${ios.minSdk} but the shell links ${sdkVersion} — run \`lecodes app update\` (or set app.json ios.sdk).`);
48197
48734
  }
@@ -48206,9 +48743,9 @@ var syncIos = async (root, config) => {
48206
48743
  log(`${c.green("plugin")} ${id}@${plugin.lock.version} (${plugin.lock.tag} → Sources/${module})`);
48207
48744
  }
48208
48745
  const keep = new Set(["LeCodesRuntime", ...runtimePlugins.map((p2) => p2.module)]);
48209
- for (const entry of existsSync14(sourcesDir) ? readdirSync5(sourcesDir) : []) {
48210
- if (!keep.has(entry) && statSync3(join15(sourcesDir, entry)).isDirectory()) {
48211
- rmSync2(join15(sourcesDir, entry), { recursive: true });
48746
+ for (const entry of existsSync16(sourcesDir) ? readdirSync7(sourcesDir) : []) {
48747
+ if (!keep.has(entry) && statSync5(join17(sourcesDir, entry)).isDirectory()) {
48748
+ rmSync4(join17(sourcesDir, entry), { recursive: true });
48212
48749
  log(`${c.yellow("removed")} ios/LeCodesRuntime/Sources/${entry} (plugin uninstalled)`);
48213
48750
  }
48214
48751
  }
@@ -48216,11 +48753,11 @@ var syncIos = async (root, config) => {
48216
48753
  if (!Array.isArray(remotePatterns) || remotePatterns.some((p2) => typeof p2 !== "string")) {
48217
48754
  throw new CliError('app.json "resources.remote" must be an array of glob strings (e.g. ["videos/**"]).');
48218
48755
  }
48219
- const resourcesDir = join15(iosDir, "App", "Resources");
48220
- rmSync2(resourcesDir, { recursive: true, force: true });
48221
- mkdirSync9(resourcesDir, { recursive: true });
48222
- const resources = await compileShell({ _: [], flags: { o: join15(resourcesDir, "app.js") } }, { remotePatterns });
48223
- const bundleJs = readFileSync14(join15(resourcesDir, "app.js"), "utf8");
48756
+ const resourcesDir = join17(iosDir, "App", "Resources");
48757
+ rmSync4(resourcesDir, { recursive: true, force: true });
48758
+ mkdirSync11(resourcesDir, { recursive: true });
48759
+ const resources = await compileShell({ _: [], flags: { o: join17(resourcesDir, "app.js") } }, { remotePatterns });
48760
+ const bundleJs = readFileSync15(join17(resourcesDir, "app.js"), "utf8");
48224
48761
  const { variant, detected } = resolveVariant(requestedVariant, bundleJs);
48225
48762
  const WHY = {
48226
48763
  core: "UI only",
@@ -48234,8 +48771,8 @@ var syncIos = async (root, config) => {
48234
48771
  const sdkProvided = new Set;
48235
48772
  if (variant === "3d" || variant === "full") {
48236
48773
  const sdkResources = await ensureSdkResourcesBundle(sdkVersion);
48237
- cpSync2(sdkResources, join15(resourcesDir, "LeCodesSDKResources.bundle"), { recursive: true });
48238
- for (const f3 of readdirSync5(sdkResources))
48774
+ cpSync3(sdkResources, join17(resourcesDir, "LeCodesSDKResources.bundle"), { recursive: true });
48775
+ for (const f3 of readdirSync7(sdkResources))
48239
48776
  if (f3 !== "Info.plist")
48240
48777
  sdkProvided.add(f3);
48241
48778
  log(`${c.green("resources")} LeCodesSDKResources.bundle (filament) → ios/App/Resources/`);
@@ -48262,7 +48799,7 @@ var syncIos = async (root, config) => {
48262
48799
  }
48263
48800
  if (candidates.length > 1)
48264
48801
  warn(`asset name collision: ${name} (${candidates.map((r2) => r2.path).join(", ")}) — embedding ${candidates[0].path}`);
48265
- cpSync2(candidates[0].absPath, join15(resourcesDir, name));
48802
+ cpSync3(candidates[0].absPath, join17(resourcesDir, name));
48266
48803
  embedded++;
48267
48804
  }
48268
48805
  if (embedded > 0)
@@ -48282,20 +48819,28 @@ var syncIos = async (root, config) => {
48282
48819
  }
48283
48820
  if (shellUuid)
48284
48821
  log(`${c.green("identity")} project ${shellUuid} baked into the shell (world identity)`);
48285
- writeFileSync11(join15(iosDir, "README.md"), IOS_README);
48286
- writeFileSync11(join15(iosDir, "LeCodesRuntime", "Package.swift"), runtimePackageTemplate(sdkVersion, variant, runtimePlugins));
48287
- writeFileSync11(join15(sourcesDir, "LeCodesRuntime", "LeCodesRuntime.swift"), runtimeSwiftTemplate(runtimePlugins, shellUuid));
48822
+ writeFileSync13(join17(iosDir, "README.md"), IOS_README);
48823
+ writeFileSync13(join17(iosDir, "LeCodesRuntime", "Package.swift"), runtimePackageTemplate(sdkVersion, variant, runtimePlugins));
48824
+ writeFileSync13(join17(sourcesDir, "LeCodesRuntime", "LeCodesRuntime.swift"), runtimeSwiftTemplate(runtimePlugins, shellUuid));
48288
48825
  const mergedKeys = {};
48289
48826
  for (const [key, values] of Object.entries(infoValues))
48290
48827
  mergedKeys[key] = [...new Set(values)].join(" ");
48291
48828
  const updateUrl = resolveUpdateUrl(config.update, root);
48292
- writeFileSync11(join15(iosDir, "App", "Info.plist"), infoPlistTemplate(mergedKeys, updateUrl));
48829
+ writeFileSync13(join17(iosDir, "App", "Info.plist"), infoPlistTemplate(mergedKeys, updateUrl));
48293
48830
  if (updateUrl)
48294
48831
  log(`${c.green("update")} OTA source ${updateUrl} — checked natively at launch, new code applies next launch`);
48295
- if (pluginSpecs.length > 0 || existsSync14(lockPath))
48832
+ else if (!config.update) {
48833
+ let manifest = null;
48834
+ try {
48835
+ manifest = readManifest(root);
48836
+ } catch {}
48837
+ if (manifest)
48838
+ note(`tip: add "update": true to app.json for over-the-air JS updates — ship with \`lecodes push --publish\`, no store release`);
48839
+ }
48840
+ if (pluginSpecs.length > 0 || existsSync16(lockPath))
48296
48841
  writeJson(lockPath, { plugins: newLock });
48297
- const pbxPath = join15(iosDir, "App.xcodeproj", "project.pbxproj");
48298
- let pbx = readFileSync14(pbxPath, "utf8").replace(/MARKETING_VERSION = [^;]+;/g, `MARKETING_VERSION = ${config.version ?? "1.0.0"};`).replace(/CURRENT_PROJECT_VERSION = [^;]+;/g, `CURRENT_PROJECT_VERSION = ${config.ios.buildNumber ?? 1};`);
48842
+ const pbxPath = join17(iosDir, "App.xcodeproj", "project.pbxproj");
48843
+ let pbx = readFileSync15(pbxPath, "utf8").replace(/MARKETING_VERSION = [^;]+;/g, `MARKETING_VERSION = ${config.version ?? "1.0.0"};`).replace(/CURRENT_PROJECT_VERSION = [^;]+;/g, `CURRENT_PROJECT_VERSION = ${config.ios.buildNumber ?? 1};`);
48299
48844
  const mergedEntitlements = {};
48300
48845
  for (const [key, values] of Object.entries(entitlementValues)) {
48301
48846
  if (values.every((v) => Array.isArray(v))) {
@@ -48316,7 +48861,7 @@ var syncIos = async (root, config) => {
48316
48861
  }
48317
48862
  const hasEntitlements = Object.keys(mergedEntitlements).length > 0;
48318
48863
  if (pbx.includes("CODE_SIGN_ENTITLEMENTS") || hasEntitlements) {
48319
- writeFileSync11(join15(iosDir, "App", "App.entitlements"), entitlementsTemplate(mergedEntitlements));
48864
+ writeFileSync13(join17(iosDir, "App", "App.entitlements"), entitlementsTemplate(mergedEntitlements));
48320
48865
  if (hasEntitlements)
48321
48866
  log(`${c.green("caps")} App/App.entitlements: ${Object.keys(mergedEntitlements).sort().join(", ")}`);
48322
48867
  if (!pbx.includes("CODE_SIGN_ENTITLEMENTS")) {
@@ -48332,7 +48877,7 @@ $2Info.plist,`);
48332
48877
  }
48333
48878
  }
48334
48879
  }
48335
- writeFileSync11(pbxPath, pbx);
48880
+ writeFileSync13(pbxPath, pbx);
48336
48881
  if (config.icon) {
48337
48882
  await writeIosAppIcon(iosDir, await loadIconMaster(root, config));
48338
48883
  }
@@ -48344,7 +48889,7 @@ $2Info.plist,`);
48344
48889
  var appPlugin = async (args) => {
48345
48890
  const sub = args._[1];
48346
48891
  const root = projectRoot();
48347
- const configPath = join15(root, "app.json");
48892
+ const configPath = join17(root, "app.json");
48348
48893
  const config = readJson(configPath);
48349
48894
  if (!config)
48350
48895
  throw new CliError("No app.json here — run `lecodes app init <ios|android>` first.");
@@ -48355,7 +48900,7 @@ var appPlugin = async (args) => {
48355
48900
  log("No plugins installed.");
48356
48901
  return;
48357
48902
  }
48358
- const lock = readJson(join15(root, "plugins.lock.json"))?.plugins ?? {};
48903
+ const lock = readJson(join17(root, "plugins.lock.json"))?.plugins ?? {};
48359
48904
  for (const [id2, spec] of entries) {
48360
48905
  const pin = lock[id2] ? c.dim(` ${lock[id2].repo} ${lock[id2].tag} (${lock[id2].commit.slice(0, 7)})`) : c.dim(" (not synced)");
48361
48906
  log(`${c.bold(id2)} ${spec}${pin}`);
@@ -48408,14 +48953,14 @@ var exportOptionsPlist = (method, teamId, destination) => `<?xml version="1.0" e
48408
48953
  `;
48409
48954
  var requireShell = () => {
48410
48955
  const root = projectRoot();
48411
- const config = readJson(join15(root, "app.json"));
48412
- if (!config?.ios || !existsSync14(join15(root, "ios"))) {
48956
+ const config = readJson(join17(root, "app.json"));
48957
+ if (!config?.ios || !existsSync16(join17(root, "ios"))) {
48413
48958
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
48414
48959
  }
48415
48960
  return { root, config };
48416
48961
  };
48417
48962
  var updateSdkPin = async (root, config, args, checkOnly, platform) => {
48418
- const [label, repo, minVersion, shellVersion] = platform === "ios" ? ["lecodes-ios-sdk", SDK_REPO, SDK_MIN_VERSION, () => shellSdkVersion(join15(root, "ios"))] : ["lecodes-android-sdk", ANDROID_SDK_REPO, ANDROID_SDK_MIN_VERSION, () => shellSdkVersionAndroid(join15(root, "android"))];
48963
+ const [label, repo, minVersion, shellVersion] = platform === "ios" ? ["lecodes-ios-sdk", SDK_REPO, SDK_MIN_VERSION, () => shellSdkVersion(join17(root, "ios"))] : ["lecodes-android-sdk", ANDROID_SDK_REPO, ANDROID_SDK_MIN_VERSION, () => shellSdkVersionAndroid(join17(root, "android"))];
48419
48964
  const section = config[platform];
48420
48965
  const wanted = flagStr(args, "sdk");
48421
48966
  let changed = false;
@@ -48446,19 +48991,19 @@ var updateSdkPin = async (root, config, args, checkOnly, platform) => {
48446
48991
  }
48447
48992
  if (!checkOnly && nextPin !== pin) {
48448
48993
  section.sdk = nextPin;
48449
- writeJson(join15(root, "app.json"), config);
48994
+ writeJson(join17(root, "app.json"), config);
48450
48995
  }
48451
48996
  return changed;
48452
48997
  };
48453
48998
  var appUpdate = async (args) => {
48454
48999
  const root = projectRoot();
48455
- const config = readJson(join15(root, "app.json"));
49000
+ const config = readJson(join17(root, "app.json"));
48456
49001
  const platform = args._[1];
48457
49002
  if (platform !== undefined && platform !== "ios" && platform !== "android") {
48458
49003
  throw new CliError(`Unknown platform "${platform}" — use ios or android.`);
48459
49004
  }
48460
- const hasIos = !!config?.ios && existsSync14(join15(root, "ios"));
48461
- const hasAndroid = !!config?.android && existsSync14(join15(root, "android"));
49005
+ const hasIos = !!config?.ios && existsSync16(join17(root, "ios"));
49006
+ const hasAndroid = !!config?.android && existsSync16(join17(root, "android"));
48462
49007
  if (platform === "ios" && !hasIos)
48463
49008
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
48464
49009
  if (platform === "android" && !hasAndroid)
@@ -48476,7 +49021,7 @@ var appUpdate = async (args) => {
48476
49021
  changed = await updateSdkPin(root, config, args, checkOnly, "ios") || changed;
48477
49022
  if (doAndroid)
48478
49023
  changed = await updateSdkPin(root, config, args, checkOnly, "android") || changed;
48479
- const lockPath = join15(root, "plugins.lock.json");
49024
+ const lockPath = join17(root, "plugins.lock.json");
48480
49025
  const lock = readJson(lockPath)?.plugins ?? {};
48481
49026
  const specs = Object.entries(config.plugins ?? {});
48482
49027
  if (specs.length > 0 && !flagBool(args, "no-plugins")) {
@@ -48505,7 +49050,7 @@ var appUpdate = async (args) => {
48505
49050
  log(`${c.green("plugin")} ${id} ${entry?.tag ?? "—"} → ${c.bold(tag)}`);
48506
49051
  changed = true;
48507
49052
  }
48508
- if (!checkOnly && existsSync14(lockPath))
49053
+ if (!checkOnly && existsSync16(lockPath))
48509
49054
  writeJson(lockPath, { plugins: kept });
48510
49055
  }
48511
49056
  log("");
@@ -48525,9 +49070,9 @@ var appUpdate = async (args) => {
48525
49070
  };
48526
49071
  var appDoctor = async (_args) => {
48527
49072
  const root = projectRoot();
48528
- const config = readJson(join15(root, "app.json"));
48529
- const hasIos = !!config?.ios && existsSync14(join15(root, "ios"));
48530
- const hasAndroid = !!config?.android && existsSync14(join15(root, "android"));
49073
+ const config = readJson(join17(root, "app.json"));
49074
+ const hasIos = !!config?.ios && existsSync16(join17(root, "ios"));
49075
+ const hasAndroid = !!config?.android && existsSync16(join17(root, "android"));
48531
49076
  if (!hasIos && !hasAndroid) {
48532
49077
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
48533
49078
  }
@@ -48538,14 +49083,14 @@ var appDoctor = async (_args) => {
48538
49083
  failed++;
48539
49084
  };
48540
49085
  if (config?.icon) {
48541
- check(existsSync14(join15(root, config.icon)), `Icon master ${config.icon}`, `app.json "icon" points at a missing file — sync will fail`);
49086
+ check(existsSync16(join17(root, config.icon)), `Icon master ${config.icon}`, `app.json "icon" points at a missing file — sync will fail`);
48542
49087
  }
48543
49088
  if (hasIos)
48544
49089
  await doctorIosChecks(root, config, check);
48545
49090
  if (hasAndroid) {
48546
49091
  await doctorAndroidChecks(root, config, check);
48547
49092
  if (!hasIos) {
48548
- const lock = readJson(join15(root, "plugins.lock.json"))?.plugins ?? {};
49093
+ const lock = readJson(join17(root, "plugins.lock.json"))?.plugins ?? {};
48549
49094
  const wanted = Object.keys(config.plugins ?? {});
48550
49095
  const inSync = wanted.every((id) => lock[id]) && Object.keys(lock).every((id) => wanted.includes(id));
48551
49096
  check(wanted.length === 0 || inSync, "Plugins synced", "run `lecodes app sync`");
@@ -48564,22 +49109,22 @@ var doctorIosChecks = async (root, config, check) => {
48564
49109
  } catch {}
48565
49110
  check(xcodeVersion !== "", `Xcode CLI (${xcodeVersion || "not found"})`, "install Xcode + command line tools");
48566
49111
  check(!!config.ios.teamId, "Development team set", "lecodes app init wrote app.json — fill ios.teamId", false);
48567
- const iconDir = join15(root, "ios", "App", "Assets.xcassets", "AppIcon.appiconset");
48568
- const hasIcon = existsSync14(iconDir) && readdirSync5(iconDir).some((f3) => f3.endsWith(".png"));
49112
+ const iconDir = join17(root, "ios", "App", "Assets.xcassets", "AppIcon.appiconset");
49113
+ const hasIcon = existsSync16(iconDir) && readdirSync7(iconDir).some((f3) => f3.endsWith(".png"));
48569
49114
  check(hasIcon, "App icon present", "drop a 1024×1024 PNG into ios/App/Assets.xcassets/AppIcon.appiconset/ (App Store requires it)", false);
48570
- const bundle = join15(root, "ios", "App", "Resources", "app.js");
48571
- check(existsSync14(bundle), "Bundled app.js", "run `lecodes app sync`");
49115
+ const bundle = join17(root, "ios", "App", "Resources", "app.js");
49116
+ check(existsSync16(bundle), "Bundled app.js", "run `lecodes app sync`");
48572
49117
  const requestedVariant = config.ios.variant ?? "auto";
48573
49118
  const validVariant = requestedVariant === "auto" || SDK_VARIANTS.includes(requestedVariant);
48574
49119
  check(validVariant, `ios.variant "${requestedVariant}"`, `use "auto" or one of ${SDK_VARIANTS.join(", ")}`);
48575
- if (existsSync14(bundle) && validVariant) {
48576
- const { variant } = resolveVariant(requestedVariant, readFileSync14(bundle, "utf8"));
48577
- const pkg = readFileSync14(join15(root, "ios", "LeCodesRuntime", "Package.swift"), "utf8");
49120
+ if (existsSync16(bundle) && validVariant) {
49121
+ const { variant } = resolveVariant(requestedVariant, readFileSync15(bundle, "utf8"));
49122
+ const pkg = readFileSync15(join17(root, "ios", "LeCodesRuntime", "Package.swift"), "utf8");
48578
49123
  const needsResources = variant === "3d" || variant === "full";
48579
- const hasResources = existsSync14(join15(root, "ios", "App", "Resources", "LeCodesSDKResources.bundle"));
49124
+ const hasResources = existsSync16(join17(root, "ios", "App", "Resources", "LeCodesSDKResources.bundle"));
48580
49125
  check(pkg.includes(`"LeCodesSDK-${variant}"`) && (!needsResources || hasResources), `SDK variant ${variant}${needsResources ? " (+ filament resources)" : ""}`, "run `lecodes app sync`");
48581
49126
  }
48582
- const linked = shellSdkVersion(join15(root, "ios"));
49127
+ const linked = shellSdkVersion(join17(root, "ios"));
48583
49128
  const pin = config.ios.sdk ?? "latest";
48584
49129
  check(!!linked, `SDK ${linked ?? "version unreadable"}`, "run `lecodes app sync`");
48585
49130
  if (linked && pin !== "latest") {
@@ -48591,24 +49136,27 @@ var doctorIosChecks = async (root, config, check) => {
48591
49136
  check(cmpVersion(linked, latest) >= 0, `SDK is the latest release (${latest})`, "run `lecodes app update`", false);
48592
49137
  } catch {}
48593
49138
  }
48594
- const lock = readJson(join15(root, "plugins.lock.json"))?.plugins ?? {};
49139
+ const lock = readJson(join17(root, "plugins.lock.json"))?.plugins ?? {};
48595
49140
  const wanted = Object.keys(config.plugins ?? {});
48596
49141
  const inSync = wanted.every((id) => lock[id]) && Object.keys(lock).every((id) => wanted.includes(id));
48597
49142
  check(wanted.length === 0 || inSync, "Plugins synced", "run `lecodes app sync`");
48598
- const pbxFile = join15(root, "ios", "App.xcodeproj", "project.pbxproj");
48599
- if (existsSync14(pbxFile) && readFileSync14(pbxFile, "utf8").includes("CODE_SIGN_ENTITLEMENTS")) {
48600
- check(existsSync14(join15(root, "ios", "App", "App.entitlements")), "App/App.entitlements present", "run `lecodes app sync`");
49143
+ const pbxFile = join17(root, "ios", "App.xcodeproj", "project.pbxproj");
49144
+ if (existsSync16(pbxFile) && readFileSync15(pbxFile, "utf8").includes("CODE_SIGN_ENTITLEMENTS")) {
49145
+ check(existsSync16(join17(root, "ios", "App", "App.entitlements")), "App/App.entitlements present", "run `lecodes app sync`");
48601
49146
  }
48602
49147
  if (wanted.includes("push")) {
48603
- const runtimeSwift = join15(root, "ios", "LeCodesRuntime", "Sources", "LeCodesRuntime", "LeCodesRuntime.swift");
48604
- const baked = existsSync14(runtimeSwift) && readFileSync14(runtimeSwift, "utf8").includes("bootProjectUuid");
49148
+ const runtimeSwift = join17(root, "ios", "LeCodesRuntime", "Sources", "LeCodesRuntime", "LeCodesRuntime.swift");
49149
+ const baked = existsSync16(runtimeSwift) && readFileSync15(runtimeSwift, "utf8").includes("bootProjectUuid");
48605
49150
  check(baked, "Push world identity baked", `without it Push.register() rejects "unavailable" — needs a pushed project (uuid), lecodes-ios-sdk ${SDK_IDENTITY_VERSION}+ and \`lecodes app sync\``, false);
48606
49151
  }
48607
49152
  if (config.update) {
48608
- const plist = readFileSync14(join15(root, "ios", "App", "Info.plist"), "utf8");
49153
+ const plist = readFileSync15(join17(root, "ios", "App", "Info.plist"), "utf8");
48609
49154
  check(plist.includes("<key>LeCodesUpdateURL</key>"), "OTA update URL in Info.plist", "run `lecodes app sync`", false);
49155
+ const appDir = join17(root, "ios", "App");
49156
+ const callsUpdater = readdirSync7(appDir).some((f3) => f3.endsWith(".swift") && readFileSync15(join17(appDir, f3), "utf8").includes("LeCodesUpdater.activeBundle"));
49157
+ 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);
48610
49158
  }
48611
- const asc = readJson(join15(homedir5(), ".lecodes", "asc.json"));
49159
+ const asc = readJson(join17(homedir6(), ".lecodes", "asc.json"));
48612
49160
  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);
48613
49161
  };
48614
49162
  var ARCHIVE_PATH = "ios/build/App.xcarchive";
@@ -48616,8 +49164,10 @@ var appBuild = async (args) => {
48616
49164
  const platform = args._[1];
48617
49165
  if (platform === "android")
48618
49166
  return buildAndroid(args);
49167
+ if (platform === "desktop")
49168
+ return buildDesktop(args);
48619
49169
  if (platform !== undefined && platform !== "ios") {
48620
- throw new CliError(`Unknown platform "${platform}" — use ios or android.`);
49170
+ throw new CliError(`Unknown platform "${platform}" — use ios, android or desktop.`);
48621
49171
  }
48622
49172
  const { root, config } = requireShell();
48623
49173
  const configuration = flagStr(args, "configuration") ?? "Release";
@@ -48642,8 +49192,8 @@ var appBuild = async (args) => {
48642
49192
  success(`Archive: ios/build/App.xcarchive`);
48643
49193
  if (flagBool(args, "no-export"))
48644
49194
  return;
48645
- const optionsPath = join15(root, "ios", "build", "ExportOptions.plist");
48646
- writeFileSync11(optionsPath, exportOptionsPlist(method, config.ios.teamId ?? ""));
49195
+ const optionsPath = join17(root, "ios", "build", "ExportOptions.plist");
49196
+ writeFileSync13(optionsPath, exportOptionsPlist(method, config.ios.teamId ?? ""));
48647
49197
  note(`Exporting (.ipa, method ${method})…`);
48648
49198
  runTool(root, "xcodebuild", [
48649
49199
  "-exportArchive",
@@ -48662,10 +49212,10 @@ var appUpload = async (args) => {
48662
49212
  throw new CliError("Play upload isn't wired yet — upload android/app/build/outputs/bundle/release/*.aab in the Play Console (`lecodes app build android` produces it).");
48663
49213
  }
48664
49214
  const { root, config } = requireShell();
48665
- if (!existsSync14(join15(root, ARCHIVE_PATH))) {
49215
+ if (!existsSync16(join17(root, ARCHIVE_PATH))) {
48666
49216
  throw new CliError("No archive at ios/build/App.xcarchive — run `lecodes app build ios` first.");
48667
49217
  }
48668
- const ascPath = join15(homedir5(), ".lecodes", "asc.json");
49218
+ const ascPath = join17(homedir6(), ".lecodes", "asc.json");
48669
49219
  const stored = readJson(ascPath) ?? {};
48670
49220
  const keyPath = flagStr(args, "key-path") ?? stored.keyPath;
48671
49221
  const keyId = flagStr(args, "key-id") ?? stored.keyId;
@@ -48674,15 +49224,15 @@ var appUpload = async (args) => {
48674
49224
  throw new CliError(`App Store Connect API key needed: --key-path <AuthKey.p8> --key-id <id> --issuer-id <id>
48675
49225
  (create one at appstoreconnect.apple.com → Users and Access → Integrations; stored in ~/.lecodes/asc.json after first use)`);
48676
49226
  }
48677
- if (!existsSync14(keyPath))
49227
+ if (!existsSync16(keyPath))
48678
49228
  throw new CliError(`API key file not found: ${keyPath}`);
48679
49229
  if (flagStr(args, "key-path")) {
48680
- mkdirSync9(dirname11(ascPath), { recursive: true });
49230
+ mkdirSync11(dirname12(ascPath), { recursive: true });
48681
49231
  writeJson(ascPath, { keyPath, keyId, issuerId });
48682
49232
  note("Stored the key reference in ~/.lecodes/asc.json");
48683
49233
  }
48684
- const optionsPath = join15(root, "ios", "build", "UploadOptions.plist");
48685
- writeFileSync11(optionsPath, exportOptionsPlist("app-store-connect", config.ios.teamId ?? "", "upload"));
49234
+ const optionsPath = join17(root, "ios", "build", "UploadOptions.plist");
49235
+ writeFileSync13(optionsPath, exportOptionsPlist("app-store-connect", config.ios.teamId ?? "", "upload"));
48686
49236
  note("Uploading to App Store Connect… (processing continues on Apple's side)");
48687
49237
  runTool(root, "xcodebuild", [
48688
49238
  "-exportArchive",
@@ -48693,14 +49243,14 @@ var appUpload = async (args) => {
48693
49243
  "-exportPath",
48694
49244
  "ios/build/upload",
48695
49245
  "-authenticationKeyPath",
48696
- resolve4(keyPath),
49246
+ resolve6(keyPath),
48697
49247
  "-authenticationKeyID",
48698
49248
  keyId,
48699
49249
  "-authenticationKeyIssuerID",
48700
49250
  issuerId,
48701
49251
  "-allowProvisioningUpdates"
48702
49252
  ]);
48703
- const configPath = join15(root, "app.json");
49253
+ const configPath = join17(root, "app.json");
48704
49254
  const fresh = readJson(configPath);
48705
49255
  fresh.ios.buildNumber = (fresh.ios.buildNumber ?? 1) + 1;
48706
49256
  writeJson(configPath, fresh);
@@ -48711,19 +49261,19 @@ var appOpen = async (args) => {
48711
49261
  const root = projectRoot();
48712
49262
  let platform = args._[1];
48713
49263
  if (!platform) {
48714
- const hasIos = existsSync14(join15(root, "ios"));
48715
- const hasAndroid = existsSync14(join15(root, "android"));
49264
+ const hasIos = existsSync16(join17(root, "ios"));
49265
+ const hasAndroid = existsSync16(join17(root, "android"));
48716
49266
  if (hasIos && hasAndroid)
48717
49267
  throw new CliError("Both shells exist — `lecodes app open ios` or `lecodes app open android`.");
48718
49268
  platform = hasAndroid ? "android" : "ios";
48719
49269
  }
48720
49270
  if (platform === "ios") {
48721
- const proj = join15(root, "ios", "App.xcodeproj");
48722
- if (!existsSync14(proj))
49271
+ const proj = join17(root, "ios", "App.xcodeproj");
49272
+ if (!existsSync16(proj))
48723
49273
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
48724
49274
  if (process.platform !== "darwin")
48725
49275
  throw new CliError("Opening Xcode requires macOS.");
48726
- execFileSync3("open", [proj], { stdio: "ignore" });
49276
+ execFileSync4("open", [proj], { stdio: "ignore" });
48727
49277
  success("Opened ios/App.xcodeproj in Xcode.");
48728
49278
  return;
48729
49279
  }
@@ -48749,15 +49299,15 @@ var app = async (args) => {
48749
49299
  return appUpload(args);
48750
49300
  if (sub === "open")
48751
49301
  return appOpen(args);
48752
- throw new CliError("Usage: lecodes app <init ios|android | sync | update | open | plugin list|add|remove | doctor | build ios|android | upload ios> — see `lecodes help`.");
49302
+ throw new CliError("Usage: lecodes app <init ios|android|desktop | sync | update | open | plugin list|add|remove | doctor | build ios|android|desktop | upload ios> — see `lecodes help`.");
48753
49303
  };
48754
49304
 
48755
49305
  // src/commands/clone.ts
48756
49306
  init_api();
48757
49307
  init_config();
48758
49308
  init_manifest();
48759
- import { existsSync as existsSync16, mkdirSync as mkdirSync11, readdirSync as readdirSync7 } from "node:fs";
48760
- import { resolve as resolve5 } from "node:path";
49309
+ import { existsSync as existsSync18, mkdirSync as mkdirSync13, readdirSync as readdirSync9 } from "node:fs";
49310
+ import { resolve as resolve7 } from "node:path";
48761
49311
  init_types4();
48762
49312
  init_ignore();
48763
49313
  init_util();
@@ -48778,11 +49328,11 @@ var clone3 = async (args) => {
48778
49328
  const uuid = parseUuid(args._[0]);
48779
49329
  const project = await getProject(apiUrl, token, uuid);
48780
49330
  const dir = args._[1] ?? slug(project.name);
48781
- const root = resolve5(dir);
48782
- if (existsSync16(root) && readdirSync7(root).length > 0) {
49331
+ const root = resolve7(dir);
49332
+ if (existsSync18(root) && readdirSync9(root).length > 0) {
48783
49333
  throw new CliError(`Directory "${dir}" already exists and is not empty.`);
48784
49334
  }
48785
- mkdirSync11(root, { recursive: true });
49335
+ mkdirSync13(root, { recursive: true });
48786
49336
  info(c.dim(`Fetching project ${c.bold(project.name)}…`));
48787
49337
  const files = await materializeAssets(root, project, apiUrl, token, progress);
48788
49338
  endProgress();
@@ -48804,8 +49354,8 @@ init_config();
48804
49354
  init_manifest();
48805
49355
  init_types4();
48806
49356
  init_ignore();
48807
- import { existsSync as existsSync17, mkdirSync as mkdirSync12, readdirSync as readdirSync8, writeFileSync as writeFileSync13 } from "node:fs";
48808
- import { join as join17, resolve as resolve6 } from "node:path";
49357
+ import { existsSync as existsSync19, mkdirSync as mkdirSync14, readdirSync as readdirSync10, writeFileSync as writeFileSync15 } from "node:fs";
49358
+ import { join as join19, resolve as resolve8 } from "node:path";
48809
49359
 
48810
49360
  // src/commands/projectTemplates.ts
48811
49361
  var appClaudeMd = (name) => `# ${name} — LeCodes app
@@ -48861,11 +49411,28 @@ optional \`expect\` (\`screen\`, \`node\`/\`notNode\`, \`text\`/\`notText\`,
48861
49411
  with \`"fixtures": "fixtures.json"\` in its envelope.
48862
49412
  - \`fill\` fills every empty input — use it before tapping a disabled-until-filled submit.
48863
49413
 
49414
+ ## SDK documentation
49415
+
49416
+ \`.lecodes/types/\` is the exact API surface (signatures only). For semantics and examples,
49417
+ fetch the docs — they are served as raw markdown for AI tools:
49418
+
49419
+ - https://le.codes/llms.txt — the docs map; fetch it first to find the right page
49420
+ - https://le.codes/en/docs/api/<area>/<page>.md — one reference page as raw markdown
49421
+ (e.g. \`ui/styling.md\`, \`2d/scene2d.md\`, \`3d/scene.md\`, \`runtime/storage.md\`)
49422
+ - https://le.codes/en/docs/guide/<page>.md — guide pages (project anatomy, CLI, tracks)
49423
+ - https://le.codes/llms-full.txt — everything in one file (large; prefer per-page)
49424
+
49425
+ Always fetch the \`.md\` URLs — the HTML routes are a JS app and return an empty shell.
49426
+
48864
49427
  ## Other commands
48865
49428
 
48866
49429
  \`lecodes dev\` (run on a device with hot reload + QR), \`lecodes compile -o app.js\`
48867
49430
  (single-file bundle), \`lecodes design\` (screen-design board — the living spec).
48868
49431
  `;
49432
+ var CLAUDE_SETTINGS = JSON.stringify({
49433
+ permissions: { allow: ["WebFetch(domain:le.codes)"] }
49434
+ }, null, 2) + `
49435
+ `;
48869
49436
  var STARTER_FLOW = `{
48870
49437
  "name": "counter",
48871
49438
  "steps": [
@@ -48895,19 +49462,21 @@ var create = async (args) => {
48895
49462
  if (!name)
48896
49463
  throw new CliError("Usage: lecodes create <name> [directory]");
48897
49464
  const dir = args._[1] ?? slug2(name);
48898
- const root = resolve6(dir);
48899
- if (existsSync17(root) && readdirSync8(root).length > 0) {
49465
+ const root = resolve8(dir);
49466
+ if (existsSync19(root) && readdirSync10(root).length > 0) {
48900
49467
  throw new CliError(`Directory "${dir}" already exists and is not empty.`);
48901
49468
  }
48902
- mkdirSync12(root, { recursive: true });
49469
+ mkdirSync14(root, { recursive: true });
48903
49470
  info(c.dim(`Creating project ${c.bold(name)}…`));
48904
49471
  const { uuid } = await createProject(apiUrl, token, name, flagStr(args, "team"));
48905
49472
  info(c.dim("Fetching type definitions…"));
48906
49473
  await materializeTypes(root, apiUrl);
48907
49474
  writeTsconfig(root);
48908
49475
  writeDefaultIgnore(root);
48909
- writeFileSync13(join17(root, "main.ts"), STARTER);
48910
- writeFileSync13(join17(root, "CLAUDE.md"), appClaudeMd(name));
49476
+ writeFileSync15(join19(root, "main.ts"), STARTER);
49477
+ writeFileSync15(join19(root, "CLAUDE.md"), appClaudeMd(name));
49478
+ mkdirSync14(join19(root, ".claude"), { recursive: true });
49479
+ writeFileSync15(join19(root, ".claude", "settings.json"), CLAUDE_SETTINGS);
48911
49480
  const commits = await getCommits(apiUrl, token, uuid);
48912
49481
  writeManifest(root, { uuid, apiUrl, name, baseHeadId: commits.headId, files: {} });
48913
49482
  success(`Created ${c.bold(name)} in ${c.bold(dir)}/`);
@@ -48917,260 +49486,18 @@ var create = async (args) => {
48917
49486
  // src/commands/desktop.ts
48918
49487
  init_util();
48919
49488
  init_manifest();
48920
-
48921
- // src/desktopRenderer.ts
48922
- init_util();
48923
- import { createHash as createHash5 } from "node:crypto";
48924
- import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
48925
- import { copyFileSync, existsSync as existsSync18, mkdirSync as mkdirSync13, mkdtempSync, readdirSync as readdirSync9, readFileSync as readFileSync16, renameSync as renameSync2, rmSync as rmSync4, statSync as statSync5, writeFileSync as writeFileSync14 } from "node:fs";
48926
- import { homedir as homedir6, tmpdir } from "node:os";
48927
- import { join as join18, resolve as resolve7 } from "node:path";
48928
- var MANIFEST_URL = "https://storage.yandexcloud.net/le-codes/releases/versions.json";
48929
- var DESKTOP_RENDERER_VERSION = "1.1.0";
48930
- var semverNewer = (a2, b) => {
48931
- const pa = a2.split(/[.-]/).map(Number), pb = b.split(/[.-]/).map(Number);
48932
- for (let i2 = 0;i2 < 3; i2++)
48933
- if ((pb[i2] ?? 0) !== (pa[i2] ?? 0))
48934
- return (pb[i2] ?? 0) - (pa[i2] ?? 0);
48935
- return 0;
48936
- };
48937
- var platformKey = () => {
48938
- if (process.platform === "win32")
48939
- return "windows-x64";
48940
- if (process.platform === "linux")
48941
- return "ubuntu-linux-x64";
48942
- throw new CliError(process.platform === "darwin" ? "`lecodes render --desktop` isn't available on macOS yet (the macOS host ships separately)." : `\`lecodes render --desktop\` isn't available on ${process.platform}.`);
48943
- };
48944
- var exeName = () => process.platform === "win32" ? "lecodes-desktop.exe" : "lecodes-desktop";
48945
- var findExe = (dir) => {
48946
- const name = exeName();
48947
- for (const entry of readdirSync9(dir, { withFileTypes: true })) {
48948
- const p2 = join18(dir, entry.name);
48949
- if (entry.isFile() && entry.name === name)
48950
- return p2;
48951
- if (entry.isDirectory()) {
48952
- const found = findExe(p2);
48953
- if (found)
48954
- return found;
48955
- }
48956
- }
48957
- return null;
48958
- };
48959
- var cacheRoot = () => join18(homedir6(), ".lecodes", "renderer");
48960
- var currentFile = () => join18(cacheRoot(), "current.json");
48961
- var installedDesktopVersions = () => {
48962
- try {
48963
- return readdirSync9(cacheRoot(), { withFileTypes: true }).filter((e2) => e2.isDirectory() && e2.name.startsWith("desktop-")).map((e2) => e2.name.slice("desktop-".length)).filter((v) => findExe(join18(cacheRoot(), `desktop-${v}`)) !== null).sort(semverNewer);
48964
- } catch {
48965
- return [];
48966
- }
48967
- };
48968
- var selectedDesktopVersion = () => {
48969
- try {
48970
- const saved = JSON.parse(readFileSync16(currentFile(), "utf8")).version;
48971
- if (saved && /^\d+\.\d+\.\d+/.test(saved) && semverNewer(DESKTOP_RENDERER_VERSION, saved) > 0)
48972
- return saved;
48973
- } catch {}
48974
- return DESKTOP_RENDERER_VERSION;
48975
- };
48976
- var fetchDesktopManifest = async () => {
48977
- let manifest;
48978
- try {
48979
- const res = await fetch(MANIFEST_URL);
48980
- if (!res.ok)
48981
- throw new Error(`HTTP ${res.status}`);
48982
- manifest = await res.json();
48983
- } catch (e2) {
48984
- throw new CliError(`Can't fetch the release manifest (${MANIFEST_URL}): ${e2 instanceof Error ? e2.message : String(e2)}`);
48985
- }
48986
- const desktop = manifest.products?.desktop;
48987
- if (!desktop)
48988
- throw new CliError("The release manifest has no desktop product.");
48989
- return desktop;
48990
- };
48991
- var downloadDesktop = async (version, dest) => {
48992
- const key = platformKey();
48993
- const entry = (await fetchDesktopManifest()).versions.find((v) => v.version === version);
48994
- const file = entry?.files[key];
48995
- if (!file) {
48996
- throw new CliError(`LeCodes Desktop ${version} [${key}] isn't published yet — \`--desktop\` needs it for the ` + `headless render contract.
48997
- Set LECODES_DESKTOP_EXE to a locally built host, or update lecodes-cli.`);
48998
- }
48999
- note(`Downloading the native renderer (LeCodes Desktop ${version}, ${(file.size / 1024 / 1024).toFixed(1)} MB)…`);
49000
- let bytes;
49001
- try {
49002
- const res = await fetch(file.url);
49003
- if (!res.ok)
49004
- throw new Error(`HTTP ${res.status}`);
49005
- bytes = Buffer.from(await res.arrayBuffer());
49006
- } catch (e2) {
49007
- throw new CliError(`Download failed (${file.url}): ${e2 instanceof Error ? e2.message : String(e2)}`);
49008
- }
49009
- const sha = createHash5("sha256").update(bytes).digest("hex");
49010
- if (sha !== file.sha256)
49011
- throw new CliError(`Downloaded archive is corrupt (sha256 mismatch) — try again.`);
49012
- mkdirSync13(cacheRoot(), { recursive: true });
49013
- const stage = mkdtempSync(join18(cacheRoot(), "dl-"));
49014
- try {
49015
- const archive = join18(stage, file.url.split("/").pop() ?? "archive");
49016
- writeFileSync14(archive, bytes);
49017
- const tar = spawnSync3("tar", ["-xf", archive, "-C", stage], { stdio: ["ignore", "ignore", "pipe"] });
49018
- if (tar.status !== 0)
49019
- throw new CliError(`Couldn't extract the renderer archive: ${tar.stderr?.toString() ?? "tar failed"}`);
49020
- rmSync4(archive);
49021
- if (!findExe(stage))
49022
- throw new CliError("The downloaded archive doesn't contain the desktop host binary.");
49023
- try {
49024
- renameSync2(stage, dest);
49025
- } catch {
49026
- if (!findExe(dest))
49027
- throw new CliError(`Couldn't move the renderer into ${dest}.`);
49028
- }
49029
- } finally {
49030
- rmSync4(stage, { recursive: true, force: true });
49031
- }
49032
- };
49033
- var resolveDesktopExe = async () => {
49034
- const override = process.env.LECODES_DESKTOP_EXE;
49035
- if (override) {
49036
- if (!existsSync18(override))
49037
- throw new CliError(`LECODES_DESKTOP_EXE points at "${override}" which doesn't exist.`);
49038
- return resolve7(override);
49039
- }
49040
- const version = selectedDesktopVersion();
49041
- const dir = join18(cacheRoot(), `desktop-${version}`);
49042
- let exe = existsSync18(dir) ? findExe(dir) : null;
49043
- if (!exe) {
49044
- await downloadDesktop(version, dir);
49045
- exe = findExe(dir);
49046
- if (!exe)
49047
- throw new CliError(`Renderer cache at ${dir} is broken — delete the folder and retry.`);
49048
- }
49049
- return exe;
49050
- };
49051
- var updateDesktop = async () => {
49052
- const { latest } = await fetchDesktopManifest();
49053
- if (semverNewer(latest, DESKTOP_RENDERER_VERSION) > 0) {
49054
- throw new CliError(`The newest published LeCodes Desktop is ${latest}, but this CLI needs ≥ ${DESKTOP_RENDERER_VERSION} ` + `for the render contract. Publish the desktop release first.`);
49055
- }
49056
- const before = selectedDesktopVersion();
49057
- const dir = join18(cacheRoot(), `desktop-${latest}`);
49058
- const hadIt = existsSync18(dir) && findExe(dir) !== null;
49059
- if (!hadIt)
49060
- await downloadDesktop(latest, dir);
49061
- mkdirSync13(cacheRoot(), { recursive: true });
49062
- writeFileSync14(currentFile(), JSON.stringify({ version: latest }, null, 2));
49063
- if (latest === before && hadIt)
49064
- note(`Already up to date: LeCodes Desktop ${latest}.`);
49065
- else if (latest === before)
49066
- note(`Re-downloaded LeCodes Desktop ${latest}.`);
49067
- else
49068
- note(`Desktop renderer set to ${latest} (was ${before}).`);
49069
- };
49070
- var windowEnv = (opts) => {
49071
- const env = {};
49072
- const width = opts.width ?? (opts.height !== undefined ? 1280 : undefined);
49073
- const height = opts.height ?? (opts.width !== undefined ? 720 : undefined);
49074
- if (width && height)
49075
- env.CREATOR_WINDOW_SIZE = `${width}x${height}`;
49076
- if (opts.minWidth && opts.minHeight)
49077
- env.CREATOR_MIN_WINDOW_SIZE = `${opts.minWidth}x${opts.minHeight}`;
49078
- if (opts.title)
49079
- env.CREATOR_WINDOW_TITLE = opts.title;
49080
- if (opts.resizable === false)
49081
- env.CREATOR_WINDOW_FIXED = "1";
49082
- return env;
49083
- };
49084
- var stageBundle = (js, resources) => {
49085
- const runDir = mkdtempSync(join18(tmpdir(), "lecodes-render-"));
49086
- writeFileSync14(join18(runDir, "app.js"), js);
49087
- for (const r2 of resources) {
49088
- if (r2.absPath && existsSync18(r2.absPath) && statSync5(r2.absPath).isFile()) {
49089
- copyFileSync(r2.absPath, join18(runDir, r2.name));
49090
- }
49091
- }
49092
- return runDir;
49093
- };
49094
- var runDesktopApp = async (js, resources, opts = {}) => {
49095
- const exe = await resolveDesktopExe();
49096
- const runDir = stageBundle(js, resources);
49097
- try {
49098
- const env = { ...process.env, ...windowEnv(opts) };
49099
- await new Promise((done, fail) => {
49100
- const child = spawn2(exe, [join18(runDir, "app.js")], { cwd: runDir, env, stdio: ["ignore", "inherit", "inherit"] });
49101
- child.on("error", (e2) => fail(new CliError(`Can't run the desktop host: ${e2.message}`)));
49102
- child.on("exit", (code) => {
49103
- if (code === 0 || code === null)
49104
- done();
49105
- else
49106
- fail(new CliError(`The desktop host exited with code ${code}.`));
49107
- });
49108
- });
49109
- } finally {
49110
- rmSync4(runDir, { recursive: true, force: true });
49111
- }
49112
- };
49113
- var runDesktopDev = async (url, opts = {}) => {
49114
- const exe = await resolveDesktopExe();
49115
- const env = { ...process.env, ...windowEnv(opts) };
49116
- await new Promise((done, fail) => {
49117
- const child = spawn2(exe, [url], { env, stdio: ["ignore", "ignore", "inherit"] });
49118
- child.on("error", (e2) => fail(new CliError(`Can't run the desktop host: ${e2.message}`)));
49119
- child.on("exit", () => done());
49120
- });
49121
- };
49122
- var runDesktopRender = async (js, resources, opts) => {
49123
- const exe = await resolveDesktopExe();
49124
- const runDir = stageBundle(js, resources);
49125
- try {
49126
- const appJs = join18(runDir, "app.js");
49127
- const outAbs = resolve7(opts.outPng);
49128
- await new Promise((done, fail) => {
49129
- const child = spawn2(exe, [appJs], {
49130
- cwd: runDir,
49131
- env: {
49132
- ...process.env,
49133
- CREATOR_DUMP_PNG: outAbs,
49134
- CREATOR_MAX_FRAMES: String(opts.frames),
49135
- CREATOR_WINDOW_SIZE: `${opts.width}x${opts.height}`,
49136
- CREATOR_HIDDEN: "1"
49137
- },
49138
- stdio: ["ignore", "pipe", "pipe"]
49139
- });
49140
- const tail = [];
49141
- const sink = (d) => {
49142
- const text = String(d);
49143
- tail.push(text);
49144
- while (tail.length > 80)
49145
- tail.shift();
49146
- if (opts.logs)
49147
- process.stderr.write(text);
49148
- };
49149
- child.stdout.on("data", sink);
49150
- child.stderr.on("data", sink);
49151
- const killer = setTimeout(() => {
49152
- child.kill();
49153
- fail(new CliError(`The native render timed out after ${opts.timeoutMs}ms (raise --timeout, or lower --frames).`));
49154
- }, opts.timeoutMs);
49155
- child.on("error", (e2) => {
49156
- clearTimeout(killer);
49157
- fail(new CliError(`Can't run the desktop renderer: ${e2.message}`));
49158
- });
49159
- child.on("exit", (code) => {
49160
- clearTimeout(killer);
49161
- if (code === 0 && existsSync18(outAbs))
49162
- return done();
49163
- const detail = tail.length > 0 ? `
49164
- ${tail.join("").trimEnd()}` : "";
49165
- fail(new CliError(code === 0 ? `The renderer exited cleanly but produced no PNG — did the app open a screen?${detail}` : `The renderer exited with code ${code}.${detail}`));
49166
- });
49167
- });
49168
- } finally {
49169
- rmSync4(runDir, { recursive: true, force: true });
49489
+ var resolveScale = (flag, cfg) => {
49490
+ const raw = flag ?? cfg;
49491
+ if (raw === undefined || raw === "system")
49492
+ return;
49493
+ if (raw === "fit")
49494
+ return "fit";
49495
+ const n2 = Number(raw);
49496
+ if (!Number.isFinite(n2) || n2 < 0.5 || n2 > 4) {
49497
+ throw new CliError(`scale must be a number 0.5..4, "fit" or "system" (got "${raw}")`);
49170
49498
  }
49499
+ return n2;
49171
49500
  };
49172
-
49173
- // src/commands/desktop.ts
49174
49501
  var run = async (args) => {
49175
49502
  let root;
49176
49503
  try {
@@ -49188,7 +49515,9 @@ var run = async (args) => {
49188
49515
  minWidth: win?.minWidth,
49189
49516
  minHeight: win?.minHeight,
49190
49517
  resizable: win?.resizable,
49191
- title: flagStr(args, "title") ?? win?.title ?? appCfg?.name
49518
+ title: flagStr(args, "title") ?? win?.title ?? appCfg?.name,
49519
+ fullscreen: flagBool(args, "fullscreen") ? true : flagBool(args, "windowed") ? false : win?.fullscreen,
49520
+ scale: resolveScale(flagStr(args, "scale"), win?.scale)
49192
49521
  });
49193
49522
  };
49194
49523
  var status = async () => {
@@ -49224,15 +49553,15 @@ init_design();
49224
49553
  // src/commands/dev.ts
49225
49554
  init_manifest();
49226
49555
  init_ignore();
49227
- import { existsSync as existsSync20, mkdirSync as mkdirSync15, readFileSync as readFileSync19, watch, writeFileSync as writeFileSync16 } from "node:fs";
49556
+ import { existsSync as existsSync21, mkdirSync as mkdirSync16, readFileSync as readFileSync19, watch, writeFileSync as writeFileSync17 } from "node:fs";
49228
49557
  import { randomBytes as randomBytes2 } from "node:crypto";
49229
49558
  import { networkInterfaces } from "node:os";
49230
- import { basename as basename8, join as join20 } from "node:path";
49559
+ import { basename as basename9, join as join21 } from "node:path";
49231
49560
 
49232
49561
  // src/dev/devServer.ts
49233
49562
  init_localFiles();
49234
49563
  import { createServer as createServer2 } from "node:http";
49235
- import { createReadStream, statSync as statSync6 } from "node:fs";
49564
+ import { createReadStream, statSync as statSync7 } from "node:fs";
49236
49565
  init_projectCompile();
49237
49566
  init_fonts();
49238
49567
  init_assetIcons();
@@ -49500,14 +49829,14 @@ var sendText = (res, status2, type, body) => {
49500
49829
  res.end(body);
49501
49830
  };
49502
49831
  var sendJson = (res, status2, body) => sendText(res, status2, "application/json", JSON.stringify(body));
49503
- var readBody = (req) => new Promise((resolve8) => {
49832
+ var readBody = (req) => new Promise((resolve9) => {
49504
49833
  let body = "";
49505
49834
  req.on("data", (c4) => {
49506
49835
  if (body.length < 256 * 1024)
49507
49836
  body += c4.toString("utf8");
49508
49837
  });
49509
- req.on("end", () => resolve8(body));
49510
- req.on("error", () => resolve8(body));
49838
+ req.on("end", () => resolve9(body));
49839
+ req.on("error", () => resolve9(body));
49511
49840
  });
49512
49841
  var startDevServer = async (options) => {
49513
49842
  const { root, manifest, name, origin, token, entryOverride } = options;
@@ -49607,7 +49936,7 @@ var startDevServer = async (options) => {
49607
49936
  return sendText(res, 400, "text/plain", "Bad path");
49608
49937
  }
49609
49938
  const abs = resolveLocalPath(root, key);
49610
- if (!abs || !statSync6(abs).isFile())
49939
+ if (!abs || !statSync7(abs).isFile())
49611
49940
  return sendText(res, 404, "text/plain", "Not found");
49612
49941
  res.writeHead(200, { "content-type": contentType(key), "cache-control": "no-store" });
49613
49942
  createReadStream(abs).pipe(res);
@@ -49650,11 +49979,11 @@ var startDevServer = async (options) => {
49650
49979
  if (conn)
49651
49980
  clients.add(conn);
49652
49981
  });
49653
- await new Promise((resolve8, reject) => {
49982
+ await new Promise((resolve9, reject) => {
49654
49983
  server.once("error", reject);
49655
49984
  server.listen(options.port, options.bind ?? "0.0.0.0", () => {
49656
49985
  server.removeListener("error", reject);
49657
- resolve8();
49986
+ resolve9();
49658
49987
  });
49659
49988
  });
49660
49989
  const address = server.address();
@@ -49702,24 +50031,24 @@ var lanCandidates = () => {
49702
50031
  return out.sort((x2, y2) => x2.rank - y2.rank).map(({ address, iface }) => ({ address, iface }));
49703
50032
  };
49704
50033
  var loadDevToken = (root) => {
49705
- const path2 = join20(root, LECODES_DIR, "dev.json");
50034
+ const path2 = join21(root, LECODES_DIR, "dev.json");
49706
50035
  try {
49707
50036
  const token2 = JSON.parse(readFileSync19(path2, "utf8")).token;
49708
50037
  if (token2 && /^[a-z0-9]{8,}$/.test(token2))
49709
50038
  return token2;
49710
50039
  } catch {}
49711
50040
  const token = randomBytes2(6).toString("hex");
49712
- mkdirSync15(join20(root, LECODES_DIR), { recursive: true });
49713
- writeFileSync16(path2, JSON.stringify({ token }, null, 2) + `
50041
+ mkdirSync16(join21(root, LECODES_DIR), { recursive: true });
50042
+ writeFileSync17(path2, JSON.stringify({ token }, null, 2) + `
49714
50043
  `);
49715
50044
  return token;
49716
50045
  };
49717
50046
  var printQr = async (url) => {
49718
50047
  try {
49719
50048
  const qrcode = (await Promise.resolve().then(() => __toESM(require_main(), 1))).default;
49720
- await new Promise((resolve8) => qrcode.generate(url, { small: true }, (out) => {
50049
+ await new Promise((resolve9) => qrcode.generate(url, { small: true }, (out) => {
49721
50050
  log(out);
49722
- resolve8();
50051
+ resolve9();
49723
50052
  }));
49724
50053
  } catch {
49725
50054
  note("(couldn't render a QR code — open the URL below on the device)");
@@ -49735,7 +50064,7 @@ var dev = async (args) => {
49735
50064
  root = process.cwd();
49736
50065
  manifest = null;
49737
50066
  }
49738
- const name = manifest?.name ?? basename8(root);
50067
+ const name = manifest?.name ?? basename9(root);
49739
50068
  if (typeof globalThis.Bun !== "undefined") {
49740
50069
  warnErr("Running under Bun: the websocket reload channel doesn't work here — run `lecodes dev` with Node (or use ?t=poll on the device URL).");
49741
50070
  }
@@ -49827,7 +50156,7 @@ ${result.error}`);
49827
50156
  if (alternatives.length > 0) {
49828
50157
  note(`Device can't reach it? This machine also has ${alternatives.map((a2) => `${a2.address} (${a2.iface})`).join(", ")} — retry with --host <ip>.`);
49829
50158
  }
49830
- if (existsSync20(join20(root, LECODES_DIR, "manifest.json")))
50159
+ if (existsSync21(join21(root, LECODES_DIR, "manifest.json")))
49831
50160
  note(`Token stored in ${LECODES_DIR}/dev.json (delete it to rotate the URL).`);
49832
50161
  log("");
49833
50162
  log(`Watching ${root} — save a file to hot-reload. Ctrl+C to stop.`);
@@ -49841,7 +50170,8 @@ ${result.error}`);
49841
50170
  minWidth: win?.minWidth,
49842
50171
  minHeight: win?.minHeight,
49843
50172
  resizable: win?.resizable,
49844
- title: win?.title ?? appCfg?.name ?? name
50173
+ title: win?.title ?? appCfg?.name ?? name,
50174
+ scale: resolveScale(undefined, win?.scale)
49845
50175
  }).then(() => note("Desktop window closed — the dev server keeps running (Ctrl+C to stop)."), (e2) => warnErr(`Desktop host: ${e2 instanceof Error ? e2.message : String(e2)}`));
49846
50176
  }
49847
50177
  };
@@ -49855,7 +50185,7 @@ init_localFiles();
49855
50185
  // src/serverDiff.ts
49856
50186
  init_localFiles();
49857
50187
  import { readFileSync as readFileSync20 } from "node:fs";
49858
- import { basename as basename9 } from "node:path";
50188
+ import { basename as basename10 } from "node:path";
49859
50189
  var isTextAsset = (t2) => t2 === "text" || t2 === "shader" || t2 === "scene";
49860
50190
  var computeServerDiff = (project, scan2) => {
49861
50191
  const pathById = buildPaths(project.assets);
@@ -49872,7 +50202,7 @@ var computeServerDiff = (project, scan2) => {
49872
50202
  const server = serverByPath.get(path2);
49873
50203
  const local = localByPath.get(path2);
49874
50204
  if (local && !server) {
49875
- const text = isTextKind(classifyKind(basename9(path2)));
50205
+ const text = isTextKind(classifyKind(basename10(path2)));
49876
50206
  entries.push({ path: path2, status: "added", binary: !text, serverText: "", localText: text ? readFileSync20(local.absPath, "utf8") : undefined });
49877
50207
  continue;
49878
50208
  }
@@ -50047,8 +50377,8 @@ var statusLabel = (e2) => e2.status === "added" ? c.green(" added ") : e2.st
50047
50377
  init_manifest();
50048
50378
  init_types4();
50049
50379
  init_ignore();
50050
- import { existsSync as existsSync21, mkdirSync as mkdirSync16, writeFileSync as writeFileSync17 } from "node:fs";
50051
- import { basename as basename10, dirname as dirname13, join as join21, resolve as resolve8 } from "node:path";
50380
+ import { existsSync as existsSync22, mkdirSync as mkdirSync17, writeFileSync as writeFileSync18 } from "node:fs";
50381
+ import { basename as basename11, dirname as dirname14, join as join22, resolve as resolve9 } from "node:path";
50052
50382
  init_util();
50053
50383
  var STARTER2 = `// Welcome to LeCodes — the whole SDK is available as globals, no imports needed.
50054
50384
  // Run it live on a device with \`lecodes dev\`, or bundle it with \`lecodes compile -o app.js\`.
@@ -50153,18 +50483,18 @@ Full command list: \`lecodes --help\`.
50153
50483
  `;
50154
50484
  var init2 = async (args) => {
50155
50485
  const dirArg = (args._[0] ?? ".").replace(/[/\\]+$/, "") || ".";
50156
- const root = resolve8(dirArg);
50157
- const name = basename10(root);
50158
- mkdirSync16(root, { recursive: true });
50486
+ const root = resolve9(dirArg);
50487
+ const name = basename11(root);
50488
+ mkdirSync17(root, { recursive: true });
50159
50489
  let created = 0;
50160
50490
  const write = (rel, content) => {
50161
- const abs = join21(root, rel);
50162
- if (existsSync21(abs)) {
50491
+ const abs = join22(root, rel);
50492
+ if (existsSync22(abs)) {
50163
50493
  note(`kept ${rel}`);
50164
50494
  return;
50165
50495
  }
50166
- mkdirSync16(dirname13(abs), { recursive: true });
50167
- writeFileSync17(abs, content);
50496
+ mkdirSync17(dirname14(abs), { recursive: true });
50497
+ writeFileSync18(abs, content);
50168
50498
  log(`${c.green("created")} ${rel}`);
50169
50499
  created++;
50170
50500
  };
@@ -50174,6 +50504,7 @@ var init2 = async (args) => {
50174
50504
  if (forDesktop)
50175
50505
  write("app.json", appJsonDesktop(name));
50176
50506
  write("CLAUDE.md", appClaudeMd(name));
50507
+ write(".claude/settings.json", CLAUDE_SETTINGS);
50177
50508
  write("tests/counter.flow.json", STARTER_FLOW);
50178
50509
  write(".lecodesignore", DEFAULT_IGNORE_FILE);
50179
50510
  const wroteTypes = materializeTypesLocal(root);
@@ -50202,8 +50533,8 @@ init_config();
50202
50533
  init_ignore();
50203
50534
  init_localFiles();
50204
50535
  init_manifest();
50205
- import { existsSync as existsSync22 } from "node:fs";
50206
- import { basename as basename12, join as join22, resolve as resolve9 } from "node:path";
50536
+ import { existsSync as existsSync23 } from "node:fs";
50537
+ import { basename as basename13, join as join23, resolve as resolve10 } from "node:path";
50207
50538
  init_types4();
50208
50539
  init_util();
50209
50540
 
@@ -50212,7 +50543,7 @@ init_api();
50212
50543
  init_config();
50213
50544
  init_manifest();
50214
50545
  init_localFiles();
50215
- import { basename as basename11 } from "node:path";
50546
+ import { basename as basename12 } from "node:path";
50216
50547
  import { readFileSync as readFileSync21 } from "node:fs";
50217
50548
  init_util();
50218
50549
  var push = async (args) => {
@@ -50269,14 +50600,14 @@ var push = async (args) => {
50269
50600
  let uploaded = 0;
50270
50601
  for (const file of scan2.files) {
50271
50602
  const source = sourceFor(file.path);
50272
- const kind = source?.type ?? classifyKind(basename11(file.path));
50603
+ const kind = source?.type ?? classifyKind(basename12(file.path));
50273
50604
  const entry = { path: file.path, type: kind };
50274
50605
  if (source)
50275
50606
  entry.assetId = source.id;
50276
50607
  if (isTextKind(kind)) {
50277
50608
  entry.text = readFileSync21(file.absPath, "utf8");
50278
50609
  } else if (!source || source.sha !== file.sha) {
50279
- const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename11(file.path), readFileSync21(file.absPath));
50610
+ const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename12(file.path), readFileSync21(file.absPath));
50280
50611
  entry.blobFileId = blob.id;
50281
50612
  uploaded++;
50282
50613
  }
@@ -50383,10 +50714,10 @@ var link = async (args) => {
50383
50714
  const apiUrl = requireApiUrl(config);
50384
50715
  const token = requireToken(config);
50385
50716
  const dirArg = args._[0];
50386
- const root = dirArg ? resolve9(dirArg) : process.cwd();
50387
- if (!existsSync22(root))
50717
+ const root = dirArg ? resolve10(dirArg) : process.cwd();
50718
+ if (!existsSync23(root))
50388
50719
  throw new CliError(`No such directory: ${root}`);
50389
- if (existsSync22(join22(root, LECODES_DIR, "manifest.json")) && !flagBool(args, "force")) {
50720
+ if (existsSync23(join23(root, LECODES_DIR, "manifest.json")) && !flagBool(args, "force")) {
50390
50721
  throw new CliError("This folder is already linked (.lecodes/manifest.json exists) — use `lecodes push`, or `lecodes link --force` to re-link it to another project.");
50391
50722
  }
50392
50723
  let enclosing = null;
@@ -50413,7 +50744,7 @@ var link = async (args) => {
50413
50744
  }
50414
50745
  name = project.name;
50415
50746
  } else {
50416
- name = flagStr(args, "name") ?? basename12(root);
50747
+ name = flagStr(args, "name") ?? basename13(root);
50417
50748
  info(c.dim(`Creating project ${c.bold(name)}…`));
50418
50749
  uuid = (await createProject(apiUrl, token, name, flagStr(args, "team"))).uuid;
50419
50750
  files = {};
@@ -50462,8 +50793,8 @@ var login = async (args) => {
50462
50793
  };
50463
50794
 
50464
50795
  // src/commands/render.ts
50465
- import { mkdirSync as mkdirSync17, readFileSync as readFileSync22, writeFileSync as writeFileSync18 } from "node:fs";
50466
- import { dirname as dirname14, resolve as resolve10 } from "node:path";
50796
+ import { mkdirSync as mkdirSync18, readFileSync as readFileSync22, writeFileSync as writeFileSync19 } from "node:fs";
50797
+ import { dirname as dirname15, resolve as resolve11 } from "node:path";
50467
50798
 
50468
50799
  // src/compile/headlessBundle.ts
50469
50800
  init_api();
@@ -50574,9 +50905,9 @@ var requireRenderer = async () => {
50574
50905
  };
50575
50906
  var fileScenarioIO = (dir) => ({
50576
50907
  write: (relPath, data) => {
50577
- const abs = resolve10(dir, relPath);
50578
- mkdirSync17(dirname14(abs), { recursive: true });
50579
- writeFileSync18(abs, data);
50908
+ const abs = resolve11(dir, relPath);
50909
+ mkdirSync18(dirname15(abs), { recursive: true });
50910
+ writeFileSync19(abs, data);
50580
50911
  }
50581
50912
  });
50582
50913
  var resolveFixtures = (value, baseDir) => {
@@ -50584,7 +50915,7 @@ var resolveFixtures = (value, baseDir) => {
50584
50915
  return;
50585
50916
  let map = value;
50586
50917
  if (typeof value === "string") {
50587
- const abs = resolve10(baseDir, value);
50918
+ const abs = resolve11(baseDir, value);
50588
50919
  try {
50589
50920
  map = JSON.parse(readFileSync22(abs, "utf8"));
50590
50921
  } catch (e2) {
@@ -50599,7 +50930,7 @@ var resolveFixtures = (value, baseDir) => {
50599
50930
  var readScenarioFile = (path2) => {
50600
50931
  let raw;
50601
50932
  try {
50602
- raw = JSON.parse(readFileSync22(resolve10(path2), "utf8"));
50933
+ raw = JSON.parse(readFileSync22(resolve11(path2), "utf8"));
50603
50934
  } catch (e2) {
50604
50935
  throw new CliError(`Can't read scenario "${path2}": ${e2 instanceof Error ? e2.message : String(e2)}`);
50605
50936
  }
@@ -50657,7 +50988,7 @@ var render = async (args) => {
50657
50988
  const settleStr = flagStr(args, "settle") ?? (scenario?.settle !== undefined ? String(scenario.settle) : undefined);
50658
50989
  const timeStr = flagStr(args, "time");
50659
50990
  const onConsole = flagBool(args, "logs") ? (level, text2) => logErr(`${c.dim(`[${level}]`)} ${text2}`) : undefined;
50660
- const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname14(resolve10(scriptFlag)) : ".");
50991
+ const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname15(resolve11(scriptFlag)) : ".");
50661
50992
  const flagFixtures = resolveFixtures(flagStr(args, "fixtures"), ".");
50662
50993
  const fixtures = envelopeFixtures || flagFixtures ? { ...envelopeFixtures, ...flagFixtures } : undefined;
50663
50994
  if (fixtures) {
@@ -50696,7 +51027,7 @@ var render = async (args) => {
50696
51027
  ${c.red(st.error ?? "failed")}`);
50697
51028
  }
50698
51029
  if (!result2.ok && result2.failureArtifacts.length > 0) {
50699
- logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p2) => resolve10(outDir, p2)).join(", ")}`));
51030
+ logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p2) => resolve11(outDir, p2)).join(", ")}`));
50700
51031
  }
50701
51032
  logErr(result2.ok ? c.green(`✓ ${result2.steps.length}/${steps.length} steps passed`) : c.red(`✗ failed at step ${result2.steps.length} of ${steps.length}`));
50702
51033
  return done((cb) => cb(), result2.ok ? 0 : 1);
@@ -50718,7 +51049,7 @@ var render = async (args) => {
50718
51049
  if (isNullSink(pngFlag)) {
50719
51050
  note(`Discarded PNG (${pngFlag})`);
50720
51051
  } else {
50721
- writeFileSync18(resolve10(pngFlag), png2);
51052
+ writeFileSync19(resolve11(pngFlag), png2);
50722
51053
  note(`Wrote ${pngFlag} (${png2.length} bytes, ${width}x${height})`);
50723
51054
  }
50724
51055
  return done((cb) => cb());
@@ -50733,7 +51064,7 @@ var render = async (args) => {
50733
51064
  return done((cb) => cb());
50734
51065
  }
50735
51066
  if (out) {
50736
- writeFileSync18(resolve10(out), result.text);
51067
+ writeFileSync19(resolve11(out), result.text);
50737
51068
  note(`Wrote ${out} (${result.text.length} bytes)`);
50738
51069
  return done((cb) => cb());
50739
51070
  }
@@ -50748,8 +51079,8 @@ init_api();
50748
51079
  init_config();
50749
51080
  init_manifest();
50750
51081
  init_util();
50751
- import { appendFileSync as appendFileSync2, existsSync as existsSync23, readFileSync as readFileSync23, writeFileSync as writeFileSync19 } from "node:fs";
50752
- import { join as join23 } from "node:path";
51082
+ import { appendFileSync as appendFileSync2, existsSync as existsSync24, readFileSync as readFileSync23, writeFileSync as writeFileSync20 } from "node:fs";
51083
+ import { join as join24 } from "node:path";
50753
51084
  var ENV_VAR = "LECODES_PUSH_KEY";
50754
51085
  var context = () => {
50755
51086
  const root = findProjectRoot(process.cwd());
@@ -50893,11 +51224,11 @@ var pnKeysList = async (ctx) => {
50893
51224
  };
50894
51225
  var writeEnv = (root, key) => {
50895
51226
  const touched = [];
50896
- const envPath = join23(root, ".env");
51227
+ const envPath = join24(root, ".env");
50897
51228
  const line = `${ENV_VAR}=${key}`;
50898
- const existing = existsSync23(envPath) ? readFileSync23(envPath, "utf8") : "";
51229
+ const existing = existsSync24(envPath) ? readFileSync23(envPath, "utf8") : "";
50899
51230
  if (new RegExp(`^${ENV_VAR}=`, "m").test(existing)) {
50900
- writeFileSync19(envPath, existing.replace(new RegExp(`^${ENV_VAR}=.*$`, "m"), line));
51231
+ writeFileSync20(envPath, existing.replace(new RegExp(`^${ENV_VAR}=.*$`, "m"), line));
50901
51232
  } else {
50902
51233
  const separator = !existing || existing.endsWith(`
50903
51234
  `) ? "" : `
@@ -50906,8 +51237,8 @@ var writeEnv = (root, key) => {
50906
51237
  `);
50907
51238
  }
50908
51239
  touched.push(".env");
50909
- const ignorePath = join23(root, ".gitignore");
50910
- const ignore = existsSync23(ignorePath) ? readFileSync23(ignorePath, "utf8") : "";
51240
+ const ignorePath = join24(root, ".gitignore");
51241
+ const ignore = existsSync24(ignorePath) ? readFileSync23(ignorePath, "utf8") : "";
50911
51242
  if (!ignore.split(`
50912
51243
  `).some((l2) => l2.trim() === ".env")) {
50913
51244
  const separator = !ignore || ignore.endsWith(`
@@ -51006,8 +51337,8 @@ var pn = async (args) => {
51006
51337
  init_api();
51007
51338
  init_config();
51008
51339
  init_manifest();
51009
- import { existsSync as existsSync24, mkdirSync as mkdirSync18, rmSync as rmSync5, writeFileSync as writeFileSync20 } from "node:fs";
51010
- import { dirname as dirname15, join as join24 } from "node:path";
51340
+ import { existsSync as existsSync25, mkdirSync as mkdirSync19, rmSync as rmSync6, writeFileSync as writeFileSync21 } from "node:fs";
51341
+ import { dirname as dirname16, join as join25 } from "node:path";
51011
51342
  init_types4();
51012
51343
  init_localFiles();
51013
51344
  init_util();
@@ -51030,9 +51361,9 @@ var pull = async (args) => {
51030
51361
  const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a2) => a2.id === id)?.type !== "folder").map(([, path2]) => path2));
51031
51362
  for (const path2 of Object.keys(manifest.files)) {
51032
51363
  if (!remotePaths.has(path2)) {
51033
- const abs = join24(root, ...path2.replace(/^\//, "").split("/"));
51034
- if (existsSync24(abs))
51035
- rmSync5(abs);
51364
+ const abs = join25(root, ...path2.replace(/^\//, "").split("/"));
51365
+ if (existsSync25(abs))
51366
+ rmSync6(abs);
51036
51367
  }
51037
51368
  }
51038
51369
  const files = await materializeAssets(root, project, apiUrl, token, progress);
@@ -51069,18 +51400,18 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
51069
51400
  const files = { ...manifest.files };
51070
51401
  for (const asset of targets) {
51071
51402
  const key = pathById.get(asset.id);
51072
- const abs = join24(root, ...key.replace(/^\//, "").split("/"));
51073
- mkdirSync18(dirname15(abs), { recursive: true });
51403
+ const abs = join25(root, ...key.replace(/^\//, "").split("/"));
51404
+ mkdirSync19(dirname16(abs), { recursive: true });
51074
51405
  let sha;
51075
51406
  if (asset.type === "file") {
51076
51407
  if (!asset.file?.src)
51077
51408
  continue;
51078
51409
  const bytes = await downloadBlob(apiUrl, asset.file.src);
51079
- writeFileSync20(abs, bytes);
51410
+ writeFileSync21(abs, bytes);
51080
51411
  sha = sha256(bytes);
51081
51412
  } else {
51082
51413
  const text = asset.text ?? "";
51083
- writeFileSync20(abs, text);
51414
+ writeFileSync21(abs, text);
51084
51415
  sha = sha256(text);
51085
51416
  }
51086
51417
  files[key] = { id: asset.id, type: kindOf3(asset.type), sha };
@@ -51093,8 +51424,8 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
51093
51424
 
51094
51425
  // src/commands/scene.ts
51095
51426
  init_manifest();
51096
- import { existsSync as existsSync26, readFileSync as readFileSync25, readdirSync as readdirSync11, statSync as statSync7, writeFileSync as writeFileSync21 } from "node:fs";
51097
- import { basename as basename13, dirname as dirname16, join as join26, resolve as resolve11 } from "node:path";
51427
+ import { existsSync as existsSync27, readFileSync as readFileSync25, readdirSync as readdirSync12, statSync as statSync8, writeFileSync as writeFileSync22 } from "node:fs";
51428
+ import { basename as basename14, dirname as dirname17, join as join27, resolve as resolve12 } from "node:path";
51098
51429
 
51099
51430
  // src/compile/sceneCompile.ts
51100
51431
  init_localFiles();
@@ -51102,17 +51433,17 @@ init_ignore();
51102
51433
  init_distRoot();
51103
51434
  init_util();
51104
51435
  init_projectCompile();
51105
- import { existsSync as existsSync25, readFileSync as readFileSync24 } from "node:fs";
51106
- import { join as join25 } from "node:path";
51436
+ import { existsSync as existsSync26, readFileSync as readFileSync24 } from "node:fs";
51437
+ import { join as join26 } from "node:path";
51107
51438
  var TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i;
51108
51439
  var rel = (key) => key.replace(/^\//, "");
51109
51440
  var resolveHarness = (loadSceneHarness2) => {
51110
- const inRepo = join25(distRoot, "..", "projects", "src", "scene-editor");
51441
+ const inRepo = join26(distRoot, "..", "projects", "src", "scene-editor");
51111
51442
  try {
51112
- return loadSceneHarness2(existsSync25(inRepo) ? inRepo : undefined);
51443
+ return loadSceneHarness2(existsSync26(inRepo) ? inRepo : undefined);
51113
51444
  } catch {
51114
- const vendored = join25(distRoot, "runtime", "scene-harness.json");
51115
- if (existsSync25(vendored))
51445
+ const vendored = join26(distRoot, "runtime", "scene-harness.json");
51446
+ if (existsSync26(vendored))
51116
51447
  return JSON.parse(readFileSync24(vendored, "utf8"));
51117
51448
  throw new CliError("scene-editor harness sources not found — reinstall the CLI or set LECODES_SCENE_HARNESS_DIR.");
51118
51449
  }
@@ -51167,22 +51498,22 @@ var getContext2 = (args) => {
51167
51498
  manifest = null;
51168
51499
  const pathArg = args._[0];
51169
51500
  if (pathArg) {
51170
- const abs = resolve11(process.cwd(), pathArg);
51171
- if (existsSync26(abs) && statSync7(abs).isDirectory())
51501
+ const abs = resolve12(process.cwd(), pathArg);
51502
+ if (existsSync27(abs) && statSync8(abs).isDirectory())
51172
51503
  root = abs;
51173
- else if (abs.endsWith(".scene.ts") && existsSync26(abs))
51174
- root = dirname16(abs);
51504
+ else if (abs.endsWith(".scene.ts") && existsSync27(abs))
51505
+ root = dirname17(abs);
51175
51506
  else
51176
51507
  throw new CliError(`No such folder or .scene.ts: ${pathArg}`);
51177
51508
  }
51178
51509
  }
51179
- return { root, manifest, name: manifest?.name ?? basename13(root) };
51510
+ return { root, manifest, name: manifest?.name ?? basename14(root) };
51180
51511
  };
51181
51512
  var writeMcpConfig2 = (root, port) => {
51182
- const path2 = join26(root, ".mcp.json");
51513
+ const path2 = join27(root, ".mcp.json");
51183
51514
  const entry = { type: "http", url: `http://localhost:${port}/mcp` };
51184
51515
  let config = {};
51185
- if (existsSync26(path2)) {
51516
+ if (existsSync27(path2)) {
51186
51517
  try {
51187
51518
  config = JSON.parse(readFileSync25(path2, "utf8"));
51188
51519
  } catch {
@@ -51197,17 +51528,17 @@ var writeMcpConfig2 = (root, port) => {
51197
51528
  return;
51198
51529
  }
51199
51530
  config.mcpServers["lecodes-scene"] = entry;
51200
- writeFileSync21(path2, JSON.stringify(config, null, 2) + `
51531
+ writeFileSync22(path2, JSON.stringify(config, null, 2) + `
51201
51532
  `);
51202
51533
  note(`${existing ? "updated" : "created"} .mcp.json (lecodes-scene MCP → ${entry.url})`);
51203
51534
  };
51204
51535
  var countScenes = (dir) => {
51205
51536
  let n2 = 0;
51206
- for (const name of readdirSync11(dir)) {
51537
+ for (const name of readdirSync12(dir)) {
51207
51538
  if (name === "node_modules" || name.startsWith("."))
51208
51539
  continue;
51209
- const full = join26(dir, name);
51210
- if (statSync7(full).isDirectory())
51540
+ const full = join27(dir, name);
51541
+ if (statSync8(full).isDirectory())
51211
51542
  n2 += countScenes(full);
51212
51543
  else if (name.endsWith(".scene.ts"))
51213
51544
  n2++;
@@ -51280,8 +51611,8 @@ var status2 = async (_args) => {
51280
51611
  };
51281
51612
 
51282
51613
  // src/commands/test.ts
51283
- import { existsSync as existsSync27, readFileSync as readFileSync26, readdirSync as readdirSync12 } from "node:fs";
51284
- import { dirname as dirname17, resolve as resolve12 } from "node:path";
51614
+ import { existsSync as existsSync28, readFileSync as readFileSync26, readdirSync as readdirSync13 } from "node:fs";
51615
+ import { dirname as dirname18, resolve as resolve13 } from "node:path";
51285
51616
  init_util();
51286
51617
  var deriveDesignFlows = (meta, entryScreen) => {
51287
51618
  const edges = meta.edges ?? [];
@@ -51357,7 +51688,7 @@ var readFlowFile = (abs, displayName) => {
51357
51688
  name: file.name ?? displayName,
51358
51689
  kind: "file",
51359
51690
  steps: file.steps,
51360
- fixtures: resolveFixtures(file.fixtures, dirname17(abs)),
51691
+ fixtures: resolveFixtures(file.fixtures, dirname18(abs)),
51361
51692
  width: file.width,
51362
51693
  height: file.height,
51363
51694
  settle: file.settle,
@@ -51369,34 +51700,34 @@ var test = async (args) => {
51369
51700
  const { js, warnings, useLocalAssets, root } = await compileHeadlessBundle(args);
51370
51701
  const renderer = await requireRenderer();
51371
51702
  const projectViewport = desktopDefaultViewport(root);
51372
- const testsDir = resolve12(root, flagStr(args, "dir") ?? "tests");
51373
- const designMetaPath = resolve12(root, flagStr(args, "design-dir") ?? "design", "meta.json");
51703
+ const testsDir = resolve13(root, flagStr(args, "dir") ?? "tests");
51704
+ const designMetaPath = resolve13(root, flagStr(args, "design-dir") ?? "design", "meta.json");
51374
51705
  const asJson = flagBool(args, "json");
51375
51706
  const out = asJson ? logErr : log;
51376
51707
  const cases = [];
51377
51708
  if (args._.length > 0) {
51378
51709
  for (const given of args._) {
51379
- const candidates = [resolve12(given), resolve12(testsDir, given), resolve12(testsDir, `${given}.flow.json`)];
51380
- const abs = candidates.find((p2) => existsSync27(p2));
51710
+ const candidates = [resolve13(given), resolve13(testsDir, given), resolve13(testsDir, `${given}.flow.json`)];
51711
+ const abs = candidates.find((p2) => existsSync28(p2));
51381
51712
  if (!abs)
51382
51713
  throw new CliError(`No such test: "${given}" (looked in ${testsDir}).`);
51383
51714
  cases.push(readFlowFile(abs, given));
51384
51715
  }
51385
- } else if (existsSync27(testsDir)) {
51386
- for (const f3 of readdirSync12(testsDir).filter((f4) => f4.endsWith(".flow.json")).sort()) {
51387
- cases.push(readFlowFile(resolve12(testsDir, f3), f3));
51716
+ } else if (existsSync28(testsDir)) {
51717
+ for (const f3 of readdirSync13(testsDir).filter((f4) => f4.endsWith(".flow.json")).sort()) {
51718
+ cases.push(readFlowFile(resolve13(testsDir, f3), f3));
51388
51719
  }
51389
51720
  }
51390
51721
  const fixturesFlag = flagStr(args, "fixtures");
51391
- const defaultPack = resolve12(testsDir, "fixtures.json");
51392
- const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") : existsSync27(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
51722
+ const defaultPack = resolve13(testsDir, "fixtures.json");
51723
+ const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") : existsSync28(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
51393
51724
  const flowsOnly = flagBool(args, "flows");
51394
51725
  const wantFlows = !flagBool(args, "no-flows") && args._.length === 0;
51395
51726
  if (flowsOnly)
51396
51727
  cases.length = 0;
51397
51728
  let skipped = [];
51398
51729
  let entryScreen = null;
51399
- if ((wantFlows || flowsOnly) && existsSync27(designMetaPath)) {
51730
+ if ((wantFlows || flowsOnly) && existsSync28(designMetaPath)) {
51400
51731
  let meta;
51401
51732
  try {
51402
51733
  meta = JSON.parse(readFileSync26(designMetaPath, "utf8"));
@@ -51431,7 +51762,7 @@ var test = async (args) => {
51431
51762
  }, timeoutMs);
51432
51763
  killer.unref?.();
51433
51764
  const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
51434
- const artifactsRoot = resolve12(root, flagStr(args, "artifacts") ?? resolve12(testsDir, ".artifacts"));
51765
+ const artifactsRoot = resolve13(root, flagStr(args, "artifacts") ?? resolve13(testsDir, ".artifacts"));
51435
51766
  const results = [];
51436
51767
  for (const kase of cases) {
51437
51768
  const flagSettle = flagStr(args, "settle");
@@ -51449,14 +51780,14 @@ var test = async (args) => {
51449
51780
  onConsole
51450
51781
  });
51451
51782
  try {
51452
- const io = fileScenarioIO(resolve12(artifactsRoot, slug3(kase.name)));
51783
+ const io = fileScenarioIO(resolve13(artifactsRoot, slug3(kase.name)));
51453
51784
  const outcome = await renderer.runScenario(session, steps, io);
51454
51785
  result = {
51455
51786
  name: kase.name,
51456
51787
  kind: kase.kind,
51457
51788
  ok: outcome.ok,
51458
51789
  steps: outcome.steps.map((s2) => ({ desc: s2.desc, ok: s2.ok, error: s2.error })),
51459
- artifacts: outcome.failureArtifacts.map((p2) => resolve12(artifactsRoot, slug3(kase.name), p2))
51790
+ artifacts: outcome.failureArtifacts.map((p2) => resolve13(artifactsRoot, slug3(kase.name), p2))
51460
51791
  };
51461
51792
  } finally {
51462
51793
  session.dispose();
@@ -51518,11 +51849,11 @@ init_config();
51518
51849
  init_manifest();
51519
51850
  init_types4();
51520
51851
  init_util();
51521
- import { resolve as resolve13 } from "node:path";
51852
+ import { resolve as resolve14 } from "node:path";
51522
51853
  var label = (bundle) => `${Object.keys(bundle).length} file(s)`;
51523
51854
  var types4 = async (args) => {
51524
51855
  const dirArg = args._[0];
51525
- const root = dirArg ? resolve13(dirArg) : (() => {
51856
+ const root = dirArg ? resolve14(dirArg) : (() => {
51526
51857
  try {
51527
51858
  return findProjectRoot(process.cwd());
51528
51859
  } catch {
@@ -51581,23 +51912,23 @@ var types4 = async (args) => {
51581
51912
  };
51582
51913
 
51583
51914
  // src/index.ts
51584
- import { existsSync as existsSync29, readFileSync as readFileSync28, realpathSync as realpathSync2 } from "node:fs";
51915
+ import { existsSync as existsSync30, readFileSync as readFileSync28, realpathSync as realpathSync2 } from "node:fs";
51585
51916
  import { createRequire as createRequire4 } from "node:module";
51586
- import { delimiter as delimiter2, dirname as dirname19, join as join28 } from "node:path";
51917
+ import { delimiter as delimiter2, dirname as dirname20, join as join29 } from "node:path";
51587
51918
 
51588
51919
  // src/commands/install.ts
51589
51920
  init_util();
51590
- import { execFileSync as execFileSync4 } from "node:child_process";
51591
- import { appendFileSync as appendFileSync3, chmodSync as chmodSync2, cpSync as cpSync3, existsSync as existsSync28, mkdirSync as mkdirSync19, readFileSync as readFileSync27, realpathSync, rmSync as rmSync6, symlinkSync } from "node:fs";
51921
+ import { execFileSync as execFileSync5 } from "node:child_process";
51922
+ import { appendFileSync as appendFileSync3, chmodSync as chmodSync2, cpSync as cpSync4, existsSync as existsSync29, mkdirSync as mkdirSync20, readFileSync as readFileSync27, realpathSync, rmSync as rmSync7, symlinkSync } from "node:fs";
51592
51923
  import { homedir as homedir7 } from "node:os";
51593
- import { basename as basename14, delimiter, dirname as dirname18, join as join27, resolve as resolve14, sep as sep2 } from "node:path";
51924
+ import { basename as basename15, delimiter, dirname as dirname19, join as join28, resolve as resolve15, sep as sep2 } from "node:path";
51594
51925
  var isWin = process.platform === "win32";
51595
51926
  var HOME = homedir7();
51596
51927
  var safeRealpath = (p2) => {
51597
51928
  try {
51598
51929
  return realpathSync(p2);
51599
51930
  } catch {
51600
- return resolve14(p2);
51931
+ return resolve15(p2);
51601
51932
  }
51602
51933
  };
51603
51934
  var onPath = (dir) => {
@@ -51613,25 +51944,25 @@ var onPath = (dir) => {
51613
51944
  });
51614
51945
  };
51615
51946
  var install = async (args) => {
51616
- const bundle = dirname18(process.execPath);
51617
- const exeName2 = basename14(process.execPath);
51947
+ const bundle = dirname19(process.execPath);
51948
+ const exeName2 = basename15(process.execPath);
51618
51949
  const chiselName = isWin ? "chisel.exe" : "chisel";
51619
- if (!existsSync28(join27(bundle, "runtime")) || !existsSync28(join27(bundle, chiselName))) {
51950
+ if (!existsSync29(join28(bundle, "runtime")) || !existsSync29(join28(bundle, chiselName))) {
51620
51951
  throw new CliError("`lecodes install` only works from the packaged release build (the archive containing lecodes + " + "chisel + runtime/). You appear to be running from source or an npm install — install globally " + "with your package manager there instead.");
51621
51952
  }
51622
- const companions = [exeName2, chiselName, "runtime", "README.txt"].filter((f3) => existsSync28(join27(bundle, f3)));
51953
+ const companions = [exeName2, chiselName, "runtime", "README.txt"].filter((f3) => existsSync29(join28(bundle, f3)));
51623
51954
  if (isWin)
51624
51955
  return installWindows(args, bundle, companions);
51625
51956
  return installUnix(args, bundle, companions);
51626
51957
  };
51627
51958
  var copyBundle = (bundle, target, companions) => {
51628
- mkdirSync19(target, { recursive: true });
51959
+ mkdirSync20(target, { recursive: true });
51629
51960
  if (safeRealpath(target) === safeRealpath(bundle))
51630
51961
  return;
51631
51962
  for (const f3 of companions) {
51632
- const dest = join27(target, f3);
51633
- rmSync6(dest, { recursive: true, force: true });
51634
- cpSync3(join27(bundle, f3), dest, { recursive: true });
51963
+ const dest = join28(target, f3);
51964
+ rmSync7(dest, { recursive: true, force: true });
51965
+ cpSync4(join28(bundle, f3), dest, { recursive: true });
51635
51966
  }
51636
51967
  };
51637
51968
  var permError = (e2, dir, system) => {
@@ -51643,41 +51974,41 @@ var permError = (e2, dir, system) => {
51643
51974
  };
51644
51975
  var installUnix = (args, bundle, companions) => {
51645
51976
  const system = flagBool(args, "system");
51646
- const libDir = resolve14(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join27(HOME, ".local", "lib", "lecodes-cli")));
51647
- const binDir = resolve14(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join27(HOME, ".local", "bin")));
51648
- const link2 = join27(binDir, "lecodes");
51977
+ const libDir = resolve15(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join28(HOME, ".local", "lib", "lecodes-cli")));
51978
+ const binDir = resolve15(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join28(HOME, ".local", "bin")));
51979
+ const link2 = join28(binDir, "lecodes");
51649
51980
  try {
51650
51981
  copyBundle(bundle, libDir, companions);
51651
- chmodSync2(join27(libDir, "lecodes"), 493);
51652
- chmodSync2(join27(libDir, "chisel"), 493);
51982
+ chmodSync2(join28(libDir, "lecodes"), 493);
51983
+ chmodSync2(join28(libDir, "chisel"), 493);
51653
51984
  } catch (e2) {
51654
51985
  throw permError(e2, libDir, system);
51655
51986
  }
51656
51987
  try {
51657
- mkdirSync19(binDir, { recursive: true });
51658
- rmSync6(link2, { force: true });
51659
- symlinkSync(join27(libDir, "lecodes"), link2);
51988
+ mkdirSync20(binDir, { recursive: true });
51989
+ rmSync7(link2, { force: true });
51990
+ symlinkSync(join28(libDir, "lecodes"), link2);
51660
51991
  } catch (e2) {
51661
51992
  throw permError(e2, binDir, system);
51662
51993
  }
51663
51994
  success(`Installed to ${c.bold(libDir)}`);
51664
- log(` ${c.dim(link2 + " → " + join27(libDir, "lecodes"))}`);
51995
+ log(` ${c.dim(link2 + " → " + join28(libDir, "lecodes"))}`);
51665
51996
  finishPathUnix(args, binDir);
51666
51997
  };
51667
51998
  var shellRc = () => {
51668
- const shell = basename14(process.env.SHELL ?? "");
51999
+ const shell = basename15(process.env.SHELL ?? "");
51669
52000
  if (shell === "zsh")
51670
- return { file: join27(HOME, ".zshrc"), name: "~/.zshrc" };
52001
+ return { file: join28(HOME, ".zshrc"), name: "~/.zshrc" };
51671
52002
  if (shell === "bash")
51672
- return { file: join27(HOME, ".bashrc"), name: "~/.bashrc" };
51673
- return { file: join27(HOME, ".profile"), name: "~/.profile" };
52003
+ return { file: join28(HOME, ".bashrc"), name: "~/.bashrc" };
52004
+ return { file: join28(HOME, ".profile"), name: "~/.profile" };
51674
52005
  };
51675
52006
  var PATH_MARKER = "# Added by `lecodes install`";
51676
52007
  var appendPathToShellRc = (binDir) => {
51677
52008
  const { file, name } = shellRc();
51678
52009
  const line = `export PATH="${binDir}:$PATH"`;
51679
52010
  try {
51680
- const existing = existsSync28(file) ? readFileSync27(file, "utf8") : "";
52011
+ const existing = existsSync29(file) ? readFileSync27(file, "utf8") : "";
51681
52012
  if (existing.includes(line))
51682
52013
  return name;
51683
52014
  appendFileSync3(file, `${existing && !existing.endsWith(`
@@ -51715,7 +52046,7 @@ var finishPathUnix = (args, binDir) => {
51715
52046
  log(` • or install system-wide (no PATH edit): ${c.bold("sudo lecodes install --system")}`);
51716
52047
  };
51717
52048
  var installWindows = (args, bundle, companions) => {
51718
- const target = resolve14(flagStr(args, "dir") ?? join27(process.env.LOCALAPPDATA ?? join27(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
52049
+ const target = resolve15(flagStr(args, "dir") ?? join28(process.env.LOCALAPPDATA ?? join28(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
51719
52050
  try {
51720
52051
  copyBundle(bundle, target, companions);
51721
52052
  } catch (e2) {
@@ -51743,7 +52074,7 @@ var installWindows = (args, bundle, companions) => {
51743
52074
  var addToUserPathWindows = (dir) => {
51744
52075
  const script = "$d = $env:LECODES_INSTALL_DIR;" + "$p = [Environment]::GetEnvironmentVariable('Path','User'); if (-not $p) { $p = '' };" + "$parts = @($p.Split(';') | Where-Object { $_ -ne '' });" + "if ($parts -notcontains $d) { [Environment]::SetEnvironmentVariable('Path', (@($parts + $d) -join ';'), 'User') }";
51745
52076
  try {
51746
- execFileSync4("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], { stdio: "ignore", env: { ...process.env, LECODES_INSTALL_DIR: dir } });
52077
+ execFileSync5("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], { stdio: "ignore", env: { ...process.env, LECODES_INSTALL_DIR: dir } });
51747
52078
  return true;
51748
52079
  } catch {
51749
52080
  return false;
@@ -51753,9 +52084,9 @@ var addToUserPathWindows = (dir) => {
51753
52084
  // src/index.ts
51754
52085
  init_util();
51755
52086
  var STANDALONE = (() => {
51756
- const dir = dirname19(process.execPath);
52087
+ const dir = dirname20(process.execPath);
51757
52088
  const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
51758
- return existsSync29(join28(dir, "runtime")) && existsSync29(join28(dir, chisel));
52089
+ return existsSync30(join29(dir, "runtime")) && existsSync30(join29(dir, chisel));
51759
52090
  })();
51760
52091
  var INSTALLED = STANDALONE && (() => {
51761
52092
  const exe = process.platform === "win32" ? "lecodes.exe" : "lecodes";
@@ -51770,8 +52101,8 @@ var INSTALLED = STANDALONE && (() => {
51770
52101
  if (!d)
51771
52102
  return false;
51772
52103
  try {
51773
- const cand = join28(d, exe);
51774
- return existsSync29(cand) && realpathSync2(cand) === self2;
52104
+ const cand = join29(d, exe);
52105
+ return existsSync30(cand) && realpathSync2(cand) === self2;
51775
52106
  } catch {
51776
52107
  return false;
51777
52108
  }
@@ -51925,7 +52256,11 @@ ${c.bold("Commands:")}
51925
52256
  run compile the current project and open it in a real
51926
52257
  desktop window (size/min-size/title default from
51927
52258
  app.json's desktop block; --width/--height/--title
51928
- override; --entry overrides the entrypoint)
52259
+ override; --fullscreen/--windowed override the
52260
+ app.json fullscreen; --scale <0.5..4|fit> overrides
52261
+ the display scale — "fit" sizes a 1080p-designed app
52262
+ to the monitor with the density raised to match;
52263
+ --entry overrides the entrypoint)
51929
52264
  update move to the newest published release
51930
52265
  (cached in ~/.lecodes/renderer)
51931
52266
  scene [path] LeCodes Scene Editor — visually edit a .scene.ts (tree, inspector,
@@ -51934,8 +52269,10 @@ ${c.bold("Commands:")}
51934
52269
  --port <n> editor-server port (default 4488)
51935
52270
  --no-open don't open the browser
51936
52271
  --no-mcp don't register the MCP server in .mcp.json
51937
- app init <ios|android> Generate a native shell in <project>/ios/ or <project>/android/
51938
- (offline-first: runs the bundled app.js; never pushed to LeCodes)
52272
+ app init <ios|android|desktop> Generate a native shell in <project>/ios/ or <project>/android/
52273
+ (offline-first: runs the bundled app.js; never pushed to LeCodes).
52274
+ desktop: no shell needed — writes app.json's desktop block
52275
+ (window size; --fullscreen for kiosks)
51939
52276
  --name <s> display name (default: app.json / folder name)
51940
52277
  --bundle-id <id> iOS bundle identifier
51941
52278
  --app-id <id> Android applicationId (default com.example.<slug>)
@@ -51977,6 +52314,15 @@ ${c.bold("Commands:")}
51977
52314
  --configuration <c> Release (default) or Debug
51978
52315
  --no-aab skip the .aab (APK only)
51979
52316
  --no-sync skip the lecodes app sync step
52317
+ app build desktop Stage a portable desktop app folder (default dist/<name>/):
52318
+ the cached host renamed to <Name>.exe + app.js + every asset +
52319
+ app.json (the host ≥1.3.0 reads title/size/fullscreen from it)
52320
+ + app.ico from app.json "icon". Duplicate asset basenames are
52321
+ a build error (they silently alias in the bundle).
52322
+ -o <dir> output folder (default dist/<name>)
52323
+ --slug <name> folder/zip name override (e.g. Latin-only)
52324
+ --probe headless-render the staged folder to a PNG
52325
+ --zip <out>.zip, UTF-8/forward-slash entries
51980
52326
  app upload ios Upload the archive to App Store Connect (then bumps buildNumber)
51981
52327
  --key-path <p8> --key-id <id> --issuer-id <id>
51982
52328
  ASC API key; stored in ~/.lecodes/asc.json
@@ -52003,15 +52349,15 @@ var OPTIONAL_PEERS = [
52003
52349
  var peerVersion = (name, probe) => {
52004
52350
  try {
52005
52351
  const require2 = createRequire4(import.meta.url);
52006
- let dir = dirname19(require2.resolve(probe));
52352
+ let dir = dirname20(require2.resolve(probe));
52007
52353
  for (let depth = 0;depth < 16; depth++) {
52008
- const manifest = join28(dir, "package.json");
52009
- if (existsSync29(manifest)) {
52354
+ const manifest = join29(dir, "package.json");
52355
+ if (existsSync30(manifest)) {
52010
52356
  const parsed = JSON.parse(readFileSync28(manifest, "utf8"));
52011
52357
  if (parsed.name === name)
52012
52358
  return parsed.version ?? "unknown";
52013
52359
  }
52014
- const parent = dirname19(dir);
52360
+ const parent = dirname20(dir);
52015
52361
  if (parent === dir)
52016
52362
  break;
52017
52363
  dir = parent;