lecodes-cli 0.14.1 → 0.15.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.1",
45215
+ version: "0.15.0",
45215
45216
  dependencies: {
45216
45217
  "@letary/chisel": "^0.6.0",
45217
45218
  jimp: "^1.6.1"
@@ -45224,7 +45225,7 @@ var package_default = {
45224
45225
  sdk: "workspace:*"
45225
45226
  },
45226
45227
  peerDependencies: {
45227
- "lecodes-design": "^0.6.1",
45228
+ "lecodes-design": "^0.8.0",
45228
45229
  "lecodes-renderer": "^0.8.2",
45229
45230
  "lecodes-3d-editor": "^0.2.0"
45230
45231
  },
@@ -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();
@@ -46359,7 +46360,7 @@ var readJson = (path) => {
46359
46360
  if (!existsSync10(path))
46360
46361
  return null;
46361
46362
  try {
46362
- return JSON.parse(readFileSync11(path, "utf8"));
46363
+ return JSON.parse(readFileSync11(path, "utf8").replace(/^\uFEFF/, ""));
46363
46364
  } catch {
46364
46365
  throw new CliError(`Invalid JSON in ${path}`);
46365
46366
  }
@@ -47984,16 +47985,492 @@ var doctorAndroidChecks = async (root, config, check) => {
47984
47985
  }
47985
47986
  };
47986
47987
 
47988
+ // src/commands/appDesktop.ts
47989
+ import { execFileSync as execFileSync3, spawn as spawn3 } from "node:child_process";
47990
+ 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";
47991
+ import { basename as basename5, dirname as dirname11, join as join16, resolve as resolve5 } from "node:path";
47992
+
47993
+ // src/desktopRenderer.ts
47994
+ init_util();
47995
+ import { createHash as createHash5 } from "node:crypto";
47996
+ import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
47997
+ 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";
47998
+ import { homedir as homedir5, tmpdir } from "node:os";
47999
+ import { join as join15, resolve as resolve4 } from "node:path";
48000
+ var MANIFEST_URL = "https://storage.yandexcloud.net/le-codes/releases/versions.json";
48001
+ var DESKTOP_RENDERER_VERSION = "1.1.0";
48002
+ var semverNewer = (a2, b) => {
48003
+ const pa = a2.split(/[.-]/).map(Number), pb = b.split(/[.-]/).map(Number);
48004
+ for (let i2 = 0;i2 < 3; i2++)
48005
+ if ((pb[i2] ?? 0) !== (pa[i2] ?? 0))
48006
+ return (pb[i2] ?? 0) - (pa[i2] ?? 0);
48007
+ return 0;
48008
+ };
48009
+ var platformKey = () => {
48010
+ if (process.platform === "win32")
48011
+ return "windows-x64";
48012
+ if (process.platform === "linux")
48013
+ return "ubuntu-linux-x64";
48014
+ 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}.`);
48015
+ };
48016
+ var exeName = () => process.platform === "win32" ? "lecodes-desktop.exe" : "lecodes-desktop";
48017
+ var findExe = (dir) => {
48018
+ const name = exeName();
48019
+ for (const entry of readdirSync5(dir, { withFileTypes: true })) {
48020
+ const p2 = join15(dir, entry.name);
48021
+ if (entry.isFile() && entry.name === name)
48022
+ return p2;
48023
+ if (entry.isDirectory()) {
48024
+ const found = findExe(p2);
48025
+ if (found)
48026
+ return found;
48027
+ }
48028
+ }
48029
+ return null;
48030
+ };
48031
+ var cacheRoot = () => join15(homedir5(), ".lecodes", "renderer");
48032
+ var currentFile = () => join15(cacheRoot(), "current.json");
48033
+ var installedDesktopVersions = () => {
48034
+ try {
48035
+ 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);
48036
+ } catch {
48037
+ return [];
48038
+ }
48039
+ };
48040
+ var selectedDesktopVersion = () => {
48041
+ try {
48042
+ const saved = JSON.parse(readFileSync14(currentFile(), "utf8")).version;
48043
+ if (saved && /^\d+\.\d+\.\d+/.test(saved) && semverNewer(DESKTOP_RENDERER_VERSION, saved) > 0)
48044
+ return saved;
48045
+ } catch {}
48046
+ return DESKTOP_RENDERER_VERSION;
48047
+ };
48048
+ var fetchDesktopManifest = async () => {
48049
+ let manifest;
48050
+ try {
48051
+ const res = await fetch(MANIFEST_URL);
48052
+ if (!res.ok)
48053
+ throw new Error(`HTTP ${res.status}`);
48054
+ manifest = await res.json();
48055
+ } catch (e2) {
48056
+ throw new CliError(`Can't fetch the release manifest (${MANIFEST_URL}): ${e2 instanceof Error ? e2.message : String(e2)}`);
48057
+ }
48058
+ const desktop = manifest.products?.desktop;
48059
+ if (!desktop)
48060
+ throw new CliError("The release manifest has no desktop product.");
48061
+ return desktop;
48062
+ };
48063
+ var downloadDesktop = async (version, dest) => {
48064
+ const key = platformKey();
48065
+ const entry = (await fetchDesktopManifest()).versions.find((v) => v.version === version);
48066
+ const file = entry?.files[key];
48067
+ if (!file) {
48068
+ throw new CliError(`LeCodes Desktop ${version} [${key}] isn't published yet — \`--desktop\` needs it for the ` + `headless render contract.
48069
+ Set LECODES_DESKTOP_EXE to a locally built host, or update lecodes-cli.`);
48070
+ }
48071
+ note(`Downloading the native renderer (LeCodes Desktop ${version}, ${(file.size / 1024 / 1024).toFixed(1)} MB)…`);
48072
+ let bytes;
48073
+ try {
48074
+ const res = await fetch(file.url);
48075
+ if (!res.ok)
48076
+ throw new Error(`HTTP ${res.status}`);
48077
+ bytes = Buffer.from(await res.arrayBuffer());
48078
+ } catch (e2) {
48079
+ throw new CliError(`Download failed (${file.url}): ${e2 instanceof Error ? e2.message : String(e2)}`);
48080
+ }
48081
+ const sha = createHash5("sha256").update(bytes).digest("hex");
48082
+ if (sha !== file.sha256)
48083
+ throw new CliError(`Downloaded archive is corrupt (sha256 mismatch) — try again.`);
48084
+ mkdirSync9(cacheRoot(), { recursive: true });
48085
+ const stage = mkdtempSync(join15(cacheRoot(), "dl-"));
48086
+ try {
48087
+ const archive = join15(stage, file.url.split("/").pop() ?? "archive");
48088
+ writeFileSync11(archive, bytes);
48089
+ const tar = spawnSync3("tar", ["-xf", archive, "-C", stage], { stdio: ["ignore", "ignore", "pipe"] });
48090
+ if (tar.status !== 0)
48091
+ throw new CliError(`Couldn't extract the renderer archive: ${tar.stderr?.toString() ?? "tar failed"}`);
48092
+ rmSync2(archive);
48093
+ if (!findExe(stage))
48094
+ throw new CliError("The downloaded archive doesn't contain the desktop host binary.");
48095
+ try {
48096
+ renameSync2(stage, dest);
48097
+ } catch {
48098
+ if (!findExe(dest))
48099
+ throw new CliError(`Couldn't move the renderer into ${dest}.`);
48100
+ }
48101
+ } finally {
48102
+ rmSync2(stage, { recursive: true, force: true });
48103
+ }
48104
+ };
48105
+ var resolveDesktopExe = async () => {
48106
+ const override = process.env.LECODES_DESKTOP_EXE;
48107
+ if (override) {
48108
+ if (!existsSync14(override))
48109
+ throw new CliError(`LECODES_DESKTOP_EXE points at "${override}" which doesn't exist.`);
48110
+ return resolve4(override);
48111
+ }
48112
+ const version = selectedDesktopVersion();
48113
+ const dir = join15(cacheRoot(), `desktop-${version}`);
48114
+ let exe = existsSync14(dir) ? findExe(dir) : null;
48115
+ if (!exe) {
48116
+ await downloadDesktop(version, dir);
48117
+ exe = findExe(dir);
48118
+ if (!exe)
48119
+ throw new CliError(`Renderer cache at ${dir} is broken — delete the folder and retry.`);
48120
+ }
48121
+ return exe;
48122
+ };
48123
+ var updateDesktop = async () => {
48124
+ const { latest } = await fetchDesktopManifest();
48125
+ if (semverNewer(latest, DESKTOP_RENDERER_VERSION) > 0) {
48126
+ 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.`);
48127
+ }
48128
+ const before = selectedDesktopVersion();
48129
+ const dir = join15(cacheRoot(), `desktop-${latest}`);
48130
+ const hadIt = existsSync14(dir) && findExe(dir) !== null;
48131
+ if (!hadIt)
48132
+ await downloadDesktop(latest, dir);
48133
+ mkdirSync9(cacheRoot(), { recursive: true });
48134
+ writeFileSync11(currentFile(), JSON.stringify({ version: latest }, null, 2));
48135
+ if (latest === before && hadIt)
48136
+ note(`Already up to date: LeCodes Desktop ${latest}.`);
48137
+ else if (latest === before)
48138
+ note(`Re-downloaded LeCodes Desktop ${latest}.`);
48139
+ else
48140
+ note(`Desktop renderer set to ${latest} (was ${before}).`);
48141
+ };
48142
+ var windowEnv = (opts) => {
48143
+ const env = {};
48144
+ const width = opts.width ?? (opts.height !== undefined ? 1280 : undefined);
48145
+ const height = opts.height ?? (opts.width !== undefined ? 720 : undefined);
48146
+ if (width && height)
48147
+ env.CREATOR_WINDOW_SIZE = `${width}x${height}`;
48148
+ if (opts.minWidth && opts.minHeight)
48149
+ env.CREATOR_MIN_WINDOW_SIZE = `${opts.minWidth}x${opts.minHeight}`;
48150
+ if (opts.title)
48151
+ env.CREATOR_WINDOW_TITLE = opts.title;
48152
+ if (opts.resizable === false)
48153
+ env.CREATOR_WINDOW_FIXED = "1";
48154
+ if (opts.fullscreen !== undefined)
48155
+ env.CREATOR_WINDOW_FULLSCREEN = opts.fullscreen ? "1" : "0";
48156
+ if (opts.scale === "fit")
48157
+ env.CREATOR_UI_SCALE = "fit";
48158
+ else if (typeof opts.scale === "number")
48159
+ env.CREATOR_UI_SCALE = String(opts.scale);
48160
+ return env;
48161
+ };
48162
+ var stageBundle = (js, resources) => {
48163
+ const runDir = mkdtempSync(join15(tmpdir(), "lecodes-render-"));
48164
+ writeFileSync11(join15(runDir, "app.js"), js);
48165
+ for (const r2 of resources) {
48166
+ if (r2.absPath && existsSync14(r2.absPath) && statSync3(r2.absPath).isFile()) {
48167
+ copyFileSync(r2.absPath, join15(runDir, r2.name));
48168
+ }
48169
+ }
48170
+ return runDir;
48171
+ };
48172
+ var runDesktopApp = async (js, resources, opts = {}) => {
48173
+ const exe = await resolveDesktopExe();
48174
+ const runDir = stageBundle(js, resources);
48175
+ try {
48176
+ const env = { ...process.env, ...windowEnv(opts) };
48177
+ await new Promise((done, fail) => {
48178
+ const child = spawn2(exe, [join15(runDir, "app.js")], { cwd: runDir, env, stdio: ["ignore", "inherit", "inherit"] });
48179
+ child.on("error", (e2) => fail(new CliError(`Can't run the desktop host: ${e2.message}`)));
48180
+ child.on("exit", (code) => {
48181
+ if (code === 0 || code === null)
48182
+ done();
48183
+ else
48184
+ fail(new CliError(`The desktop host exited with code ${code}.`));
48185
+ });
48186
+ });
48187
+ } finally {
48188
+ rmSync2(runDir, { recursive: true, force: true });
48189
+ }
48190
+ };
48191
+ var runDesktopDev = async (url, opts = {}) => {
48192
+ const exe = await resolveDesktopExe();
48193
+ const env = { ...process.env, ...windowEnv(opts) };
48194
+ await new Promise((done, fail) => {
48195
+ const child = spawn2(exe, [url], { env, stdio: ["ignore", "ignore", "inherit"] });
48196
+ child.on("error", (e2) => fail(new CliError(`Can't run the desktop host: ${e2.message}`)));
48197
+ child.on("exit", () => done());
48198
+ });
48199
+ };
48200
+ var runDesktopRender = async (js, resources, opts) => {
48201
+ const exe = await resolveDesktopExe();
48202
+ const runDir = stageBundle(js, resources);
48203
+ try {
48204
+ const appJs = join15(runDir, "app.js");
48205
+ const outAbs = resolve4(opts.outPng);
48206
+ await new Promise((done, fail) => {
48207
+ const child = spawn2(exe, [appJs], {
48208
+ cwd: runDir,
48209
+ env: {
48210
+ ...process.env,
48211
+ CREATOR_DUMP_PNG: outAbs,
48212
+ CREATOR_MAX_FRAMES: String(opts.frames),
48213
+ CREATOR_WINDOW_SIZE: `${opts.width}x${opts.height}`,
48214
+ CREATOR_HIDDEN: "1"
48215
+ },
48216
+ stdio: ["ignore", "pipe", "pipe"]
48217
+ });
48218
+ const tail = [];
48219
+ const sink = (d) => {
48220
+ const text = String(d);
48221
+ tail.push(text);
48222
+ while (tail.length > 80)
48223
+ tail.shift();
48224
+ if (opts.logs)
48225
+ process.stderr.write(text);
48226
+ };
48227
+ child.stdout.on("data", sink);
48228
+ child.stderr.on("data", sink);
48229
+ const killer = setTimeout(() => {
48230
+ child.kill();
48231
+ fail(new CliError(`The native render timed out after ${opts.timeoutMs}ms (raise --timeout, or lower --frames).`));
48232
+ }, opts.timeoutMs);
48233
+ child.on("error", (e2) => {
48234
+ clearTimeout(killer);
48235
+ fail(new CliError(`Can't run the desktop renderer: ${e2.message}`));
48236
+ });
48237
+ child.on("exit", (code) => {
48238
+ clearTimeout(killer);
48239
+ if (code === 0 && existsSync14(outAbs))
48240
+ return done();
48241
+ const detail = tail.length > 0 ? `
48242
+ ${tail.join("").trimEnd()}` : "";
48243
+ 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}`));
48244
+ });
48245
+ });
48246
+ } finally {
48247
+ rmSync2(runDir, { recursive: true, force: true });
48248
+ }
48249
+ };
48250
+
48251
+ // src/commands/appDesktop.ts
48252
+ init_util();
48253
+ var mb = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)} MB`;
48254
+ var dirSize = (dir) => readdirSync6(dir, { withFileTypes: true }).reduce((sum, e2) => {
48255
+ const p2 = join16(dir, e2.name);
48256
+ return sum + (e2.isDirectory() ? dirSize(p2) : statSync4(p2).size);
48257
+ }, 0);
48258
+ var initDesktop = async (args) => {
48259
+ const root = projectRoot();
48260
+ const existing = readJson(join16(root, "app.json")) ?? {};
48261
+ const name = flagStr(args, "name") ?? existing.name ?? basename5(root);
48262
+ const window2 = {
48263
+ width: 1280,
48264
+ height: 800,
48265
+ minWidth: 640,
48266
+ minHeight: 480,
48267
+ ...existing.desktop?.window,
48268
+ ...flagBool(args, "fullscreen") ? { fullscreen: true } : {}
48269
+ };
48270
+ const config = { ...existing, name, desktop: { ...existing.desktop, window: window2 } };
48271
+ writeJson(join16(root, "app.json"), config);
48272
+ log(`${c.green("written")} app.json (desktop.window ${window2.width}×${window2.height}${window2.fullscreen ? ", fullscreen" : ""})`);
48273
+ log(`Next: ${c.bold("lecodes dev --desktop")} to develop, ${c.bold("lecodes app build desktop")} to package.`);
48274
+ };
48275
+ var cleanFileName = (name) => name.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "").replace(/[. ]+$/, "").trim() || "app";
48276
+ var buildDesktop = async (args) => {
48277
+ const root = projectRoot();
48278
+ const config = readJson(join16(root, "app.json")) ?? {};
48279
+ const name = (config.name ?? basename5(root)).trim() || "app";
48280
+ const slug = flagStr(args, "slug") ?? cleanFileName(name);
48281
+ const out = resolve5(root, flagStr(args, "o", "out") ?? join16(root, "dist", slug));
48282
+ if (existsSync15(out)) {
48283
+ const entries = readdirSync6(out);
48284
+ const looksLikeBuild = entries.length === 0 || entries.includes("app.js");
48285
+ if (!looksLikeBuild) {
48286
+ throw new CliError(`${out} exists and doesn't look like a previous desktop build — pick another -o or clear it yourself.`);
48287
+ }
48288
+ }
48289
+ if (config.desktop?.entry && flagStr(args, "entry") === undefined)
48290
+ args.flags.entry = config.desktop.entry;
48291
+ note(`Building "${name}" for desktop…`);
48292
+ const { js, resources } = await buildBundle(args, { remotePatterns: config.resources?.remote ?? [] });
48293
+ const wanted = new Set([...js.matchAll(/fetchLocal\("([^"]+)"\)/g)].map((m) => m[1]));
48294
+ const shipped = resources.filter((r2) => !r2.remote && wanted.has(r2.name));
48295
+ const byName = new Map;
48296
+ for (const r2 of resources) {
48297
+ if (r2.remote || !wanted.has(r2.name))
48298
+ continue;
48299
+ const set = byName.get(r2.name) ?? new Set;
48300
+ set.add(r2.path);
48301
+ byName.set(r2.name, set);
48302
+ }
48303
+ const collisions = [...byName.entries()].filter(([, paths]) => paths.size > 1);
48304
+ if (collisions.length > 0) {
48305
+ 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:
48306
+ ` + collisions.map(([n2, paths]) => ` ${n2}
48307
+ ` + [...paths].map((p2) => ` ${p2}`).join(`
48308
+ `)).join(`
48309
+ `));
48310
+ }
48311
+ const shippedNames = new Set(shipped.map((r2) => r2.name));
48312
+ const missing = [...wanted].filter((n2) => !shippedNames.has(n2));
48313
+ if (missing.length > 0) {
48314
+ throw new CliError(`The bundle references assets with no local file: ${missing.join(", ")}`);
48315
+ }
48316
+ const exe = await resolveDesktopExe();
48317
+ const hostDir = dirname11(exe);
48318
+ const version = process.env.LECODES_DESKTOP_EXE ? "local build" : selectedDesktopVersion();
48319
+ rmSync3(out, { recursive: true, force: true });
48320
+ mkdirSync10(out, { recursive: true });
48321
+ for (const entry of readdirSync6(hostDir, { withFileTypes: true })) {
48322
+ const keep = entry.isFile() && entry.name.toLowerCase().endsWith(".dll") || entry.isDirectory() && (entry.name === "assets" || entry.name === "fonts");
48323
+ if (keep)
48324
+ cpSync2(join16(hostDir, entry.name), join16(out, entry.name), { recursive: true });
48325
+ }
48326
+ if (!existsSync15(join16(out, "assets", "initial.js"))) {
48327
+ 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.)`);
48328
+ }
48329
+ const exeName2 = process.platform === "win32" ? `${cleanFileName(name)}.exe` : cleanFileName(name);
48330
+ cpSync2(exe, join16(out, exeName2));
48331
+ note(` host: LeCodes Desktop ${version}`);
48332
+ writeFileSync12(join16(out, "app.js"), js);
48333
+ let assetBytes = 0, assetCount = 0;
48334
+ for (const r2 of shipped) {
48335
+ if (!r2.absPath || !existsSync15(r2.absPath) || !statSync4(r2.absPath).isFile()) {
48336
+ warn(`asset "${r2.name}" (${r2.path}) has no local file — skipped`);
48337
+ continue;
48338
+ }
48339
+ cpSync2(r2.absPath, join16(out, r2.name));
48340
+ assetBytes += statSync4(r2.absPath).size;
48341
+ assetCount++;
48342
+ }
48343
+ note(` assets: ${assetCount} files, ${mb(assetBytes)}`);
48344
+ writeJson(join16(out, "app.json"), { name, ...config.desktop ? { desktop: { window: config.desktop.window } } : {} });
48345
+ if (config.icon) {
48346
+ await writeIco(root, config, join16(out, "app.ico"));
48347
+ note(" app.ico rendered from " + config.icon);
48348
+ }
48349
+ writeFileSync12(join16(out, "README.txt"), "\uFEFF" + readmeText(name, exeName2, version));
48350
+ success(`Portable build: ${out} (${mb(dirSize(out))})`);
48351
+ if (flagBool(args, "probe"))
48352
+ await probeBuild(out, exeName2, config, args);
48353
+ if (flagBool(args, "zip"))
48354
+ zipBuild(out);
48355
+ if (!flagBool(args, "probe"))
48356
+ note(`Tip: --probe renders the staged folder headless to verify exactly what ships.`);
48357
+ };
48358
+ var writeIco = async (root, config, dest) => {
48359
+ const { image: image2 } = await loadIconMaster(root, config);
48360
+ const sizes = [16, 24, 32, 48, 64, 256];
48361
+ const pngs = [];
48362
+ for (const s2 of sizes) {
48363
+ const frame = image2.clone();
48364
+ frame.resize({ w: s2, h: s2 });
48365
+ pngs.push(Buffer.from(await frame.getBuffer("image/png")));
48366
+ }
48367
+ const header = Buffer.alloc(6 + sizes.length * 16);
48368
+ header.writeUInt16LE(0, 0);
48369
+ header.writeUInt16LE(1, 2);
48370
+ header.writeUInt16LE(sizes.length, 4);
48371
+ let offset = header.length;
48372
+ sizes.forEach((s2, i2) => {
48373
+ const e2 = 6 + i2 * 16;
48374
+ header.writeUInt8(s2 === 256 ? 0 : s2, e2);
48375
+ header.writeUInt8(s2 === 256 ? 0 : s2, e2 + 1);
48376
+ header.writeUInt16LE(1, e2 + 4);
48377
+ header.writeUInt16LE(32, e2 + 6);
48378
+ header.writeUInt32LE(pngs[i2].length, e2 + 8);
48379
+ header.writeUInt32LE(offset, e2 + 12);
48380
+ offset += pngs[i2].length;
48381
+ });
48382
+ writeFileSync12(dest, Buffer.concat([header, ...pngs]));
48383
+ };
48384
+ var probeBuild = async (out, exeName2, config, args) => {
48385
+ const win = config.desktop?.window;
48386
+ const frames = Number(flagStr(args, "frames")) || 90;
48387
+ const shot = join16(dirname11(out), `${basename5(out)}-probe.png`);
48388
+ note(`Probe: hidden render of the staged folder (${frames} frames)…`);
48389
+ await new Promise((done, fail) => {
48390
+ const child = spawn3(join16(out, exeName2), [], {
48391
+ cwd: out,
48392
+ env: {
48393
+ ...process.env,
48394
+ CREATOR_HIDDEN: "1",
48395
+ CREATOR_MAX_FRAMES: String(frames),
48396
+ CREATOR_DUMP_PNG: shot,
48397
+ CREATOR_WINDOW_SIZE: `${win?.width ?? 1280}x${win?.height ?? 720}`
48398
+ },
48399
+ stdio: ["ignore", "pipe", "pipe"]
48400
+ });
48401
+ const tail = [];
48402
+ const sink = (d) => {
48403
+ tail.push(String(d));
48404
+ while (tail.length > 60)
48405
+ tail.shift();
48406
+ };
48407
+ child.stdout.on("data", sink);
48408
+ child.stderr.on("data", sink);
48409
+ const killer = setTimeout(() => {
48410
+ child.kill();
48411
+ fail(new CliError("The probe timed out after 120s — raise --frames headroom or check the app."));
48412
+ }, 120000);
48413
+ child.on("error", (e2) => {
48414
+ clearTimeout(killer);
48415
+ fail(new CliError(`Probe couldn't run: ${e2.message}`));
48416
+ });
48417
+ child.on("exit", (code) => {
48418
+ clearTimeout(killer);
48419
+ if (code === 0 && existsSync15(shot)) {
48420
+ success(`Probe: ${shot}`);
48421
+ done();
48422
+ } else {
48423
+ const detail = tail.length > 0 ? `
48424
+ ${tail.join("").trimEnd()}` : "";
48425
+ fail(new CliError(`Probe failed (exit ${code}).${detail}`));
48426
+ }
48427
+ });
48428
+ });
48429
+ };
48430
+ var zipBuild = (out) => {
48431
+ const zip = `${out}.zip`;
48432
+ rmSync3(zip, { force: true });
48433
+ note("Zipping…");
48434
+ if (process.platform === "win32") {
48435
+ const q = (s2) => s2.replaceAll("'", "''");
48436
+ execFileSync3("powershell", ["-NoProfile", "-Command", [
48437
+ "Add-Type -AssemblyName System.IO.Compression.FileSystem;",
48438
+ `$src='${q(out)}'; $base=Split-Path $src -Parent;`,
48439
+ `$zip=[System.IO.Compression.ZipFile]::Open('${q(zip)}','Create');`,
48440
+ "Get-ChildItem $src -Recurse -File | ForEach-Object {",
48441
+ " $rel=$_.FullName.Substring($base.Length+1).Replace('\\','/');",
48442
+ " [void][System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip,$_.FullName,$rel,'Optimal') };",
48443
+ "$zip.Dispose()"
48444
+ ].join(" ")], { stdio: ["ignore", "ignore", "pipe"] });
48445
+ } else {
48446
+ execFileSync3("zip", ["-rq", zip, basename5(out)], { cwd: dirname11(out) });
48447
+ }
48448
+ success(`Zip: ${zip} (${mb(statSync4(zip).size)})`);
48449
+ };
48450
+ var readmeText = (name, exeName2, version) => `${name}
48451
+ ${"=".repeat(Math.max(name.length, 4))}
48452
+
48453
+ A LeCodes desktop app. Nothing to install: unpack the WHOLE folder anywhere and run ${exeName2}.
48454
+ Keep the files together — the app reads its assets from this folder.
48455
+
48456
+ On first run Windows may show a SmartScreen prompt (any unsigned app gets one):
48457
+ choose "More info", then "Run anyway".
48458
+
48459
+ Requires 64-bit Windows 10/11.
48460
+
48461
+ Built with LeCodes Desktop ${version}.
48462
+ `;
48463
+
47987
48464
  // src/commands/app.ts
47988
48465
  var SDK_REPO = "letary/lecodes-ios-sdk";
47989
48466
  var SDK_MIN_VERSION = "1.2.0";
47990
48467
  var SDK_VARIANTS = ["core", "2d", "3d", "full"];
47991
48468
  var SDK_IDENTITY_VERSION = "1.6.0";
47992
48469
  var shellSdkVersion = (iosDir) => {
47993
- const path2 = join15(iosDir, "LeCodesRuntime", "Package.swift");
47994
- if (!existsSync14(path2))
48470
+ const path2 = join17(iosDir, "LeCodesRuntime", "Package.swift");
48471
+ if (!existsSync16(path2))
47995
48472
  return null;
47996
- const m = /lecodes-ios-sdk\.git",\s*(?:exact|from):\s*"([^"]+)"/.exec(readFileSync14(path2, "utf8"));
48473
+ const m = /lecodes-ios-sdk\.git",\s*(?:exact|from):\s*"([^"]+)"/.exec(readFileSync15(path2, "utf8"));
47997
48474
  return m && VERSION_RE.test(m[1]) ? m[1] : null;
47998
48475
  };
47999
48476
  var IOS_SDK_OPTS = { repo: SDK_REPO, minVersion: SDK_MIN_VERSION, configKey: "ios.sdk" };
@@ -48001,15 +48478,17 @@ var appInit = async (args) => {
48001
48478
  const platform = args._[1];
48002
48479
  if (platform === "android")
48003
48480
  return initAndroid(args);
48481
+ if (platform === "desktop")
48482
+ return initDesktop(args);
48004
48483
  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>]");
48484
+ 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
48485
  }
48007
48486
  const root = projectRoot();
48008
- const iosDir = join15(root, "ios");
48009
- if (existsSync14(iosDir)) {
48487
+ const iosDir = join17(root, "ios");
48488
+ if (existsSync16(iosDir)) {
48010
48489
  throw new CliError("ios/ already exists — run `lecodes app sync` to update it.");
48011
48490
  }
48012
- const existing = readJson(join15(root, "app.json")) ?? {};
48491
+ const existing = readJson(join17(root, "app.json")) ?? {};
48013
48492
  const variant = flagStr(args, "variant") ?? existing.ios?.variant ?? "auto";
48014
48493
  if (variant !== "auto" && !SDK_VARIANTS.includes(variant)) {
48015
48494
  throw new CliError(`--variant must be auto, ${SDK_VARIANTS.join(", ")} (got "${variant}")`);
@@ -48017,7 +48496,7 @@ var appInit = async (args) => {
48017
48496
  const sdkFlag = flagStr(args, "sdk") ?? existing.ios?.sdk;
48018
48497
  const sdkVersion = await resolveSdkVersion(process.env.LECODES_IOS_SDK ?? sdkFlag ?? "latest", null, IOS_SDK_OPTS);
48019
48498
  const sdk = sdkFlag === "latest" ? "latest" : sdkVersion;
48020
- const name = flagStr(args, "name") ?? existing.name ?? basename5(root);
48499
+ const name = flagStr(args, "name") ?? existing.name ?? basename6(root);
48021
48500
  const config = {
48022
48501
  ...existing,
48023
48502
  name,
@@ -48031,7 +48510,7 @@ var appInit = async (args) => {
48031
48510
  buildNumber: existing.ios?.buildNumber ?? 1
48032
48511
  }
48033
48512
  };
48034
- writeJson(join15(root, "app.json"), config);
48513
+ writeJson(join17(root, "app.json"), config);
48035
48514
  log(`${c.green("written")} app.json`);
48036
48515
  ensureShellIgnored(root, "ios");
48037
48516
  const identity = {
@@ -48042,9 +48521,9 @@ var appInit = async (args) => {
48042
48521
  buildNumber: config.ios.buildNumber
48043
48522
  };
48044
48523
  const write = (rel, content) => {
48045
- const abs = join15(iosDir, rel);
48046
- mkdirSync9(dirname11(abs), { recursive: true });
48047
- writeFileSync11(abs, content);
48524
+ const abs = join17(iosDir, rel);
48525
+ mkdirSync11(dirname12(abs), { recursive: true });
48526
+ writeFileSync13(abs, content);
48048
48527
  log(`${c.green("created")} ios/${rel}`);
48049
48528
  };
48050
48529
  write("README.md", IOS_README);
@@ -48099,23 +48578,23 @@ var ensureSdkResourcesBundle = async (sdkVersion) => {
48099
48578
  const override = process.env.LECODES_SDK_RESOURCES;
48100
48579
  if (override) {
48101
48580
  note(`Using local resources bundle ${override} (LECODES_SDK_RESOURCES)`);
48102
- return resolve4(override);
48581
+ return resolve6(override);
48103
48582
  }
48104
- const dir = join15(homedir5(), ".lecodes", "sdk-resources", sdkVersion);
48105
- const bundle = join15(dir, "LeCodesSDKResources.bundle");
48106
- if (existsSync14(bundle))
48583
+ const dir = join17(homedir6(), ".lecodes", "sdk-resources", sdkVersion);
48584
+ const bundle = join17(dir, "LeCodesSDKResources.bundle");
48585
+ if (existsSync16(bundle))
48107
48586
  return bundle;
48108
48587
  const url = `https://github.com/${SDK_REPO}/releases/download/${sdkVersion}/LeCodesSDKResources.bundle.zip`;
48109
48588
  note(`Downloading LeCodesSDKResources.bundle (SDK ${sdkVersion})…`);
48110
48589
  const res = await fetch(url);
48111
48590
  if (!res.ok)
48112
48591
  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))
48592
+ mkdirSync11(dir, { recursive: true });
48593
+ const zip = join17(dir, "LeCodesSDKResources.bundle.zip");
48594
+ writeFileSync13(zip, new Uint8Array(await res.arrayBuffer()));
48595
+ execFileSync4("ditto", ["-x", "-k", zip, dir]);
48596
+ rmSync4(zip);
48597
+ if (!existsSync16(bundle))
48119
48598
  throw new CliError(`No LeCodesSDKResources.bundle inside the release asset ${url}`);
48120
48599
  return bundle;
48121
48600
  };
@@ -48140,13 +48619,13 @@ var resolveUpdateUrl = (update, root) => {
48140
48619
  };
48141
48620
  var appSync = async (args) => {
48142
48621
  const root = projectRoot();
48143
- const config = readJson(join15(root, "app.json"));
48622
+ const config = readJson(join17(root, "app.json"));
48144
48623
  const platform = args._[1];
48145
48624
  if (platform !== undefined && platform !== "ios" && platform !== "android") {
48146
48625
  throw new CliError(`Unknown platform "${platform}" — use ios or android.`);
48147
48626
  }
48148
- const hasIos = !!config?.ios && existsSync14(join15(root, "ios"));
48149
- const hasAndroid = !!config?.android && existsSync14(join15(root, "android"));
48627
+ const hasIos = !!config?.ios && existsSync16(join17(root, "ios"));
48628
+ const hasAndroid = !!config?.android && existsSync16(join17(root, "android"));
48150
48629
  if (platform === "ios" && !hasIos)
48151
48630
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
48152
48631
  if (platform === "android" && !hasAndroid)
@@ -48161,7 +48640,7 @@ var appSync = async (args) => {
48161
48640
  await syncAndroid(root, config, iosArtifacts);
48162
48641
  };
48163
48642
  var syncIos = async (root, config) => {
48164
- const iosDir = join15(root, "ios");
48643
+ const iosDir = join17(root, "ios");
48165
48644
  if (!config.ios)
48166
48645
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
48167
48646
  const requestedVariant = config.ios.variant ?? "auto";
@@ -48171,14 +48650,14 @@ var syncIos = async (root, config) => {
48171
48650
  const linkedSdk = shellSdkVersion(iosDir);
48172
48651
  const sdkPin = process.env.LECODES_IOS_SDK ?? config.ios.sdk ?? linkedSdk ?? "latest";
48173
48652
  const sdkVersion = await resolveSdkVersion(sdkPin, linkedSdk, IOS_SDK_OPTS);
48174
- const lockPath = join15(root, "plugins.lock.json");
48653
+ const lockPath = join17(root, "plugins.lock.json");
48175
48654
  const oldLock = readJson(lockPath)?.plugins ?? {};
48176
48655
  const pluginSpecs = Object.entries(config.plugins ?? {});
48177
48656
  const runtimePlugins = [];
48178
48657
  const infoValues = {};
48179
48658
  const entitlementValues = {};
48180
48659
  const newLock = {};
48181
- const sourcesDir = join15(iosDir, "LeCodesRuntime", "Sources");
48660
+ const sourcesDir = join17(iosDir, "LeCodesRuntime", "Sources");
48182
48661
  for (const [id, spec] of pluginSpecs) {
48183
48662
  const plugin = resolvePlugin(id, spec, oldLock[id], "ios");
48184
48663
  const ios = plugin.manifest.ios;
@@ -48186,12 +48665,12 @@ var syncIos = async (root, config) => {
48186
48665
  const register = ios.register;
48187
48666
  if (!module || !register)
48188
48667
  throw new CliError(`Plugin "${id}": manifest ios.module / ios.register missing`);
48189
- const src = join15(plugin.dir, ios.sources ?? "ios");
48190
- if (!existsSync14(src))
48668
+ const src = join17(plugin.dir, ios.sources ?? "ios");
48669
+ if (!existsSync16(src))
48191
48670
  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 });
48671
+ const dest = join17(sourcesDir, module);
48672
+ rmSync4(dest, { recursive: true, force: true });
48673
+ cpSync3(src, dest, { recursive: true });
48195
48674
  if (ios.minSdk && VERSION_RE.test(ios.minSdk) && cmpVersion(sdkVersion, ios.minSdk) < 0) {
48196
48675
  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
48676
  }
@@ -48206,9 +48685,9 @@ var syncIos = async (root, config) => {
48206
48685
  log(`${c.green("plugin")} ${id}@${plugin.lock.version} (${plugin.lock.tag} → Sources/${module})`);
48207
48686
  }
48208
48687
  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 });
48688
+ for (const entry of existsSync16(sourcesDir) ? readdirSync7(sourcesDir) : []) {
48689
+ if (!keep.has(entry) && statSync5(join17(sourcesDir, entry)).isDirectory()) {
48690
+ rmSync4(join17(sourcesDir, entry), { recursive: true });
48212
48691
  log(`${c.yellow("removed")} ios/LeCodesRuntime/Sources/${entry} (plugin uninstalled)`);
48213
48692
  }
48214
48693
  }
@@ -48216,11 +48695,11 @@ var syncIos = async (root, config) => {
48216
48695
  if (!Array.isArray(remotePatterns) || remotePatterns.some((p2) => typeof p2 !== "string")) {
48217
48696
  throw new CliError('app.json "resources.remote" must be an array of glob strings (e.g. ["videos/**"]).');
48218
48697
  }
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");
48698
+ const resourcesDir = join17(iosDir, "App", "Resources");
48699
+ rmSync4(resourcesDir, { recursive: true, force: true });
48700
+ mkdirSync11(resourcesDir, { recursive: true });
48701
+ const resources = await compileShell({ _: [], flags: { o: join17(resourcesDir, "app.js") } }, { remotePatterns });
48702
+ const bundleJs = readFileSync15(join17(resourcesDir, "app.js"), "utf8");
48224
48703
  const { variant, detected } = resolveVariant(requestedVariant, bundleJs);
48225
48704
  const WHY = {
48226
48705
  core: "UI only",
@@ -48234,8 +48713,8 @@ var syncIos = async (root, config) => {
48234
48713
  const sdkProvided = new Set;
48235
48714
  if (variant === "3d" || variant === "full") {
48236
48715
  const sdkResources = await ensureSdkResourcesBundle(sdkVersion);
48237
- cpSync2(sdkResources, join15(resourcesDir, "LeCodesSDKResources.bundle"), { recursive: true });
48238
- for (const f3 of readdirSync5(sdkResources))
48716
+ cpSync3(sdkResources, join17(resourcesDir, "LeCodesSDKResources.bundle"), { recursive: true });
48717
+ for (const f3 of readdirSync7(sdkResources))
48239
48718
  if (f3 !== "Info.plist")
48240
48719
  sdkProvided.add(f3);
48241
48720
  log(`${c.green("resources")} LeCodesSDKResources.bundle (filament) → ios/App/Resources/`);
@@ -48262,7 +48741,7 @@ var syncIos = async (root, config) => {
48262
48741
  }
48263
48742
  if (candidates.length > 1)
48264
48743
  warn(`asset name collision: ${name} (${candidates.map((r2) => r2.path).join(", ")}) — embedding ${candidates[0].path}`);
48265
- cpSync2(candidates[0].absPath, join15(resourcesDir, name));
48744
+ cpSync3(candidates[0].absPath, join17(resourcesDir, name));
48266
48745
  embedded++;
48267
48746
  }
48268
48747
  if (embedded > 0)
@@ -48282,20 +48761,20 @@ var syncIos = async (root, config) => {
48282
48761
  }
48283
48762
  if (shellUuid)
48284
48763
  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));
48764
+ writeFileSync13(join17(iosDir, "README.md"), IOS_README);
48765
+ writeFileSync13(join17(iosDir, "LeCodesRuntime", "Package.swift"), runtimePackageTemplate(sdkVersion, variant, runtimePlugins));
48766
+ writeFileSync13(join17(sourcesDir, "LeCodesRuntime", "LeCodesRuntime.swift"), runtimeSwiftTemplate(runtimePlugins, shellUuid));
48288
48767
  const mergedKeys = {};
48289
48768
  for (const [key, values] of Object.entries(infoValues))
48290
48769
  mergedKeys[key] = [...new Set(values)].join(" ");
48291
48770
  const updateUrl = resolveUpdateUrl(config.update, root);
48292
- writeFileSync11(join15(iosDir, "App", "Info.plist"), infoPlistTemplate(mergedKeys, updateUrl));
48771
+ writeFileSync13(join17(iosDir, "App", "Info.plist"), infoPlistTemplate(mergedKeys, updateUrl));
48293
48772
  if (updateUrl)
48294
48773
  log(`${c.green("update")} OTA source ${updateUrl} — checked natively at launch, new code applies next launch`);
48295
- if (pluginSpecs.length > 0 || existsSync14(lockPath))
48774
+ if (pluginSpecs.length > 0 || existsSync16(lockPath))
48296
48775
  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};`);
48776
+ const pbxPath = join17(iosDir, "App.xcodeproj", "project.pbxproj");
48777
+ 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
48778
  const mergedEntitlements = {};
48300
48779
  for (const [key, values] of Object.entries(entitlementValues)) {
48301
48780
  if (values.every((v) => Array.isArray(v))) {
@@ -48316,7 +48795,7 @@ var syncIos = async (root, config) => {
48316
48795
  }
48317
48796
  const hasEntitlements = Object.keys(mergedEntitlements).length > 0;
48318
48797
  if (pbx.includes("CODE_SIGN_ENTITLEMENTS") || hasEntitlements) {
48319
- writeFileSync11(join15(iosDir, "App", "App.entitlements"), entitlementsTemplate(mergedEntitlements));
48798
+ writeFileSync13(join17(iosDir, "App", "App.entitlements"), entitlementsTemplate(mergedEntitlements));
48320
48799
  if (hasEntitlements)
48321
48800
  log(`${c.green("caps")} App/App.entitlements: ${Object.keys(mergedEntitlements).sort().join(", ")}`);
48322
48801
  if (!pbx.includes("CODE_SIGN_ENTITLEMENTS")) {
@@ -48332,7 +48811,7 @@ $2Info.plist,`);
48332
48811
  }
48333
48812
  }
48334
48813
  }
48335
- writeFileSync11(pbxPath, pbx);
48814
+ writeFileSync13(pbxPath, pbx);
48336
48815
  if (config.icon) {
48337
48816
  await writeIosAppIcon(iosDir, await loadIconMaster(root, config));
48338
48817
  }
@@ -48344,7 +48823,7 @@ $2Info.plist,`);
48344
48823
  var appPlugin = async (args) => {
48345
48824
  const sub = args._[1];
48346
48825
  const root = projectRoot();
48347
- const configPath = join15(root, "app.json");
48826
+ const configPath = join17(root, "app.json");
48348
48827
  const config = readJson(configPath);
48349
48828
  if (!config)
48350
48829
  throw new CliError("No app.json here — run `lecodes app init <ios|android>` first.");
@@ -48355,7 +48834,7 @@ var appPlugin = async (args) => {
48355
48834
  log("No plugins installed.");
48356
48835
  return;
48357
48836
  }
48358
- const lock = readJson(join15(root, "plugins.lock.json"))?.plugins ?? {};
48837
+ const lock = readJson(join17(root, "plugins.lock.json"))?.plugins ?? {};
48359
48838
  for (const [id2, spec] of entries) {
48360
48839
  const pin = lock[id2] ? c.dim(` ${lock[id2].repo} ${lock[id2].tag} (${lock[id2].commit.slice(0, 7)})`) : c.dim(" (not synced)");
48361
48840
  log(`${c.bold(id2)} ${spec}${pin}`);
@@ -48408,14 +48887,14 @@ var exportOptionsPlist = (method, teamId, destination) => `<?xml version="1.0" e
48408
48887
  `;
48409
48888
  var requireShell = () => {
48410
48889
  const root = projectRoot();
48411
- const config = readJson(join15(root, "app.json"));
48412
- if (!config?.ios || !existsSync14(join15(root, "ios"))) {
48890
+ const config = readJson(join17(root, "app.json"));
48891
+ if (!config?.ios || !existsSync16(join17(root, "ios"))) {
48413
48892
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
48414
48893
  }
48415
48894
  return { root, config };
48416
48895
  };
48417
48896
  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"))];
48897
+ 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
48898
  const section = config[platform];
48420
48899
  const wanted = flagStr(args, "sdk");
48421
48900
  let changed = false;
@@ -48446,19 +48925,19 @@ var updateSdkPin = async (root, config, args, checkOnly, platform) => {
48446
48925
  }
48447
48926
  if (!checkOnly && nextPin !== pin) {
48448
48927
  section.sdk = nextPin;
48449
- writeJson(join15(root, "app.json"), config);
48928
+ writeJson(join17(root, "app.json"), config);
48450
48929
  }
48451
48930
  return changed;
48452
48931
  };
48453
48932
  var appUpdate = async (args) => {
48454
48933
  const root = projectRoot();
48455
- const config = readJson(join15(root, "app.json"));
48934
+ const config = readJson(join17(root, "app.json"));
48456
48935
  const platform = args._[1];
48457
48936
  if (platform !== undefined && platform !== "ios" && platform !== "android") {
48458
48937
  throw new CliError(`Unknown platform "${platform}" — use ios or android.`);
48459
48938
  }
48460
- const hasIos = !!config?.ios && existsSync14(join15(root, "ios"));
48461
- const hasAndroid = !!config?.android && existsSync14(join15(root, "android"));
48939
+ const hasIos = !!config?.ios && existsSync16(join17(root, "ios"));
48940
+ const hasAndroid = !!config?.android && existsSync16(join17(root, "android"));
48462
48941
  if (platform === "ios" && !hasIos)
48463
48942
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
48464
48943
  if (platform === "android" && !hasAndroid)
@@ -48476,7 +48955,7 @@ var appUpdate = async (args) => {
48476
48955
  changed = await updateSdkPin(root, config, args, checkOnly, "ios") || changed;
48477
48956
  if (doAndroid)
48478
48957
  changed = await updateSdkPin(root, config, args, checkOnly, "android") || changed;
48479
- const lockPath = join15(root, "plugins.lock.json");
48958
+ const lockPath = join17(root, "plugins.lock.json");
48480
48959
  const lock = readJson(lockPath)?.plugins ?? {};
48481
48960
  const specs = Object.entries(config.plugins ?? {});
48482
48961
  if (specs.length > 0 && !flagBool(args, "no-plugins")) {
@@ -48505,7 +48984,7 @@ var appUpdate = async (args) => {
48505
48984
  log(`${c.green("plugin")} ${id} ${entry?.tag ?? "—"} → ${c.bold(tag)}`);
48506
48985
  changed = true;
48507
48986
  }
48508
- if (!checkOnly && existsSync14(lockPath))
48987
+ if (!checkOnly && existsSync16(lockPath))
48509
48988
  writeJson(lockPath, { plugins: kept });
48510
48989
  }
48511
48990
  log("");
@@ -48525,9 +49004,9 @@ var appUpdate = async (args) => {
48525
49004
  };
48526
49005
  var appDoctor = async (_args) => {
48527
49006
  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"));
49007
+ const config = readJson(join17(root, "app.json"));
49008
+ const hasIos = !!config?.ios && existsSync16(join17(root, "ios"));
49009
+ const hasAndroid = !!config?.android && existsSync16(join17(root, "android"));
48531
49010
  if (!hasIos && !hasAndroid) {
48532
49011
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
48533
49012
  }
@@ -48538,14 +49017,14 @@ var appDoctor = async (_args) => {
48538
49017
  failed++;
48539
49018
  };
48540
49019
  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`);
49020
+ check(existsSync16(join17(root, config.icon)), `Icon master ${config.icon}`, `app.json "icon" points at a missing file — sync will fail`);
48542
49021
  }
48543
49022
  if (hasIos)
48544
49023
  await doctorIosChecks(root, config, check);
48545
49024
  if (hasAndroid) {
48546
49025
  await doctorAndroidChecks(root, config, check);
48547
49026
  if (!hasIos) {
48548
- const lock = readJson(join15(root, "plugins.lock.json"))?.plugins ?? {};
49027
+ const lock = readJson(join17(root, "plugins.lock.json"))?.plugins ?? {};
48549
49028
  const wanted = Object.keys(config.plugins ?? {});
48550
49029
  const inSync = wanted.every((id) => lock[id]) && Object.keys(lock).every((id) => wanted.includes(id));
48551
49030
  check(wanted.length === 0 || inSync, "Plugins synced", "run `lecodes app sync`");
@@ -48564,22 +49043,22 @@ var doctorIosChecks = async (root, config, check) => {
48564
49043
  } catch {}
48565
49044
  check(xcodeVersion !== "", `Xcode CLI (${xcodeVersion || "not found"})`, "install Xcode + command line tools");
48566
49045
  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"));
49046
+ const iconDir = join17(root, "ios", "App", "Assets.xcassets", "AppIcon.appiconset");
49047
+ const hasIcon = existsSync16(iconDir) && readdirSync7(iconDir).some((f3) => f3.endsWith(".png"));
48569
49048
  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`");
49049
+ const bundle = join17(root, "ios", "App", "Resources", "app.js");
49050
+ check(existsSync16(bundle), "Bundled app.js", "run `lecodes app sync`");
48572
49051
  const requestedVariant = config.ios.variant ?? "auto";
48573
49052
  const validVariant = requestedVariant === "auto" || SDK_VARIANTS.includes(requestedVariant);
48574
49053
  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");
49054
+ if (existsSync16(bundle) && validVariant) {
49055
+ const { variant } = resolveVariant(requestedVariant, readFileSync15(bundle, "utf8"));
49056
+ const pkg = readFileSync15(join17(root, "ios", "LeCodesRuntime", "Package.swift"), "utf8");
48578
49057
  const needsResources = variant === "3d" || variant === "full";
48579
- const hasResources = existsSync14(join15(root, "ios", "App", "Resources", "LeCodesSDKResources.bundle"));
49058
+ const hasResources = existsSync16(join17(root, "ios", "App", "Resources", "LeCodesSDKResources.bundle"));
48580
49059
  check(pkg.includes(`"LeCodesSDK-${variant}"`) && (!needsResources || hasResources), `SDK variant ${variant}${needsResources ? " (+ filament resources)" : ""}`, "run `lecodes app sync`");
48581
49060
  }
48582
- const linked = shellSdkVersion(join15(root, "ios"));
49061
+ const linked = shellSdkVersion(join17(root, "ios"));
48583
49062
  const pin = config.ios.sdk ?? "latest";
48584
49063
  check(!!linked, `SDK ${linked ?? "version unreadable"}`, "run `lecodes app sync`");
48585
49064
  if (linked && pin !== "latest") {
@@ -48591,24 +49070,24 @@ var doctorIosChecks = async (root, config, check) => {
48591
49070
  check(cmpVersion(linked, latest) >= 0, `SDK is the latest release (${latest})`, "run `lecodes app update`", false);
48592
49071
  } catch {}
48593
49072
  }
48594
- const lock = readJson(join15(root, "plugins.lock.json"))?.plugins ?? {};
49073
+ const lock = readJson(join17(root, "plugins.lock.json"))?.plugins ?? {};
48595
49074
  const wanted = Object.keys(config.plugins ?? {});
48596
49075
  const inSync = wanted.every((id) => lock[id]) && Object.keys(lock).every((id) => wanted.includes(id));
48597
49076
  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`");
49077
+ const pbxFile = join17(root, "ios", "App.xcodeproj", "project.pbxproj");
49078
+ if (existsSync16(pbxFile) && readFileSync15(pbxFile, "utf8").includes("CODE_SIGN_ENTITLEMENTS")) {
49079
+ check(existsSync16(join17(root, "ios", "App", "App.entitlements")), "App/App.entitlements present", "run `lecodes app sync`");
48601
49080
  }
48602
49081
  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");
49082
+ const runtimeSwift = join17(root, "ios", "LeCodesRuntime", "Sources", "LeCodesRuntime", "LeCodesRuntime.swift");
49083
+ const baked = existsSync16(runtimeSwift) && readFileSync15(runtimeSwift, "utf8").includes("bootProjectUuid");
48605
49084
  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
49085
  }
48607
49086
  if (config.update) {
48608
- const plist = readFileSync14(join15(root, "ios", "App", "Info.plist"), "utf8");
49087
+ const plist = readFileSync15(join17(root, "ios", "App", "Info.plist"), "utf8");
48609
49088
  check(plist.includes("<key>LeCodesUpdateURL</key>"), "OTA update URL in Info.plist", "run `lecodes app sync`", false);
48610
49089
  }
48611
- const asc = readJson(join15(homedir5(), ".lecodes", "asc.json"));
49090
+ const asc = readJson(join17(homedir6(), ".lecodes", "asc.json"));
48612
49091
  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
49092
  };
48614
49093
  var ARCHIVE_PATH = "ios/build/App.xcarchive";
@@ -48616,8 +49095,10 @@ var appBuild = async (args) => {
48616
49095
  const platform = args._[1];
48617
49096
  if (platform === "android")
48618
49097
  return buildAndroid(args);
49098
+ if (platform === "desktop")
49099
+ return buildDesktop(args);
48619
49100
  if (platform !== undefined && platform !== "ios") {
48620
- throw new CliError(`Unknown platform "${platform}" — use ios or android.`);
49101
+ throw new CliError(`Unknown platform "${platform}" — use ios, android or desktop.`);
48621
49102
  }
48622
49103
  const { root, config } = requireShell();
48623
49104
  const configuration = flagStr(args, "configuration") ?? "Release";
@@ -48642,8 +49123,8 @@ var appBuild = async (args) => {
48642
49123
  success(`Archive: ios/build/App.xcarchive`);
48643
49124
  if (flagBool(args, "no-export"))
48644
49125
  return;
48645
- const optionsPath = join15(root, "ios", "build", "ExportOptions.plist");
48646
- writeFileSync11(optionsPath, exportOptionsPlist(method, config.ios.teamId ?? ""));
49126
+ const optionsPath = join17(root, "ios", "build", "ExportOptions.plist");
49127
+ writeFileSync13(optionsPath, exportOptionsPlist(method, config.ios.teamId ?? ""));
48647
49128
  note(`Exporting (.ipa, method ${method})…`);
48648
49129
  runTool(root, "xcodebuild", [
48649
49130
  "-exportArchive",
@@ -48662,10 +49143,10 @@ var appUpload = async (args) => {
48662
49143
  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
49144
  }
48664
49145
  const { root, config } = requireShell();
48665
- if (!existsSync14(join15(root, ARCHIVE_PATH))) {
49146
+ if (!existsSync16(join17(root, ARCHIVE_PATH))) {
48666
49147
  throw new CliError("No archive at ios/build/App.xcarchive — run `lecodes app build ios` first.");
48667
49148
  }
48668
- const ascPath = join15(homedir5(), ".lecodes", "asc.json");
49149
+ const ascPath = join17(homedir6(), ".lecodes", "asc.json");
48669
49150
  const stored = readJson(ascPath) ?? {};
48670
49151
  const keyPath = flagStr(args, "key-path") ?? stored.keyPath;
48671
49152
  const keyId = flagStr(args, "key-id") ?? stored.keyId;
@@ -48674,15 +49155,15 @@ var appUpload = async (args) => {
48674
49155
  throw new CliError(`App Store Connect API key needed: --key-path <AuthKey.p8> --key-id <id> --issuer-id <id>
48675
49156
  (create one at appstoreconnect.apple.com → Users and Access → Integrations; stored in ~/.lecodes/asc.json after first use)`);
48676
49157
  }
48677
- if (!existsSync14(keyPath))
49158
+ if (!existsSync16(keyPath))
48678
49159
  throw new CliError(`API key file not found: ${keyPath}`);
48679
49160
  if (flagStr(args, "key-path")) {
48680
- mkdirSync9(dirname11(ascPath), { recursive: true });
49161
+ mkdirSync11(dirname12(ascPath), { recursive: true });
48681
49162
  writeJson(ascPath, { keyPath, keyId, issuerId });
48682
49163
  note("Stored the key reference in ~/.lecodes/asc.json");
48683
49164
  }
48684
- const optionsPath = join15(root, "ios", "build", "UploadOptions.plist");
48685
- writeFileSync11(optionsPath, exportOptionsPlist("app-store-connect", config.ios.teamId ?? "", "upload"));
49165
+ const optionsPath = join17(root, "ios", "build", "UploadOptions.plist");
49166
+ writeFileSync13(optionsPath, exportOptionsPlist("app-store-connect", config.ios.teamId ?? "", "upload"));
48686
49167
  note("Uploading to App Store Connect… (processing continues on Apple's side)");
48687
49168
  runTool(root, "xcodebuild", [
48688
49169
  "-exportArchive",
@@ -48693,14 +49174,14 @@ var appUpload = async (args) => {
48693
49174
  "-exportPath",
48694
49175
  "ios/build/upload",
48695
49176
  "-authenticationKeyPath",
48696
- resolve4(keyPath),
49177
+ resolve6(keyPath),
48697
49178
  "-authenticationKeyID",
48698
49179
  keyId,
48699
49180
  "-authenticationKeyIssuerID",
48700
49181
  issuerId,
48701
49182
  "-allowProvisioningUpdates"
48702
49183
  ]);
48703
- const configPath = join15(root, "app.json");
49184
+ const configPath = join17(root, "app.json");
48704
49185
  const fresh = readJson(configPath);
48705
49186
  fresh.ios.buildNumber = (fresh.ios.buildNumber ?? 1) + 1;
48706
49187
  writeJson(configPath, fresh);
@@ -48711,19 +49192,19 @@ var appOpen = async (args) => {
48711
49192
  const root = projectRoot();
48712
49193
  let platform = args._[1];
48713
49194
  if (!platform) {
48714
- const hasIos = existsSync14(join15(root, "ios"));
48715
- const hasAndroid = existsSync14(join15(root, "android"));
49195
+ const hasIos = existsSync16(join17(root, "ios"));
49196
+ const hasAndroid = existsSync16(join17(root, "android"));
48716
49197
  if (hasIos && hasAndroid)
48717
49198
  throw new CliError("Both shells exist — `lecodes app open ios` or `lecodes app open android`.");
48718
49199
  platform = hasAndroid ? "android" : "ios";
48719
49200
  }
48720
49201
  if (platform === "ios") {
48721
- const proj = join15(root, "ios", "App.xcodeproj");
48722
- if (!existsSync14(proj))
49202
+ const proj = join17(root, "ios", "App.xcodeproj");
49203
+ if (!existsSync16(proj))
48723
49204
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
48724
49205
  if (process.platform !== "darwin")
48725
49206
  throw new CliError("Opening Xcode requires macOS.");
48726
- execFileSync3("open", [proj], { stdio: "ignore" });
49207
+ execFileSync4("open", [proj], { stdio: "ignore" });
48727
49208
  success("Opened ios/App.xcodeproj in Xcode.");
48728
49209
  return;
48729
49210
  }
@@ -48749,15 +49230,15 @@ var app = async (args) => {
48749
49230
  return appUpload(args);
48750
49231
  if (sub === "open")
48751
49232
  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`.");
49233
+ 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
49234
  };
48754
49235
 
48755
49236
  // src/commands/clone.ts
48756
49237
  init_api();
48757
49238
  init_config();
48758
49239
  init_manifest();
48759
- import { existsSync as existsSync16, mkdirSync as mkdirSync11, readdirSync as readdirSync7 } from "node:fs";
48760
- import { resolve as resolve5 } from "node:path";
49240
+ import { existsSync as existsSync18, mkdirSync as mkdirSync13, readdirSync as readdirSync9 } from "node:fs";
49241
+ import { resolve as resolve7 } from "node:path";
48761
49242
  init_types4();
48762
49243
  init_ignore();
48763
49244
  init_util();
@@ -48778,11 +49259,11 @@ var clone3 = async (args) => {
48778
49259
  const uuid = parseUuid(args._[0]);
48779
49260
  const project = await getProject(apiUrl, token, uuid);
48780
49261
  const dir = args._[1] ?? slug(project.name);
48781
- const root = resolve5(dir);
48782
- if (existsSync16(root) && readdirSync7(root).length > 0) {
49262
+ const root = resolve7(dir);
49263
+ if (existsSync18(root) && readdirSync9(root).length > 0) {
48783
49264
  throw new CliError(`Directory "${dir}" already exists and is not empty.`);
48784
49265
  }
48785
- mkdirSync11(root, { recursive: true });
49266
+ mkdirSync13(root, { recursive: true });
48786
49267
  info(c.dim(`Fetching project ${c.bold(project.name)}…`));
48787
49268
  const files = await materializeAssets(root, project, apiUrl, token, progress);
48788
49269
  endProgress();
@@ -48804,8 +49285,8 @@ init_config();
48804
49285
  init_manifest();
48805
49286
  init_types4();
48806
49287
  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";
49288
+ import { existsSync as existsSync19, mkdirSync as mkdirSync14, readdirSync as readdirSync10, writeFileSync as writeFileSync15 } from "node:fs";
49289
+ import { join as join19, resolve as resolve8 } from "node:path";
48809
49290
 
48810
49291
  // src/commands/projectTemplates.ts
48811
49292
  var appClaudeMd = (name) => `# ${name} — LeCodes app
@@ -48861,11 +49342,28 @@ optional \`expect\` (\`screen\`, \`node\`/\`notNode\`, \`text\`/\`notText\`,
48861
49342
  with \`"fixtures": "fixtures.json"\` in its envelope.
48862
49343
  - \`fill\` fills every empty input — use it before tapping a disabled-until-filled submit.
48863
49344
 
49345
+ ## SDK documentation
49346
+
49347
+ \`.lecodes/types/\` is the exact API surface (signatures only). For semantics and examples,
49348
+ fetch the docs — they are served as raw markdown for AI tools:
49349
+
49350
+ - https://le.codes/llms.txt — the docs map; fetch it first to find the right page
49351
+ - https://le.codes/en/docs/api/<area>/<page>.md — one reference page as raw markdown
49352
+ (e.g. \`ui/styling.md\`, \`2d/scene2d.md\`, \`3d/scene.md\`, \`runtime/storage.md\`)
49353
+ - https://le.codes/en/docs/guide/<page>.md — guide pages (project anatomy, CLI, tracks)
49354
+ - https://le.codes/llms-full.txt — everything in one file (large; prefer per-page)
49355
+
49356
+ Always fetch the \`.md\` URLs — the HTML routes are a JS app and return an empty shell.
49357
+
48864
49358
  ## Other commands
48865
49359
 
48866
49360
  \`lecodes dev\` (run on a device with hot reload + QR), \`lecodes compile -o app.js\`
48867
49361
  (single-file bundle), \`lecodes design\` (screen-design board — the living spec).
48868
49362
  `;
49363
+ var CLAUDE_SETTINGS = JSON.stringify({
49364
+ permissions: { allow: ["WebFetch(domain:le.codes)"] }
49365
+ }, null, 2) + `
49366
+ `;
48869
49367
  var STARTER_FLOW = `{
48870
49368
  "name": "counter",
48871
49369
  "steps": [
@@ -48895,19 +49393,21 @@ var create = async (args) => {
48895
49393
  if (!name)
48896
49394
  throw new CliError("Usage: lecodes create <name> [directory]");
48897
49395
  const dir = args._[1] ?? slug2(name);
48898
- const root = resolve6(dir);
48899
- if (existsSync17(root) && readdirSync8(root).length > 0) {
49396
+ const root = resolve8(dir);
49397
+ if (existsSync19(root) && readdirSync10(root).length > 0) {
48900
49398
  throw new CliError(`Directory "${dir}" already exists and is not empty.`);
48901
49399
  }
48902
- mkdirSync12(root, { recursive: true });
49400
+ mkdirSync14(root, { recursive: true });
48903
49401
  info(c.dim(`Creating project ${c.bold(name)}…`));
48904
49402
  const { uuid } = await createProject(apiUrl, token, name, flagStr(args, "team"));
48905
49403
  info(c.dim("Fetching type definitions…"));
48906
49404
  await materializeTypes(root, apiUrl);
48907
49405
  writeTsconfig(root);
48908
49406
  writeDefaultIgnore(root);
48909
- writeFileSync13(join17(root, "main.ts"), STARTER);
48910
- writeFileSync13(join17(root, "CLAUDE.md"), appClaudeMd(name));
49407
+ writeFileSync15(join19(root, "main.ts"), STARTER);
49408
+ writeFileSync15(join19(root, "CLAUDE.md"), appClaudeMd(name));
49409
+ mkdirSync14(join19(root, ".claude"), { recursive: true });
49410
+ writeFileSync15(join19(root, ".claude", "settings.json"), CLAUDE_SETTINGS);
48911
49411
  const commits = await getCommits(apiUrl, token, uuid);
48912
49412
  writeManifest(root, { uuid, apiUrl, name, baseHeadId: commits.headId, files: {} });
48913
49413
  success(`Created ${c.bold(name)} in ${c.bold(dir)}/`);
@@ -48917,260 +49417,18 @@ var create = async (args) => {
48917
49417
  // src/commands/desktop.ts
48918
49418
  init_util();
48919
49419
  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 });
49420
+ var resolveScale = (flag, cfg) => {
49421
+ const raw = flag ?? cfg;
49422
+ if (raw === undefined || raw === "system")
49423
+ return;
49424
+ if (raw === "fit")
49425
+ return "fit";
49426
+ const n2 = Number(raw);
49427
+ if (!Number.isFinite(n2) || n2 < 0.5 || n2 > 4) {
49428
+ throw new CliError(`scale must be a number 0.5..4, "fit" or "system" (got "${raw}")`);
49170
49429
  }
49430
+ return n2;
49171
49431
  };
49172
-
49173
- // src/commands/desktop.ts
49174
49432
  var run = async (args) => {
49175
49433
  let root;
49176
49434
  try {
@@ -49188,7 +49446,9 @@ var run = async (args) => {
49188
49446
  minWidth: win?.minWidth,
49189
49447
  minHeight: win?.minHeight,
49190
49448
  resizable: win?.resizable,
49191
- title: flagStr(args, "title") ?? win?.title ?? appCfg?.name
49449
+ title: flagStr(args, "title") ?? win?.title ?? appCfg?.name,
49450
+ fullscreen: flagBool(args, "fullscreen") ? true : flagBool(args, "windowed") ? false : win?.fullscreen,
49451
+ scale: resolveScale(flagStr(args, "scale"), win?.scale)
49192
49452
  });
49193
49453
  };
49194
49454
  var status = async () => {
@@ -49224,15 +49484,15 @@ init_design();
49224
49484
  // src/commands/dev.ts
49225
49485
  init_manifest();
49226
49486
  init_ignore();
49227
- import { existsSync as existsSync20, mkdirSync as mkdirSync15, readFileSync as readFileSync19, watch, writeFileSync as writeFileSync16 } from "node:fs";
49487
+ import { existsSync as existsSync21, mkdirSync as mkdirSync16, readFileSync as readFileSync19, watch, writeFileSync as writeFileSync17 } from "node:fs";
49228
49488
  import { randomBytes as randomBytes2 } from "node:crypto";
49229
49489
  import { networkInterfaces } from "node:os";
49230
- import { basename as basename8, join as join20 } from "node:path";
49490
+ import { basename as basename9, join as join21 } from "node:path";
49231
49491
 
49232
49492
  // src/dev/devServer.ts
49233
49493
  init_localFiles();
49234
49494
  import { createServer as createServer2 } from "node:http";
49235
- import { createReadStream, statSync as statSync6 } from "node:fs";
49495
+ import { createReadStream, statSync as statSync7 } from "node:fs";
49236
49496
  init_projectCompile();
49237
49497
  init_fonts();
49238
49498
  init_assetIcons();
@@ -49500,14 +49760,14 @@ var sendText = (res, status2, type, body) => {
49500
49760
  res.end(body);
49501
49761
  };
49502
49762
  var sendJson = (res, status2, body) => sendText(res, status2, "application/json", JSON.stringify(body));
49503
- var readBody = (req) => new Promise((resolve8) => {
49763
+ var readBody = (req) => new Promise((resolve9) => {
49504
49764
  let body = "";
49505
49765
  req.on("data", (c4) => {
49506
49766
  if (body.length < 256 * 1024)
49507
49767
  body += c4.toString("utf8");
49508
49768
  });
49509
- req.on("end", () => resolve8(body));
49510
- req.on("error", () => resolve8(body));
49769
+ req.on("end", () => resolve9(body));
49770
+ req.on("error", () => resolve9(body));
49511
49771
  });
49512
49772
  var startDevServer = async (options) => {
49513
49773
  const { root, manifest, name, origin, token, entryOverride } = options;
@@ -49607,7 +49867,7 @@ var startDevServer = async (options) => {
49607
49867
  return sendText(res, 400, "text/plain", "Bad path");
49608
49868
  }
49609
49869
  const abs = resolveLocalPath(root, key);
49610
- if (!abs || !statSync6(abs).isFile())
49870
+ if (!abs || !statSync7(abs).isFile())
49611
49871
  return sendText(res, 404, "text/plain", "Not found");
49612
49872
  res.writeHead(200, { "content-type": contentType(key), "cache-control": "no-store" });
49613
49873
  createReadStream(abs).pipe(res);
@@ -49650,11 +49910,11 @@ var startDevServer = async (options) => {
49650
49910
  if (conn)
49651
49911
  clients.add(conn);
49652
49912
  });
49653
- await new Promise((resolve8, reject) => {
49913
+ await new Promise((resolve9, reject) => {
49654
49914
  server.once("error", reject);
49655
49915
  server.listen(options.port, options.bind ?? "0.0.0.0", () => {
49656
49916
  server.removeListener("error", reject);
49657
- resolve8();
49917
+ resolve9();
49658
49918
  });
49659
49919
  });
49660
49920
  const address = server.address();
@@ -49702,24 +49962,24 @@ var lanCandidates = () => {
49702
49962
  return out.sort((x2, y2) => x2.rank - y2.rank).map(({ address, iface }) => ({ address, iface }));
49703
49963
  };
49704
49964
  var loadDevToken = (root) => {
49705
- const path2 = join20(root, LECODES_DIR, "dev.json");
49965
+ const path2 = join21(root, LECODES_DIR, "dev.json");
49706
49966
  try {
49707
49967
  const token2 = JSON.parse(readFileSync19(path2, "utf8")).token;
49708
49968
  if (token2 && /^[a-z0-9]{8,}$/.test(token2))
49709
49969
  return token2;
49710
49970
  } catch {}
49711
49971
  const token = randomBytes2(6).toString("hex");
49712
- mkdirSync15(join20(root, LECODES_DIR), { recursive: true });
49713
- writeFileSync16(path2, JSON.stringify({ token }, null, 2) + `
49972
+ mkdirSync16(join21(root, LECODES_DIR), { recursive: true });
49973
+ writeFileSync17(path2, JSON.stringify({ token }, null, 2) + `
49714
49974
  `);
49715
49975
  return token;
49716
49976
  };
49717
49977
  var printQr = async (url) => {
49718
49978
  try {
49719
49979
  const qrcode = (await Promise.resolve().then(() => __toESM(require_main(), 1))).default;
49720
- await new Promise((resolve8) => qrcode.generate(url, { small: true }, (out) => {
49980
+ await new Promise((resolve9) => qrcode.generate(url, { small: true }, (out) => {
49721
49981
  log(out);
49722
- resolve8();
49982
+ resolve9();
49723
49983
  }));
49724
49984
  } catch {
49725
49985
  note("(couldn't render a QR code — open the URL below on the device)");
@@ -49735,7 +49995,7 @@ var dev = async (args) => {
49735
49995
  root = process.cwd();
49736
49996
  manifest = null;
49737
49997
  }
49738
- const name = manifest?.name ?? basename8(root);
49998
+ const name = manifest?.name ?? basename9(root);
49739
49999
  if (typeof globalThis.Bun !== "undefined") {
49740
50000
  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
50001
  }
@@ -49827,7 +50087,7 @@ ${result.error}`);
49827
50087
  if (alternatives.length > 0) {
49828
50088
  note(`Device can't reach it? This machine also has ${alternatives.map((a2) => `${a2.address} (${a2.iface})`).join(", ")} — retry with --host <ip>.`);
49829
50089
  }
49830
- if (existsSync20(join20(root, LECODES_DIR, "manifest.json")))
50090
+ if (existsSync21(join21(root, LECODES_DIR, "manifest.json")))
49831
50091
  note(`Token stored in ${LECODES_DIR}/dev.json (delete it to rotate the URL).`);
49832
50092
  log("");
49833
50093
  log(`Watching ${root} — save a file to hot-reload. Ctrl+C to stop.`);
@@ -49841,7 +50101,8 @@ ${result.error}`);
49841
50101
  minWidth: win?.minWidth,
49842
50102
  minHeight: win?.minHeight,
49843
50103
  resizable: win?.resizable,
49844
- title: win?.title ?? appCfg?.name ?? name
50104
+ title: win?.title ?? appCfg?.name ?? name,
50105
+ scale: resolveScale(undefined, win?.scale)
49845
50106
  }).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
50107
  }
49847
50108
  };
@@ -49855,7 +50116,7 @@ init_localFiles();
49855
50116
  // src/serverDiff.ts
49856
50117
  init_localFiles();
49857
50118
  import { readFileSync as readFileSync20 } from "node:fs";
49858
- import { basename as basename9 } from "node:path";
50119
+ import { basename as basename10 } from "node:path";
49859
50120
  var isTextAsset = (t2) => t2 === "text" || t2 === "shader" || t2 === "scene";
49860
50121
  var computeServerDiff = (project, scan2) => {
49861
50122
  const pathById = buildPaths(project.assets);
@@ -49872,7 +50133,7 @@ var computeServerDiff = (project, scan2) => {
49872
50133
  const server = serverByPath.get(path2);
49873
50134
  const local = localByPath.get(path2);
49874
50135
  if (local && !server) {
49875
- const text = isTextKind(classifyKind(basename9(path2)));
50136
+ const text = isTextKind(classifyKind(basename10(path2)));
49876
50137
  entries.push({ path: path2, status: "added", binary: !text, serverText: "", localText: text ? readFileSync20(local.absPath, "utf8") : undefined });
49877
50138
  continue;
49878
50139
  }
@@ -50047,8 +50308,8 @@ var statusLabel = (e2) => e2.status === "added" ? c.green(" added ") : e2.st
50047
50308
  init_manifest();
50048
50309
  init_types4();
50049
50310
  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";
50311
+ import { existsSync as existsSync22, mkdirSync as mkdirSync17, writeFileSync as writeFileSync18 } from "node:fs";
50312
+ import { basename as basename11, dirname as dirname14, join as join22, resolve as resolve9 } from "node:path";
50052
50313
  init_util();
50053
50314
  var STARTER2 = `// Welcome to LeCodes — the whole SDK is available as globals, no imports needed.
50054
50315
  // Run it live on a device with \`lecodes dev\`, or bundle it with \`lecodes compile -o app.js\`.
@@ -50153,18 +50414,18 @@ Full command list: \`lecodes --help\`.
50153
50414
  `;
50154
50415
  var init2 = async (args) => {
50155
50416
  const dirArg = (args._[0] ?? ".").replace(/[/\\]+$/, "") || ".";
50156
- const root = resolve8(dirArg);
50157
- const name = basename10(root);
50158
- mkdirSync16(root, { recursive: true });
50417
+ const root = resolve9(dirArg);
50418
+ const name = basename11(root);
50419
+ mkdirSync17(root, { recursive: true });
50159
50420
  let created = 0;
50160
50421
  const write = (rel, content) => {
50161
- const abs = join21(root, rel);
50162
- if (existsSync21(abs)) {
50422
+ const abs = join22(root, rel);
50423
+ if (existsSync22(abs)) {
50163
50424
  note(`kept ${rel}`);
50164
50425
  return;
50165
50426
  }
50166
- mkdirSync16(dirname13(abs), { recursive: true });
50167
- writeFileSync17(abs, content);
50427
+ mkdirSync17(dirname14(abs), { recursive: true });
50428
+ writeFileSync18(abs, content);
50168
50429
  log(`${c.green("created")} ${rel}`);
50169
50430
  created++;
50170
50431
  };
@@ -50174,6 +50435,7 @@ var init2 = async (args) => {
50174
50435
  if (forDesktop)
50175
50436
  write("app.json", appJsonDesktop(name));
50176
50437
  write("CLAUDE.md", appClaudeMd(name));
50438
+ write(".claude/settings.json", CLAUDE_SETTINGS);
50177
50439
  write("tests/counter.flow.json", STARTER_FLOW);
50178
50440
  write(".lecodesignore", DEFAULT_IGNORE_FILE);
50179
50441
  const wroteTypes = materializeTypesLocal(root);
@@ -50202,8 +50464,8 @@ init_config();
50202
50464
  init_ignore();
50203
50465
  init_localFiles();
50204
50466
  init_manifest();
50205
- import { existsSync as existsSync22 } from "node:fs";
50206
- import { basename as basename12, join as join22, resolve as resolve9 } from "node:path";
50467
+ import { existsSync as existsSync23 } from "node:fs";
50468
+ import { basename as basename13, join as join23, resolve as resolve10 } from "node:path";
50207
50469
  init_types4();
50208
50470
  init_util();
50209
50471
 
@@ -50212,7 +50474,7 @@ init_api();
50212
50474
  init_config();
50213
50475
  init_manifest();
50214
50476
  init_localFiles();
50215
- import { basename as basename11 } from "node:path";
50477
+ import { basename as basename12 } from "node:path";
50216
50478
  import { readFileSync as readFileSync21 } from "node:fs";
50217
50479
  init_util();
50218
50480
  var push = async (args) => {
@@ -50269,14 +50531,14 @@ var push = async (args) => {
50269
50531
  let uploaded = 0;
50270
50532
  for (const file of scan2.files) {
50271
50533
  const source = sourceFor(file.path);
50272
- const kind = source?.type ?? classifyKind(basename11(file.path));
50534
+ const kind = source?.type ?? classifyKind(basename12(file.path));
50273
50535
  const entry = { path: file.path, type: kind };
50274
50536
  if (source)
50275
50537
  entry.assetId = source.id;
50276
50538
  if (isTextKind(kind)) {
50277
50539
  entry.text = readFileSync21(file.absPath, "utf8");
50278
50540
  } else if (!source || source.sha !== file.sha) {
50279
- const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename11(file.path), readFileSync21(file.absPath));
50541
+ const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename12(file.path), readFileSync21(file.absPath));
50280
50542
  entry.blobFileId = blob.id;
50281
50543
  uploaded++;
50282
50544
  }
@@ -50383,10 +50645,10 @@ var link = async (args) => {
50383
50645
  const apiUrl = requireApiUrl(config);
50384
50646
  const token = requireToken(config);
50385
50647
  const dirArg = args._[0];
50386
- const root = dirArg ? resolve9(dirArg) : process.cwd();
50387
- if (!existsSync22(root))
50648
+ const root = dirArg ? resolve10(dirArg) : process.cwd();
50649
+ if (!existsSync23(root))
50388
50650
  throw new CliError(`No such directory: ${root}`);
50389
- if (existsSync22(join22(root, LECODES_DIR, "manifest.json")) && !flagBool(args, "force")) {
50651
+ if (existsSync23(join23(root, LECODES_DIR, "manifest.json")) && !flagBool(args, "force")) {
50390
50652
  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
50653
  }
50392
50654
  let enclosing = null;
@@ -50413,7 +50675,7 @@ var link = async (args) => {
50413
50675
  }
50414
50676
  name = project.name;
50415
50677
  } else {
50416
- name = flagStr(args, "name") ?? basename12(root);
50678
+ name = flagStr(args, "name") ?? basename13(root);
50417
50679
  info(c.dim(`Creating project ${c.bold(name)}…`));
50418
50680
  uuid = (await createProject(apiUrl, token, name, flagStr(args, "team"))).uuid;
50419
50681
  files = {};
@@ -50462,8 +50724,8 @@ var login = async (args) => {
50462
50724
  };
50463
50725
 
50464
50726
  // 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";
50727
+ import { mkdirSync as mkdirSync18, readFileSync as readFileSync22, writeFileSync as writeFileSync19 } from "node:fs";
50728
+ import { dirname as dirname15, resolve as resolve11 } from "node:path";
50467
50729
 
50468
50730
  // src/compile/headlessBundle.ts
50469
50731
  init_api();
@@ -50574,9 +50836,9 @@ var requireRenderer = async () => {
50574
50836
  };
50575
50837
  var fileScenarioIO = (dir) => ({
50576
50838
  write: (relPath, data) => {
50577
- const abs = resolve10(dir, relPath);
50578
- mkdirSync17(dirname14(abs), { recursive: true });
50579
- writeFileSync18(abs, data);
50839
+ const abs = resolve11(dir, relPath);
50840
+ mkdirSync18(dirname15(abs), { recursive: true });
50841
+ writeFileSync19(abs, data);
50580
50842
  }
50581
50843
  });
50582
50844
  var resolveFixtures = (value, baseDir) => {
@@ -50584,7 +50846,7 @@ var resolveFixtures = (value, baseDir) => {
50584
50846
  return;
50585
50847
  let map = value;
50586
50848
  if (typeof value === "string") {
50587
- const abs = resolve10(baseDir, value);
50849
+ const abs = resolve11(baseDir, value);
50588
50850
  try {
50589
50851
  map = JSON.parse(readFileSync22(abs, "utf8"));
50590
50852
  } catch (e2) {
@@ -50599,7 +50861,7 @@ var resolveFixtures = (value, baseDir) => {
50599
50861
  var readScenarioFile = (path2) => {
50600
50862
  let raw;
50601
50863
  try {
50602
- raw = JSON.parse(readFileSync22(resolve10(path2), "utf8"));
50864
+ raw = JSON.parse(readFileSync22(resolve11(path2), "utf8"));
50603
50865
  } catch (e2) {
50604
50866
  throw new CliError(`Can't read scenario "${path2}": ${e2 instanceof Error ? e2.message : String(e2)}`);
50605
50867
  }
@@ -50657,7 +50919,7 @@ var render = async (args) => {
50657
50919
  const settleStr = flagStr(args, "settle") ?? (scenario?.settle !== undefined ? String(scenario.settle) : undefined);
50658
50920
  const timeStr = flagStr(args, "time");
50659
50921
  const onConsole = flagBool(args, "logs") ? (level, text2) => logErr(`${c.dim(`[${level}]`)} ${text2}`) : undefined;
50660
- const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname14(resolve10(scriptFlag)) : ".");
50922
+ const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname15(resolve11(scriptFlag)) : ".");
50661
50923
  const flagFixtures = resolveFixtures(flagStr(args, "fixtures"), ".");
50662
50924
  const fixtures = envelopeFixtures || flagFixtures ? { ...envelopeFixtures, ...flagFixtures } : undefined;
50663
50925
  if (fixtures) {
@@ -50696,7 +50958,7 @@ var render = async (args) => {
50696
50958
  ${c.red(st.error ?? "failed")}`);
50697
50959
  }
50698
50960
  if (!result2.ok && result2.failureArtifacts.length > 0) {
50699
- logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p2) => resolve10(outDir, p2)).join(", ")}`));
50961
+ logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p2) => resolve11(outDir, p2)).join(", ")}`));
50700
50962
  }
50701
50963
  logErr(result2.ok ? c.green(`✓ ${result2.steps.length}/${steps.length} steps passed`) : c.red(`✗ failed at step ${result2.steps.length} of ${steps.length}`));
50702
50964
  return done((cb) => cb(), result2.ok ? 0 : 1);
@@ -50718,7 +50980,7 @@ var render = async (args) => {
50718
50980
  if (isNullSink(pngFlag)) {
50719
50981
  note(`Discarded PNG (${pngFlag})`);
50720
50982
  } else {
50721
- writeFileSync18(resolve10(pngFlag), png2);
50983
+ writeFileSync19(resolve11(pngFlag), png2);
50722
50984
  note(`Wrote ${pngFlag} (${png2.length} bytes, ${width}x${height})`);
50723
50985
  }
50724
50986
  return done((cb) => cb());
@@ -50733,7 +50995,7 @@ var render = async (args) => {
50733
50995
  return done((cb) => cb());
50734
50996
  }
50735
50997
  if (out) {
50736
- writeFileSync18(resolve10(out), result.text);
50998
+ writeFileSync19(resolve11(out), result.text);
50737
50999
  note(`Wrote ${out} (${result.text.length} bytes)`);
50738
51000
  return done((cb) => cb());
50739
51001
  }
@@ -50748,8 +51010,8 @@ init_api();
50748
51010
  init_config();
50749
51011
  init_manifest();
50750
51012
  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";
51013
+ import { appendFileSync as appendFileSync2, existsSync as existsSync24, readFileSync as readFileSync23, writeFileSync as writeFileSync20 } from "node:fs";
51014
+ import { join as join24 } from "node:path";
50753
51015
  var ENV_VAR = "LECODES_PUSH_KEY";
50754
51016
  var context = () => {
50755
51017
  const root = findProjectRoot(process.cwd());
@@ -50893,11 +51155,11 @@ var pnKeysList = async (ctx) => {
50893
51155
  };
50894
51156
  var writeEnv = (root, key) => {
50895
51157
  const touched = [];
50896
- const envPath = join23(root, ".env");
51158
+ const envPath = join24(root, ".env");
50897
51159
  const line = `${ENV_VAR}=${key}`;
50898
- const existing = existsSync23(envPath) ? readFileSync23(envPath, "utf8") : "";
51160
+ const existing = existsSync24(envPath) ? readFileSync23(envPath, "utf8") : "";
50899
51161
  if (new RegExp(`^${ENV_VAR}=`, "m").test(existing)) {
50900
- writeFileSync19(envPath, existing.replace(new RegExp(`^${ENV_VAR}=.*$`, "m"), line));
51162
+ writeFileSync20(envPath, existing.replace(new RegExp(`^${ENV_VAR}=.*$`, "m"), line));
50901
51163
  } else {
50902
51164
  const separator = !existing || existing.endsWith(`
50903
51165
  `) ? "" : `
@@ -50906,8 +51168,8 @@ var writeEnv = (root, key) => {
50906
51168
  `);
50907
51169
  }
50908
51170
  touched.push(".env");
50909
- const ignorePath = join23(root, ".gitignore");
50910
- const ignore = existsSync23(ignorePath) ? readFileSync23(ignorePath, "utf8") : "";
51171
+ const ignorePath = join24(root, ".gitignore");
51172
+ const ignore = existsSync24(ignorePath) ? readFileSync23(ignorePath, "utf8") : "";
50911
51173
  if (!ignore.split(`
50912
51174
  `).some((l2) => l2.trim() === ".env")) {
50913
51175
  const separator = !ignore || ignore.endsWith(`
@@ -51006,8 +51268,8 @@ var pn = async (args) => {
51006
51268
  init_api();
51007
51269
  init_config();
51008
51270
  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";
51271
+ import { existsSync as existsSync25, mkdirSync as mkdirSync19, rmSync as rmSync6, writeFileSync as writeFileSync21 } from "node:fs";
51272
+ import { dirname as dirname16, join as join25 } from "node:path";
51011
51273
  init_types4();
51012
51274
  init_localFiles();
51013
51275
  init_util();
@@ -51030,9 +51292,9 @@ var pull = async (args) => {
51030
51292
  const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a2) => a2.id === id)?.type !== "folder").map(([, path2]) => path2));
51031
51293
  for (const path2 of Object.keys(manifest.files)) {
51032
51294
  if (!remotePaths.has(path2)) {
51033
- const abs = join24(root, ...path2.replace(/^\//, "").split("/"));
51034
- if (existsSync24(abs))
51035
- rmSync5(abs);
51295
+ const abs = join25(root, ...path2.replace(/^\//, "").split("/"));
51296
+ if (existsSync25(abs))
51297
+ rmSync6(abs);
51036
51298
  }
51037
51299
  }
51038
51300
  const files = await materializeAssets(root, project, apiUrl, token, progress);
@@ -51069,18 +51331,18 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
51069
51331
  const files = { ...manifest.files };
51070
51332
  for (const asset of targets) {
51071
51333
  const key = pathById.get(asset.id);
51072
- const abs = join24(root, ...key.replace(/^\//, "").split("/"));
51073
- mkdirSync18(dirname15(abs), { recursive: true });
51334
+ const abs = join25(root, ...key.replace(/^\//, "").split("/"));
51335
+ mkdirSync19(dirname16(abs), { recursive: true });
51074
51336
  let sha;
51075
51337
  if (asset.type === "file") {
51076
51338
  if (!asset.file?.src)
51077
51339
  continue;
51078
51340
  const bytes = await downloadBlob(apiUrl, asset.file.src);
51079
- writeFileSync20(abs, bytes);
51341
+ writeFileSync21(abs, bytes);
51080
51342
  sha = sha256(bytes);
51081
51343
  } else {
51082
51344
  const text = asset.text ?? "";
51083
- writeFileSync20(abs, text);
51345
+ writeFileSync21(abs, text);
51084
51346
  sha = sha256(text);
51085
51347
  }
51086
51348
  files[key] = { id: asset.id, type: kindOf3(asset.type), sha };
@@ -51093,8 +51355,8 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
51093
51355
 
51094
51356
  // src/commands/scene.ts
51095
51357
  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";
51358
+ import { existsSync as existsSync27, readFileSync as readFileSync25, readdirSync as readdirSync12, statSync as statSync8, writeFileSync as writeFileSync22 } from "node:fs";
51359
+ import { basename as basename14, dirname as dirname17, join as join27, resolve as resolve12 } from "node:path";
51098
51360
 
51099
51361
  // src/compile/sceneCompile.ts
51100
51362
  init_localFiles();
@@ -51102,17 +51364,17 @@ init_ignore();
51102
51364
  init_distRoot();
51103
51365
  init_util();
51104
51366
  init_projectCompile();
51105
- import { existsSync as existsSync25, readFileSync as readFileSync24 } from "node:fs";
51106
- import { join as join25 } from "node:path";
51367
+ import { existsSync as existsSync26, readFileSync as readFileSync24 } from "node:fs";
51368
+ import { join as join26 } from "node:path";
51107
51369
  var TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i;
51108
51370
  var rel = (key) => key.replace(/^\//, "");
51109
51371
  var resolveHarness = (loadSceneHarness2) => {
51110
- const inRepo = join25(distRoot, "..", "projects", "src", "scene-editor");
51372
+ const inRepo = join26(distRoot, "..", "projects", "src", "scene-editor");
51111
51373
  try {
51112
- return loadSceneHarness2(existsSync25(inRepo) ? inRepo : undefined);
51374
+ return loadSceneHarness2(existsSync26(inRepo) ? inRepo : undefined);
51113
51375
  } catch {
51114
- const vendored = join25(distRoot, "runtime", "scene-harness.json");
51115
- if (existsSync25(vendored))
51376
+ const vendored = join26(distRoot, "runtime", "scene-harness.json");
51377
+ if (existsSync26(vendored))
51116
51378
  return JSON.parse(readFileSync24(vendored, "utf8"));
51117
51379
  throw new CliError("scene-editor harness sources not found — reinstall the CLI or set LECODES_SCENE_HARNESS_DIR.");
51118
51380
  }
@@ -51167,22 +51429,22 @@ var getContext2 = (args) => {
51167
51429
  manifest = null;
51168
51430
  const pathArg = args._[0];
51169
51431
  if (pathArg) {
51170
- const abs = resolve11(process.cwd(), pathArg);
51171
- if (existsSync26(abs) && statSync7(abs).isDirectory())
51432
+ const abs = resolve12(process.cwd(), pathArg);
51433
+ if (existsSync27(abs) && statSync8(abs).isDirectory())
51172
51434
  root = abs;
51173
- else if (abs.endsWith(".scene.ts") && existsSync26(abs))
51174
- root = dirname16(abs);
51435
+ else if (abs.endsWith(".scene.ts") && existsSync27(abs))
51436
+ root = dirname17(abs);
51175
51437
  else
51176
51438
  throw new CliError(`No such folder or .scene.ts: ${pathArg}`);
51177
51439
  }
51178
51440
  }
51179
- return { root, manifest, name: manifest?.name ?? basename13(root) };
51441
+ return { root, manifest, name: manifest?.name ?? basename14(root) };
51180
51442
  };
51181
51443
  var writeMcpConfig2 = (root, port) => {
51182
- const path2 = join26(root, ".mcp.json");
51444
+ const path2 = join27(root, ".mcp.json");
51183
51445
  const entry = { type: "http", url: `http://localhost:${port}/mcp` };
51184
51446
  let config = {};
51185
- if (existsSync26(path2)) {
51447
+ if (existsSync27(path2)) {
51186
51448
  try {
51187
51449
  config = JSON.parse(readFileSync25(path2, "utf8"));
51188
51450
  } catch {
@@ -51197,17 +51459,17 @@ var writeMcpConfig2 = (root, port) => {
51197
51459
  return;
51198
51460
  }
51199
51461
  config.mcpServers["lecodes-scene"] = entry;
51200
- writeFileSync21(path2, JSON.stringify(config, null, 2) + `
51462
+ writeFileSync22(path2, JSON.stringify(config, null, 2) + `
51201
51463
  `);
51202
51464
  note(`${existing ? "updated" : "created"} .mcp.json (lecodes-scene MCP → ${entry.url})`);
51203
51465
  };
51204
51466
  var countScenes = (dir) => {
51205
51467
  let n2 = 0;
51206
- for (const name of readdirSync11(dir)) {
51468
+ for (const name of readdirSync12(dir)) {
51207
51469
  if (name === "node_modules" || name.startsWith("."))
51208
51470
  continue;
51209
- const full = join26(dir, name);
51210
- if (statSync7(full).isDirectory())
51471
+ const full = join27(dir, name);
51472
+ if (statSync8(full).isDirectory())
51211
51473
  n2 += countScenes(full);
51212
51474
  else if (name.endsWith(".scene.ts"))
51213
51475
  n2++;
@@ -51280,8 +51542,8 @@ var status2 = async (_args) => {
51280
51542
  };
51281
51543
 
51282
51544
  // 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";
51545
+ import { existsSync as existsSync28, readFileSync as readFileSync26, readdirSync as readdirSync13 } from "node:fs";
51546
+ import { dirname as dirname18, resolve as resolve13 } from "node:path";
51285
51547
  init_util();
51286
51548
  var deriveDesignFlows = (meta, entryScreen) => {
51287
51549
  const edges = meta.edges ?? [];
@@ -51357,7 +51619,7 @@ var readFlowFile = (abs, displayName) => {
51357
51619
  name: file.name ?? displayName,
51358
51620
  kind: "file",
51359
51621
  steps: file.steps,
51360
- fixtures: resolveFixtures(file.fixtures, dirname17(abs)),
51622
+ fixtures: resolveFixtures(file.fixtures, dirname18(abs)),
51361
51623
  width: file.width,
51362
51624
  height: file.height,
51363
51625
  settle: file.settle,
@@ -51369,34 +51631,34 @@ var test = async (args) => {
51369
51631
  const { js, warnings, useLocalAssets, root } = await compileHeadlessBundle(args);
51370
51632
  const renderer = await requireRenderer();
51371
51633
  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");
51634
+ const testsDir = resolve13(root, flagStr(args, "dir") ?? "tests");
51635
+ const designMetaPath = resolve13(root, flagStr(args, "design-dir") ?? "design", "meta.json");
51374
51636
  const asJson = flagBool(args, "json");
51375
51637
  const out = asJson ? logErr : log;
51376
51638
  const cases = [];
51377
51639
  if (args._.length > 0) {
51378
51640
  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));
51641
+ const candidates = [resolve13(given), resolve13(testsDir, given), resolve13(testsDir, `${given}.flow.json`)];
51642
+ const abs = candidates.find((p2) => existsSync28(p2));
51381
51643
  if (!abs)
51382
51644
  throw new CliError(`No such test: "${given}" (looked in ${testsDir}).`);
51383
51645
  cases.push(readFlowFile(abs, given));
51384
51646
  }
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));
51647
+ } else if (existsSync28(testsDir)) {
51648
+ for (const f3 of readdirSync13(testsDir).filter((f4) => f4.endsWith(".flow.json")).sort()) {
51649
+ cases.push(readFlowFile(resolve13(testsDir, f3), f3));
51388
51650
  }
51389
51651
  }
51390
51652
  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;
51653
+ const defaultPack = resolve13(testsDir, "fixtures.json");
51654
+ const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") : existsSync28(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
51393
51655
  const flowsOnly = flagBool(args, "flows");
51394
51656
  const wantFlows = !flagBool(args, "no-flows") && args._.length === 0;
51395
51657
  if (flowsOnly)
51396
51658
  cases.length = 0;
51397
51659
  let skipped = [];
51398
51660
  let entryScreen = null;
51399
- if ((wantFlows || flowsOnly) && existsSync27(designMetaPath)) {
51661
+ if ((wantFlows || flowsOnly) && existsSync28(designMetaPath)) {
51400
51662
  let meta;
51401
51663
  try {
51402
51664
  meta = JSON.parse(readFileSync26(designMetaPath, "utf8"));
@@ -51431,7 +51693,7 @@ var test = async (args) => {
51431
51693
  }, timeoutMs);
51432
51694
  killer.unref?.();
51433
51695
  const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
51434
- const artifactsRoot = resolve12(root, flagStr(args, "artifacts") ?? resolve12(testsDir, ".artifacts"));
51696
+ const artifactsRoot = resolve13(root, flagStr(args, "artifacts") ?? resolve13(testsDir, ".artifacts"));
51435
51697
  const results = [];
51436
51698
  for (const kase of cases) {
51437
51699
  const flagSettle = flagStr(args, "settle");
@@ -51449,14 +51711,14 @@ var test = async (args) => {
51449
51711
  onConsole
51450
51712
  });
51451
51713
  try {
51452
- const io = fileScenarioIO(resolve12(artifactsRoot, slug3(kase.name)));
51714
+ const io = fileScenarioIO(resolve13(artifactsRoot, slug3(kase.name)));
51453
51715
  const outcome = await renderer.runScenario(session, steps, io);
51454
51716
  result = {
51455
51717
  name: kase.name,
51456
51718
  kind: kase.kind,
51457
51719
  ok: outcome.ok,
51458
51720
  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))
51721
+ artifacts: outcome.failureArtifacts.map((p2) => resolve13(artifactsRoot, slug3(kase.name), p2))
51460
51722
  };
51461
51723
  } finally {
51462
51724
  session.dispose();
@@ -51518,11 +51780,11 @@ init_config();
51518
51780
  init_manifest();
51519
51781
  init_types4();
51520
51782
  init_util();
51521
- import { resolve as resolve13 } from "node:path";
51783
+ import { resolve as resolve14 } from "node:path";
51522
51784
  var label = (bundle) => `${Object.keys(bundle).length} file(s)`;
51523
51785
  var types4 = async (args) => {
51524
51786
  const dirArg = args._[0];
51525
- const root = dirArg ? resolve13(dirArg) : (() => {
51787
+ const root = dirArg ? resolve14(dirArg) : (() => {
51526
51788
  try {
51527
51789
  return findProjectRoot(process.cwd());
51528
51790
  } catch {
@@ -51581,23 +51843,23 @@ var types4 = async (args) => {
51581
51843
  };
51582
51844
 
51583
51845
  // src/index.ts
51584
- import { existsSync as existsSync29, readFileSync as readFileSync28, realpathSync as realpathSync2 } from "node:fs";
51846
+ import { existsSync as existsSync30, readFileSync as readFileSync28, realpathSync as realpathSync2 } from "node:fs";
51585
51847
  import { createRequire as createRequire4 } from "node:module";
51586
- import { delimiter as delimiter2, dirname as dirname19, join as join28 } from "node:path";
51848
+ import { delimiter as delimiter2, dirname as dirname20, join as join29 } from "node:path";
51587
51849
 
51588
51850
  // src/commands/install.ts
51589
51851
  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";
51852
+ import { execFileSync as execFileSync5 } from "node:child_process";
51853
+ 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
51854
  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";
51855
+ import { basename as basename15, delimiter, dirname as dirname19, join as join28, resolve as resolve15, sep as sep2 } from "node:path";
51594
51856
  var isWin = process.platform === "win32";
51595
51857
  var HOME = homedir7();
51596
51858
  var safeRealpath = (p2) => {
51597
51859
  try {
51598
51860
  return realpathSync(p2);
51599
51861
  } catch {
51600
- return resolve14(p2);
51862
+ return resolve15(p2);
51601
51863
  }
51602
51864
  };
51603
51865
  var onPath = (dir) => {
@@ -51613,25 +51875,25 @@ var onPath = (dir) => {
51613
51875
  });
51614
51876
  };
51615
51877
  var install = async (args) => {
51616
- const bundle = dirname18(process.execPath);
51617
- const exeName2 = basename14(process.execPath);
51878
+ const bundle = dirname19(process.execPath);
51879
+ const exeName2 = basename15(process.execPath);
51618
51880
  const chiselName = isWin ? "chisel.exe" : "chisel";
51619
- if (!existsSync28(join27(bundle, "runtime")) || !existsSync28(join27(bundle, chiselName))) {
51881
+ if (!existsSync29(join28(bundle, "runtime")) || !existsSync29(join28(bundle, chiselName))) {
51620
51882
  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
51883
  }
51622
- const companions = [exeName2, chiselName, "runtime", "README.txt"].filter((f3) => existsSync28(join27(bundle, f3)));
51884
+ const companions = [exeName2, chiselName, "runtime", "README.txt"].filter((f3) => existsSync29(join28(bundle, f3)));
51623
51885
  if (isWin)
51624
51886
  return installWindows(args, bundle, companions);
51625
51887
  return installUnix(args, bundle, companions);
51626
51888
  };
51627
51889
  var copyBundle = (bundle, target, companions) => {
51628
- mkdirSync19(target, { recursive: true });
51890
+ mkdirSync20(target, { recursive: true });
51629
51891
  if (safeRealpath(target) === safeRealpath(bundle))
51630
51892
  return;
51631
51893
  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 });
51894
+ const dest = join28(target, f3);
51895
+ rmSync7(dest, { recursive: true, force: true });
51896
+ cpSync4(join28(bundle, f3), dest, { recursive: true });
51635
51897
  }
51636
51898
  };
51637
51899
  var permError = (e2, dir, system) => {
@@ -51643,41 +51905,41 @@ var permError = (e2, dir, system) => {
51643
51905
  };
51644
51906
  var installUnix = (args, bundle, companions) => {
51645
51907
  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");
51908
+ const libDir = resolve15(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join28(HOME, ".local", "lib", "lecodes-cli")));
51909
+ const binDir = resolve15(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join28(HOME, ".local", "bin")));
51910
+ const link2 = join28(binDir, "lecodes");
51649
51911
  try {
51650
51912
  copyBundle(bundle, libDir, companions);
51651
- chmodSync2(join27(libDir, "lecodes"), 493);
51652
- chmodSync2(join27(libDir, "chisel"), 493);
51913
+ chmodSync2(join28(libDir, "lecodes"), 493);
51914
+ chmodSync2(join28(libDir, "chisel"), 493);
51653
51915
  } catch (e2) {
51654
51916
  throw permError(e2, libDir, system);
51655
51917
  }
51656
51918
  try {
51657
- mkdirSync19(binDir, { recursive: true });
51658
- rmSync6(link2, { force: true });
51659
- symlinkSync(join27(libDir, "lecodes"), link2);
51919
+ mkdirSync20(binDir, { recursive: true });
51920
+ rmSync7(link2, { force: true });
51921
+ symlinkSync(join28(libDir, "lecodes"), link2);
51660
51922
  } catch (e2) {
51661
51923
  throw permError(e2, binDir, system);
51662
51924
  }
51663
51925
  success(`Installed to ${c.bold(libDir)}`);
51664
- log(` ${c.dim(link2 + " → " + join27(libDir, "lecodes"))}`);
51926
+ log(` ${c.dim(link2 + " → " + join28(libDir, "lecodes"))}`);
51665
51927
  finishPathUnix(args, binDir);
51666
51928
  };
51667
51929
  var shellRc = () => {
51668
- const shell = basename14(process.env.SHELL ?? "");
51930
+ const shell = basename15(process.env.SHELL ?? "");
51669
51931
  if (shell === "zsh")
51670
- return { file: join27(HOME, ".zshrc"), name: "~/.zshrc" };
51932
+ return { file: join28(HOME, ".zshrc"), name: "~/.zshrc" };
51671
51933
  if (shell === "bash")
51672
- return { file: join27(HOME, ".bashrc"), name: "~/.bashrc" };
51673
- return { file: join27(HOME, ".profile"), name: "~/.profile" };
51934
+ return { file: join28(HOME, ".bashrc"), name: "~/.bashrc" };
51935
+ return { file: join28(HOME, ".profile"), name: "~/.profile" };
51674
51936
  };
51675
51937
  var PATH_MARKER = "# Added by `lecodes install`";
51676
51938
  var appendPathToShellRc = (binDir) => {
51677
51939
  const { file, name } = shellRc();
51678
51940
  const line = `export PATH="${binDir}:$PATH"`;
51679
51941
  try {
51680
- const existing = existsSync28(file) ? readFileSync27(file, "utf8") : "";
51942
+ const existing = existsSync29(file) ? readFileSync27(file, "utf8") : "";
51681
51943
  if (existing.includes(line))
51682
51944
  return name;
51683
51945
  appendFileSync3(file, `${existing && !existing.endsWith(`
@@ -51715,7 +51977,7 @@ var finishPathUnix = (args, binDir) => {
51715
51977
  log(` • or install system-wide (no PATH edit): ${c.bold("sudo lecodes install --system")}`);
51716
51978
  };
51717
51979
  var installWindows = (args, bundle, companions) => {
51718
- const target = resolve14(flagStr(args, "dir") ?? join27(process.env.LOCALAPPDATA ?? join27(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
51980
+ const target = resolve15(flagStr(args, "dir") ?? join28(process.env.LOCALAPPDATA ?? join28(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
51719
51981
  try {
51720
51982
  copyBundle(bundle, target, companions);
51721
51983
  } catch (e2) {
@@ -51743,7 +52005,7 @@ var installWindows = (args, bundle, companions) => {
51743
52005
  var addToUserPathWindows = (dir) => {
51744
52006
  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
52007
  try {
51746
- execFileSync4("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], { stdio: "ignore", env: { ...process.env, LECODES_INSTALL_DIR: dir } });
52008
+ execFileSync5("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], { stdio: "ignore", env: { ...process.env, LECODES_INSTALL_DIR: dir } });
51747
52009
  return true;
51748
52010
  } catch {
51749
52011
  return false;
@@ -51753,9 +52015,9 @@ var addToUserPathWindows = (dir) => {
51753
52015
  // src/index.ts
51754
52016
  init_util();
51755
52017
  var STANDALONE = (() => {
51756
- const dir = dirname19(process.execPath);
52018
+ const dir = dirname20(process.execPath);
51757
52019
  const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
51758
- return existsSync29(join28(dir, "runtime")) && existsSync29(join28(dir, chisel));
52020
+ return existsSync30(join29(dir, "runtime")) && existsSync30(join29(dir, chisel));
51759
52021
  })();
51760
52022
  var INSTALLED = STANDALONE && (() => {
51761
52023
  const exe = process.platform === "win32" ? "lecodes.exe" : "lecodes";
@@ -51770,8 +52032,8 @@ var INSTALLED = STANDALONE && (() => {
51770
52032
  if (!d)
51771
52033
  return false;
51772
52034
  try {
51773
- const cand = join28(d, exe);
51774
- return existsSync29(cand) && realpathSync2(cand) === self2;
52035
+ const cand = join29(d, exe);
52036
+ return existsSync30(cand) && realpathSync2(cand) === self2;
51775
52037
  } catch {
51776
52038
  return false;
51777
52039
  }
@@ -51925,7 +52187,11 @@ ${c.bold("Commands:")}
51925
52187
  run compile the current project and open it in a real
51926
52188
  desktop window (size/min-size/title default from
51927
52189
  app.json's desktop block; --width/--height/--title
51928
- override; --entry overrides the entrypoint)
52190
+ override; --fullscreen/--windowed override the
52191
+ app.json fullscreen; --scale <0.5..4|fit> overrides
52192
+ the display scale — "fit" sizes a 1080p-designed app
52193
+ to the monitor with the density raised to match;
52194
+ --entry overrides the entrypoint)
51929
52195
  update move to the newest published release
51930
52196
  (cached in ~/.lecodes/renderer)
51931
52197
  scene [path] LeCodes Scene Editor — visually edit a .scene.ts (tree, inspector,
@@ -51934,8 +52200,10 @@ ${c.bold("Commands:")}
51934
52200
  --port <n> editor-server port (default 4488)
51935
52201
  --no-open don't open the browser
51936
52202
  --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)
52203
+ app init <ios|android|desktop> Generate a native shell in <project>/ios/ or <project>/android/
52204
+ (offline-first: runs the bundled app.js; never pushed to LeCodes).
52205
+ desktop: no shell needed — writes app.json's desktop block
52206
+ (window size; --fullscreen for kiosks)
51939
52207
  --name <s> display name (default: app.json / folder name)
51940
52208
  --bundle-id <id> iOS bundle identifier
51941
52209
  --app-id <id> Android applicationId (default com.example.<slug>)
@@ -51977,6 +52245,15 @@ ${c.bold("Commands:")}
51977
52245
  --configuration <c> Release (default) or Debug
51978
52246
  --no-aab skip the .aab (APK only)
51979
52247
  --no-sync skip the lecodes app sync step
52248
+ app build desktop Stage a portable desktop app folder (default dist/<name>/):
52249
+ the cached host renamed to <Name>.exe + app.js + every asset +
52250
+ app.json (the host ≥1.3.0 reads title/size/fullscreen from it)
52251
+ + app.ico from app.json "icon". Duplicate asset basenames are
52252
+ a build error (they silently alias in the bundle).
52253
+ -o <dir> output folder (default dist/<name>)
52254
+ --slug <name> folder/zip name override (e.g. Latin-only)
52255
+ --probe headless-render the staged folder to a PNG
52256
+ --zip <out>.zip, UTF-8/forward-slash entries
51980
52257
  app upload ios Upload the archive to App Store Connect (then bumps buildNumber)
51981
52258
  --key-path <p8> --key-id <id> --issuer-id <id>
51982
52259
  ASC API key; stored in ~/.lecodes/asc.json
@@ -52003,15 +52280,15 @@ var OPTIONAL_PEERS = [
52003
52280
  var peerVersion = (name, probe) => {
52004
52281
  try {
52005
52282
  const require2 = createRequire4(import.meta.url);
52006
- let dir = dirname19(require2.resolve(probe));
52283
+ let dir = dirname20(require2.resolve(probe));
52007
52284
  for (let depth = 0;depth < 16; depth++) {
52008
- const manifest = join28(dir, "package.json");
52009
- if (existsSync29(manifest)) {
52285
+ const manifest = join29(dir, "package.json");
52286
+ if (existsSync30(manifest)) {
52010
52287
  const parsed = JSON.parse(readFileSync28(manifest, "utf8"));
52011
52288
  if (parsed.name === name)
52012
52289
  return parsed.version ?? "unknown";
52013
52290
  }
52014
- const parent = dirname19(dir);
52291
+ const parent = dirname20(dir);
52015
52292
  if (parent === dir)
52016
52293
  break;
52017
52294
  dir = parent;