lecodes-cli 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -42916,7 +42916,7 @@ var init_types4 = __esm(() => {
42916
42916
  });
42917
42917
 
42918
42918
  // src/compile/designCompile.ts
42919
- import { readFileSync as readFileSync16 } from "node:fs";
42919
+ import { readFileSync as readFileSync17 } from "node:fs";
42920
42920
  import { basename as basename6 } from "node:path";
42921
42921
  var LOCAL_ASSET_SCHEME = "lecodesfile://", compileDesignScreen = async (opts) => {
42922
42922
  const { root, manifest, designDir, designKey, screenId, state } = opts;
@@ -42934,7 +42934,7 @@ var LOCAL_ASSET_SCHEME = "lecodesfile://", compileDesignScreen = async (opts) =>
42934
42934
  } else if (type === "shader") {
42935
42935
  entries.push({ path: file.path, type, shaders: [] });
42936
42936
  } else {
42937
- entries.push({ path: file.path, type, text: readFileSync16(file.absPath, "utf8") });
42937
+ entries.push({ path: file.path, type, text: readFileSync17(file.absPath, "utf8") });
42938
42938
  }
42939
42939
  }
42940
42940
  const invoke = state !== undefined ? `screen(${JSON.stringify(state)})` : "screen()";
@@ -42979,7 +42979,7 @@ var init_designCompile = __esm(() => {
42979
42979
 
42980
42980
  // src/browserAuth.ts
42981
42981
  import { createServer } from "node:http";
42982
- import { spawn as spawn2 } from "node:child_process";
42982
+ import { spawn as spawn3 } from "node:child_process";
42983
42983
  import { randomBytes } from "node:crypto";
42984
42984
  import { hostname } from "node:os";
42985
42985
  var SUCCESS_HTML = `<!doctype html><meta charset="utf-8"><title>lecodes</title>
@@ -43004,16 +43004,16 @@ var SUCCESS_HTML = `<!doctype html><meta charset="utf-8"><title>lecodes</title>
43004
43004
  args = [url];
43005
43005
  }
43006
43006
  try {
43007
- const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
43007
+ const child = spawn3(cmd, args, { stdio: "ignore", detached: true });
43008
43008
  child.on("error", () => {});
43009
43009
  child.unref();
43010
43010
  } catch {}
43011
43011
  }, browserLogin = async (webBase, timeoutMs = 5 * 60 * 1000, open = openBrowser) => {
43012
43012
  const state = randomBytes(16).toString("hex");
43013
- let resolve7;
43013
+ let resolve8;
43014
43014
  let reject;
43015
43015
  const tokenPromise = new Promise((res, rej) => {
43016
- resolve7 = res;
43016
+ resolve8 = res;
43017
43017
  reject = rej;
43018
43018
  });
43019
43019
  const server = createServer((req, res) => {
@@ -43031,7 +43031,7 @@ var SUCCESS_HTML = `<!doctype html><meta charset="utf-8"><title>lecodes</title>
43031
43031
  }
43032
43032
  res.writeHead(200, { "content-type": "text/html" });
43033
43033
  res.end(SUCCESS_HTML);
43034
- resolve7(token);
43034
+ resolve8(token);
43035
43035
  });
43036
43036
  await new Promise((res, rej) => {
43037
43037
  server.once("error", rej);
@@ -43469,23 +43469,28 @@ overlap, unreadable contrast, off-screen content) before reporting back.
43469
43469
  ## Comments are the task list
43470
43470
 
43471
43471
  Reviewers pin comments to the board — on a screen, on the state it was showing, usually on a named
43472
- element ("the \`submit\` button on \`checkout@empty\`"). They live in \`comments.json\`; read them with
43473
- the \`design_comments\` tool (or \`lecodes design comments\`), which is the closest thing this design
43474
- has to a backlog. **Open threads are the work.** A comment tells you which file to open and what to
43475
- change in it, so start a turn there rather than asking what to do next.
43472
+ element ("the \`submit\` button on \`checkout@empty\`"). Each thread is one file under \`comments/\`;
43473
+ read them with the \`design_comments\` tool (or \`lecodes design comments\`), which is the closest
43474
+ thing this design has to a backlog. **Open threads are the work.** A comment tells you which file to
43475
+ open and what to change in it, so start a turn there rather than asking what to do next.
43476
43476
 
43477
43477
  When you've actually made the change, close the thread with \`resolve_comment\` (thread number, plus a
43478
43478
  short reply saying what you did — the reviewer sees it). Your reply goes out signed **Claude** and
43479
43479
  badged AI, because someone who asked a person for a change is owed the fact that a machine answered
43480
43480
  — so say what you changed, not that you'll look into it. Resolving without doing the work is worse
43481
- than leaving it open: it hides the request from the person who made it. Never hand-edit
43482
- \`comments.json\` to close something.
43481
+ than leaving it open: it hides the request from the person who made it. Never hand-edit the files in
43482
+ \`comments/\` to close something. To ask the reviewer something new, start a thread with
43483
+ \`comment_add\`.
43483
43484
 
43484
43485
  Most comments arrive from the **public share link**, where they're written by people who never see
43485
- this repo. On a project cloned from the platform, \`lecodes design\` keeps the two ends reconciled
43486
- while it runs: their comments appear in \`comments.json\` on their own, and your replies and resolves
43487
- go back to them the same way. With the server down, \`lecodes design comments pull\` / \`push\` do it
43488
- by hand. A thread you start here is local until you \`push\` it.
43486
+ this repo. On a project cloned from the platform, \`lecodes design\` mirrors their feedback into
43487
+ \`comments/\` on its own while it runs — but everything written here only goes OUT with the next
43488
+ \`lecodes push\`, together with (and after) the design it describes. So a resolve never reaches the
43489
+ reviewer before the change itself does; the work isn't delivered until that push runs. With the
43490
+ server down (no MCP), the same actions work from the terminal — \`lecodes design comments pull\`
43491
+ fetches the latest feedback, and \`lecodes design comments add / reply <n> / resolve <n>\` write the
43492
+ same store. Pass \`--ai\` so the write is signed as you rather than the developer; never write the
43493
+ JSON by hand (a message missing its \`pending\` flag is never delivered, and nothing warns).
43489
43494
 
43490
43495
  ## Workflow
43491
43496
 
@@ -43537,8 +43542,8 @@ __export(exports_design, {
43537
43542
  flushDesignComments: () => flushDesignComments,
43538
43543
  design: () => design
43539
43544
  });
43540
- import { existsSync as existsSync18, mkdirSync as mkdirSync13, readFileSync as readFileSync17, readdirSync as readdirSync9, writeFileSync as writeFileSync14, appendFileSync } from "node:fs";
43541
- import { basename as basename7, join as join18 } from "node:path";
43545
+ import { existsSync as existsSync19, mkdirSync as mkdirSync14, readFileSync as readFileSync18, readdirSync as readdirSync10, writeFileSync as writeFileSync15, appendFileSync } from "node:fs";
43546
+ import { basename as basename7, join as join19 } from "node:path";
43542
43547
  var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43543
43548
  let root;
43544
43549
  let manifest;
@@ -43555,11 +43560,11 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43555
43560
  manifest,
43556
43561
  name: manifest?.name ?? basename7(root),
43557
43562
  dirName,
43558
- designDir: join18(root, dirName),
43563
+ designDir: join19(root, dirName),
43559
43564
  designKey: "/" + dirName.replace(/\\/g, "/")
43560
43565
  };
43561
43566
  }, loadDesignServer = async (ctx, needs = []) => {
43562
- if (!existsSync18(ctx.designDir)) {
43567
+ if (!existsSync19(ctx.designDir)) {
43563
43568
  throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
43564
43569
  }
43565
43570
  let mod;
@@ -43575,7 +43580,7 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43575
43580
  return mod;
43576
43581
  }, requirePlatform = (ctx, verb) => {
43577
43582
  if (!ctx.manifest) {
43578
- throw new CliError(`\`lecodes design comments ${verb}\` needs a cloned project. This folder is standalone, so ${ctx.dirName}/comments.json is already the only store — there is nothing to sync with.`);
43583
+ throw new CliError(`\`lecodes design comments ${verb}\` needs a cloned project. This folder is standalone, so ${ctx.dirName}/comments/ is already the only store — there is nothing to sync with.`);
43579
43584
  }
43580
43585
  const config = loadConfig();
43581
43586
  return { apiUrl: requireApiUrl(config), token: requireToken(config), uuid: ctx.manifest.uuid };
@@ -43591,24 +43596,24 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43591
43596
  `).find((l2) => l2.trim())?.trim() ?? "";
43592
43597
  return line.length > max ? line.slice(0, max - 1) + "…" : line;
43593
43598
  }, listScreens = (designDir) => {
43594
- const dir = join18(designDir, "screens");
43595
- if (!existsSync18(dir))
43599
+ const dir = join19(designDir, "screens");
43600
+ if (!existsSync19(dir))
43596
43601
  return [];
43597
- return readdirSync9(dir).filter((f3) => f3.endsWith(".ts") && !f3.startsWith(".")).map((f3) => f3.slice(0, -3)).sort();
43602
+ return readdirSync10(dir).filter((f3) => f3.endsWith(".ts") && !f3.startsWith(".")).map((f3) => f3.slice(0, -3)).sort();
43598
43603
  }, readMetaSizes = (designDir) => {
43599
43604
  try {
43600
- const meta = JSON.parse(readFileSync17(join18(designDir, "meta.json"), "utf8"));
43605
+ const meta = JSON.parse(readFileSync18(join19(designDir, "meta.json"), "utf8"));
43601
43606
  return meta?.screens ?? {};
43602
43607
  } catch {
43603
43608
  return {};
43604
43609
  }
43605
43610
  }, writeMcpConfig = (ctx, port) => {
43606
- const path2 = join18(ctx.root, ".mcp.json");
43611
+ const path2 = join19(ctx.root, ".mcp.json");
43607
43612
  const entry = { type: "http", url: `http://localhost:${port}/mcp` };
43608
43613
  let config = {};
43609
- if (existsSync18(path2)) {
43614
+ if (existsSync19(path2)) {
43610
43615
  try {
43611
- config = JSON.parse(readFileSync17(path2, "utf8"));
43616
+ config = JSON.parse(readFileSync18(path2, "utf8"));
43612
43617
  } catch {
43613
43618
  warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-design server by hand.");
43614
43619
  return;
@@ -43621,7 +43626,7 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43621
43626
  return;
43622
43627
  }
43623
43628
  config.mcpServers["lecodes-design"] = entry;
43624
- writeFileSync14(path2, JSON.stringify(config, null, 2) + `
43629
+ writeFileSync15(path2, JSON.stringify(config, null, 2) + `
43625
43630
  `);
43626
43631
  log(`${c.green(existing ? "updated" : "created")} .mcp.json (lecodes-design MCP → ${entry.url})`);
43627
43632
  }, init = async (ctx, args) => {
@@ -43630,26 +43635,26 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43630
43635
  ["spec.md", SPEC_MD],
43631
43636
  ["README.md", README_HINT],
43632
43637
  ["CLAUDE.md", CLAUDE_MD],
43633
- [join18("shared", "tokens.ts"), TOKENS_TS],
43634
- [join18("shared", "tabs.ts"), TABS_TS],
43635
- [join18("screens", "home.ts"), HOME_SCREEN_TS]
43638
+ [join19("shared", "tokens.ts"), TOKENS_TS],
43639
+ [join19("shared", "tabs.ts"), TABS_TS],
43640
+ [join19("screens", "home.ts"), HOME_SCREEN_TS]
43636
43641
  ];
43637
- mkdirSync13(join18(ctx.designDir, "screens"), { recursive: true });
43638
- mkdirSync13(join18(ctx.designDir, "shared"), { recursive: true });
43642
+ mkdirSync14(join19(ctx.designDir, "screens"), { recursive: true });
43643
+ mkdirSync14(join19(ctx.designDir, "shared"), { recursive: true });
43639
43644
  let created = 0;
43640
43645
  for (const [rel, content] of files) {
43641
- const abs = join18(ctx.designDir, rel);
43642
- if (existsSync18(abs)) {
43646
+ const abs = join19(ctx.designDir, rel);
43647
+ if (existsSync19(abs)) {
43643
43648
  note(`kept ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
43644
43649
  continue;
43645
43650
  }
43646
- writeFileSync14(abs, content);
43651
+ writeFileSync15(abs, content);
43647
43652
  log(`${c.green("created")} ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
43648
43653
  created++;
43649
43654
  }
43650
- const ignorePath = join18(ctx.root, ".lecodesignore");
43655
+ const ignorePath = join19(ctx.root, ".lecodesignore");
43651
43656
  const ignoreLines = [`${ctx.dirName}/.snapshots/`, `${ctx.dirName}/comments/`];
43652
- const current = existsSync18(ignorePath) ? readFileSync17(ignorePath, "utf8") : "";
43657
+ const current = existsSync19(ignorePath) ? readFileSync18(ignorePath, "utf8") : "";
43653
43658
  const present = new Set(current.split(/\r?\n/));
43654
43659
  const missing = ignoreLines.filter((line) => !present.has(line));
43655
43660
  if (missing.length) {
@@ -43664,8 +43669,8 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43664
43669
  writeMcpConfig(ctx, Number(flagStr(args, "port")) || DEFAULT_PORT);
43665
43670
  const wroteTypes = materializeTypesLocal(ctx.designDir);
43666
43671
  writeTsconfig(ctx.designDir);
43667
- mkdirSync13(join18(ctx.designDir, ".lecodes", "types"), { recursive: true });
43668
- writeFileSync14(join18(ctx.designDir, ".lecodes", "types", "design-macros.d.ts"), DESIGN_MACROS_DTS);
43672
+ mkdirSync14(join19(ctx.designDir, ".lecodes", "types"), { recursive: true });
43673
+ writeFileSync15(join19(ctx.designDir, ".lecodes", "types", "design-macros.d.ts"), DESIGN_MACROS_DTS);
43669
43674
  log(`${c.green("created")} ${ctx.dirName}/tsconfig.json${wroteTypes ? ` + ${ctx.dirName}/.lecodes/types/` : ""} (IDE types)`);
43670
43675
  if (!wroteTypes)
43671
43676
  warnErr("Bundled SDK types not found — tsconfig written, but globals may show unresolved until the CLI is rebuilt with vendored types.");
@@ -43713,16 +43718,16 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43713
43718
  killer.unref?.();
43714
43719
  const png2 = flagBool(args, "png") || flagStr(args, "png") !== undefined;
43715
43720
  const sizes = readMetaSizes(ctx.designDir);
43716
- const outDir = flagStr(args, "out-dir") ?? join18(ctx.designDir, ".snapshots");
43721
+ const outDir = flagStr(args, "out-dir") ?? join19(ctx.designDir, ".snapshots");
43717
43722
  const single = targets.length === 1 && requested.length === 1 && !png2 && !allStates && !flagStr(args, "out-dir");
43718
43723
  const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
43719
43724
  if (!single)
43720
- mkdirSync13(outDir, { recursive: true });
43725
+ mkdirSync14(outDir, { recursive: true });
43721
43726
  for (const id of targets) {
43722
43727
  const [width, height] = sizes[id]?.size ?? [390, 844];
43723
43728
  let plan = [{ state: undefined, bases: [id] }];
43724
43729
  if (discover) {
43725
- const { states, canonical } = discover(readFileSync17(join18(ctx.designDir, "screens", `${id}.ts`), "utf8"));
43730
+ const { states, canonical } = discover(readFileSync18(join19(ctx.designDir, "screens", `${id}.ts`), "utf8"));
43726
43731
  if (stateFlag !== undefined) {
43727
43732
  if (!states.includes(stateFlag)) {
43728
43733
  if (requested.length === 1)
@@ -43763,11 +43768,11 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43763
43768
  for (const w of pngResult.warnings)
43764
43769
  warnErr(`${tag}: ${w}`);
43765
43770
  for (const base of bases) {
43766
- writeFileSync14(join18(outDir, `${base}.json`), result.text);
43767
- log(`${c.green("✓")} ${tag} → ${join18(outDir, `${base}.json`)}`);
43771
+ writeFileSync15(join19(outDir, `${base}.json`), result.text);
43772
+ log(`${c.green("✓")} ${tag} → ${join19(outDir, `${base}.json`)}`);
43768
43773
  if (pngResult) {
43769
- writeFileSync14(join18(outDir, `${base}.png`), pngResult.png);
43770
- log(`${c.green("✓")} ${tag} → ${join18(outDir, `${base}.png`)}`);
43774
+ writeFileSync15(join19(outDir, `${base}.png`), pngResult.png);
43775
+ log(`${c.green("✓")} ${tag} → ${join19(outDir, `${base}.png`)}`);
43771
43776
  }
43772
43777
  }
43773
43778
  }
@@ -43942,11 +43947,17 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43942
43947
  const sub = args._[1];
43943
43948
  if (sub === "pull")
43944
43949
  return commentsPull(ctx);
43950
+ if (sub === "add")
43951
+ return commentsAdd(ctx, args);
43952
+ if (sub === "reply")
43953
+ return commentsReply(ctx, args);
43954
+ if (sub === "resolve")
43955
+ return commentsResolve(ctx, args);
43945
43956
  if (sub === "push") {
43946
43957
  throw new CliError("Comments go up with the design, not on their own — run `lecodes push`.\n" + " (A reply that arrives before the change it describes is worse than one that arrives late.)");
43947
43958
  }
43948
43959
  if (sub !== undefined) {
43949
- throw new CliError(`Unknown comments subcommand "${sub}" (pull, or nothing to list).`);
43960
+ throw new CliError(`Unknown comments subcommand "${sub}" (add, reply, resolve, pull, or nothing to list).`);
43950
43961
  }
43951
43962
  const designServer = await loadDesignServer(ctx);
43952
43963
  const all = flagBool(args, "all");
@@ -43987,6 +43998,56 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43987
43998
  log("");
43988
43999
  }
43989
44000
  log(`${open} open · ${threads.length} total`);
44001
+ }, cliAuthor = (args) => flagBool(args, "ai") || process.env.CLAUDECODE ? { author: "Claude", origin: "ai" } : undefined, commentDelivery = (ctx) => optionalPlatform(ctx) ? "queued — it goes out with the next `lecodes push`, after the design it describes" : "saved — this folder is the record", findThreadByNumber = (designServer, ctx, raw) => {
44002
+ const number = Number(raw);
44003
+ if (!Number.isInteger(number) || number <= 0) {
44004
+ throw new CliError(`"${String(raw ?? "")}" is not a thread number — see: lecodes design comments`);
44005
+ }
44006
+ const thread = designServer.listDesignComments(ctx.designDir).threads.find((t2) => t2.number === number);
44007
+ if (!thread)
44008
+ throw new CliError(`No comment #${number} on this design — see: lecodes design comments`);
44009
+ return thread;
44010
+ }, commentsAdd = async (ctx, args) => {
44011
+ const body = String(args._[2] ?? "").trim();
44012
+ if (!body) {
44013
+ throw new CliError('Usage: lecodes design comments add "<text>" [--screen <id>] [--state <s>] [--element <name>] [--ai]');
44014
+ }
44015
+ const designServer = await loadDesignServer(ctx, ["startThread", "readDesignState"]);
44016
+ const screen2 = flagStr(args, "screen")?.trim() || undefined;
44017
+ if (screen2) {
44018
+ const ids = designServer.readDesignState(ctx.designDir).screens.map((s2) => s2.id);
44019
+ if (!ids.includes(screen2)) {
44020
+ throw new CliError(`No screen "${screen2}" in ${ctx.dirName}/ — the pin would point at nothing. Have: ${ids.join(", ") || "(none)"}.`);
44021
+ }
44022
+ }
44023
+ const element = flagStr(args, "element")?.trim() || undefined;
44024
+ const thread = designServer.startThread(ctx.designDir, {
44025
+ anchor: {
44026
+ screen: screen2,
44027
+ state: flagStr(args, "state")?.trim() || undefined,
44028
+ ...element ? { target: { name: element } } : {}
44029
+ },
44030
+ body,
44031
+ ...cliAuthor(args)
44032
+ }, { linked: !!optionalPlatform(ctx) });
44033
+ log(`${c.green("✓")} ${threadRef(thread)} added — ${commentDelivery(ctx)}`);
44034
+ }, commentsReply = async (ctx, args) => {
44035
+ const body = String(args._[3] ?? "").trim();
44036
+ if (!body)
44037
+ throw new CliError('Usage: lecodes design comments reply <number> "<text>" [--ai]');
44038
+ const designServer = await loadDesignServer(ctx, ["replyToThread"]);
44039
+ const thread = findThreadByNumber(designServer, ctx, args._[2]);
44040
+ designServer.replyToThread(ctx.designDir, thread.id, body, cliAuthor(args));
44041
+ log(`${c.green("✓")} replied to #${thread.number} — ${commentDelivery(ctx)}`);
44042
+ }, commentsResolve = async (ctx, args) => {
44043
+ const designServer = await loadDesignServer(ctx, ["resolveThread", "replyToThread"]);
44044
+ const thread = findThreadByNumber(designServer, ctx, args._[2]);
44045
+ const reopen = flagBool(args, "reopen");
44046
+ const reply = flagStr(args, "reply")?.trim();
44047
+ if (reply)
44048
+ designServer.replyToThread(ctx.designDir, thread.id, reply, cliAuthor(args));
44049
+ designServer.resolveThread(ctx.designDir, thread.id, !reopen);
44050
+ log(`${c.green("✓")} #${thread.number} ${reopen ? "reopened" : "resolved"} — ${commentDelivery(ctx)}`);
43990
44051
  }, threadRef = (thread) => thread.number === undefined ? "new" : `#${thread.number}`, reportMirror = (ctx, summary, open) => {
43991
44052
  const parts = [
43992
44053
  summary.added ? `+${summary.added} new` : null,
@@ -44011,7 +44072,7 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
44011
44072
  note("Read them with: lecodes design comments");
44012
44073
  }, flushDesignComments = async (args) => {
44013
44074
  const ctx = getContext(args);
44014
- if (!existsSync18(ctx.designDir))
44075
+ if (!existsSync19(ctx.designDir))
44015
44076
  return null;
44016
44077
  const platform = optionalPlatform(ctx);
44017
44078
  if (!platform)
@@ -44024,6 +44085,8 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
44024
44085
  }
44025
44086
  if (typeof designServer.queuedThreads !== "function")
44026
44087
  return null;
44088
+ if (typeof designServer.settleQueued !== "function" || designServer.settleQueued.length < 4)
44089
+ return null;
44027
44090
  const queued = designServer.queuedThreads(ctx.designDir);
44028
44091
  if (queued.length === 0)
44029
44092
  return null;
@@ -44031,8 +44094,9 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
44031
44094
  const result = { sent: 0, threads: 0, failures: [] };
44032
44095
  for (const item of queued) {
44033
44096
  const { thread, pending } = item;
44097
+ let upstream = null;
44098
+ const delivered = { messageIds: new Set, resolve: false };
44034
44099
  try {
44035
- let upstream = null;
44036
44100
  if (pending.newThread) {
44037
44101
  const [opening, ...rest] = thread.messages;
44038
44102
  if (!opening)
@@ -44043,28 +44107,31 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
44043
44107
  origin: opening.origin
44044
44108
  });
44045
44109
  result.sent++;
44110
+ delivered.messageIds.add(opening.id);
44046
44111
  for (const message of rest) {
44047
44112
  upstream = await replyDesignComment(apiUrl, token, uuid, upstream.id, message.body, message.origin);
44048
44113
  result.sent++;
44114
+ delivered.messageIds.add(message.id);
44049
44115
  }
44050
44116
  } else {
44051
44117
  for (const message of pending.messages) {
44052
44118
  upstream = await replyDesignComment(apiUrl, token, uuid, thread.id, message.body, message.origin);
44053
44119
  result.sent++;
44120
+ delivered.messageIds.add(message.id);
44054
44121
  }
44055
44122
  }
44056
44123
  if (pending.resolve) {
44057
- const id = upstream?.id ?? thread.id;
44058
- upstream = await resolveDesignComment(apiUrl, token, uuid, id, thread.resolved);
44124
+ upstream = await resolveDesignComment(apiUrl, token, uuid, upstream?.id ?? thread.id, thread.resolved);
44059
44125
  result.sent++;
44126
+ delivered.resolve = true;
44060
44127
  }
44061
- if (!upstream)
44062
- continue;
44063
- designServer.settleQueued(ctx.designDir, item, upstream);
44064
- result.threads++;
44065
44128
  } catch (e2) {
44066
44129
  result.failures.push(`${threadRef(thread)}: ${errText(e2)}`);
44067
44130
  }
44131
+ if (upstream) {
44132
+ designServer.settleQueued(ctx.designDir, item, upstream, delivered);
44133
+ result.threads++;
44134
+ }
44068
44135
  }
44069
44136
  return result;
44070
44137
  }, COMMENT_SYNC_MS = 20000, startCommentSync = (ctx, designServer) => {
@@ -45146,7 +45213,7 @@ var require_main = __commonJS((exports, module) => {
45146
45213
  // package.json
45147
45214
  var package_default = {
45148
45215
  name: "lecodes-cli",
45149
- version: "0.12.0",
45216
+ version: "0.13.1",
45150
45217
  dependencies: {
45151
45218
  "@letary/chisel": "^0.6.0",
45152
45219
  jimp: "^1.6.1"
@@ -45159,9 +45226,9 @@ var package_default = {
45159
45226
  sdk: "workspace:*"
45160
45227
  },
45161
45228
  peerDependencies: {
45162
- "lecodes-design": "^0.4.0",
45163
- "lecodes-renderer": "^0.7.1",
45164
- "lecodes-3d-editor": "^0.1.0"
45229
+ "lecodes-design": "^0.5.0",
45230
+ "lecodes-renderer": "^0.8.0",
45231
+ "lecodes-3d-editor": "^0.2.0"
45165
45232
  },
45166
45233
  bin: {
45167
45234
  lecodes: "dist/index.js"
@@ -45336,6 +45403,23 @@ var compile2 = async (args) => {
45336
45403
  };
45337
45404
  var compileShell = (args, shell) => compileImpl(args, shell);
45338
45405
  var compileImpl = async (args, shell) => {
45406
+ const { js, resources } = await buildBundle(args, shell);
45407
+ const out = flagStr(args, "o", "out");
45408
+ if (out && isNullSink(out)) {
45409
+ note(`Discarded output (${out})`);
45410
+ } else if (out) {
45411
+ writeFileSync7(resolve(out), js);
45412
+ note(`Wrote ${out} (${js.length} bytes)`);
45413
+ } else {
45414
+ process.stdout.write(js);
45415
+ if (!js.endsWith(`
45416
+ `))
45417
+ process.stdout.write(`
45418
+ `);
45419
+ }
45420
+ return resources;
45421
+ };
45422
+ var buildBundle = async (args, shell) => {
45339
45423
  let root;
45340
45424
  let manifest;
45341
45425
  try {
@@ -45412,25 +45496,13 @@ var compileImpl = async (args, shell) => {
45412
45496
  });
45413
45497
  for (const w of warnings)
45414
45498
  warnErr(w);
45415
- const out = flagStr(args, "o", "out");
45416
- if (out && isNullSink(out)) {
45417
- note(`Discarded output (${out})`);
45418
- } else if (out) {
45419
- writeFileSync7(resolve(out), js);
45420
- note(`Wrote ${out} (${js.length} bytes)`);
45421
- } else {
45422
- process.stdout.write(js);
45423
- if (!js.endsWith(`
45424
- `))
45425
- process.stdout.write(`
45426
- `);
45427
- }
45428
- return entries.filter((e) => e.type === "resource").map((e) => ({
45499
+ const resources = entries.filter((e) => e.type === "resource").map((e) => ({
45429
45500
  path: e.path.replace(/^\//, ""),
45430
45501
  name: e.path.split(/[\\/]/).pop() ?? e.path,
45431
45502
  absPath: e.absPath,
45432
45503
  remote: !!e.remote
45433
45504
  }));
45505
+ return { js, resources };
45434
45506
  };
45435
45507
  var previewLegacy = async (args) => {
45436
45508
  warnErr("'lecodes preview' is now 'lecodes compile' — the old name still works.");
@@ -48740,6 +48812,10 @@ from \`main.ts\`.
48740
48812
  \`\`\`
48741
48813
  lecodes render # semantic JSON of the launched UI (structure, text, names, values)
48742
48814
  lecodes render --png shot.png # pixels (needs @napi-rs/canvas installed)
48815
+ lecodes render --desktop --png shot.png # NATIVE render: real Filament 3D + UI + 2D composited —
48816
+ # use this to SEE 3D scenes / procedural geometry (headless
48817
+ # renders record 3D as JSON only). Windows/Linux; downloads
48818
+ # the renderer on first use. --frames <n> for slow loads.
48743
48819
  lecodes render --script <file> # drive the app through a scenario (format below)
48744
48820
  lecodes test # all tests/*.flow.json + design-derived flows; run before "done"
48745
48821
  \`\`\`
@@ -48829,21 +48905,290 @@ var create = async (args) => {
48829
48905
  info(c.dim(` cd ${dir} — edit main.ts, then \`lecodes push -m "first commit"\` to save it`));
48830
48906
  };
48831
48907
 
48908
+ // src/commands/desktop.ts
48909
+ init_util();
48910
+
48911
+ // src/desktopRenderer.ts
48912
+ init_util();
48913
+ import { createHash as createHash5 } from "node:crypto";
48914
+ import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
48915
+ 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";
48916
+ import { homedir as homedir6, tmpdir } from "node:os";
48917
+ import { join as join18, resolve as resolve7 } from "node:path";
48918
+ var MANIFEST_URL = "https://storage.yandexcloud.net/le-codes/releases/versions.json";
48919
+ var DESKTOP_RENDERER_VERSION = "1.1.0";
48920
+ var semverNewer = (a2, b) => {
48921
+ const pa = a2.split(/[.-]/).map(Number), pb = b.split(/[.-]/).map(Number);
48922
+ for (let i2 = 0;i2 < 3; i2++)
48923
+ if ((pb[i2] ?? 0) !== (pa[i2] ?? 0))
48924
+ return (pb[i2] ?? 0) - (pa[i2] ?? 0);
48925
+ return 0;
48926
+ };
48927
+ var platformKey = () => {
48928
+ if (process.platform === "win32")
48929
+ return "windows-x64";
48930
+ if (process.platform === "linux")
48931
+ return "ubuntu-linux-x64";
48932
+ 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}.`);
48933
+ };
48934
+ var exeName = () => process.platform === "win32" ? "lecodes-desktop.exe" : "lecodes-desktop";
48935
+ var findExe = (dir) => {
48936
+ const name = exeName();
48937
+ for (const entry of readdirSync9(dir, { withFileTypes: true })) {
48938
+ const p2 = join18(dir, entry.name);
48939
+ if (entry.isFile() && entry.name === name)
48940
+ return p2;
48941
+ if (entry.isDirectory()) {
48942
+ const found = findExe(p2);
48943
+ if (found)
48944
+ return found;
48945
+ }
48946
+ }
48947
+ return null;
48948
+ };
48949
+ var cacheRoot = () => join18(homedir6(), ".lecodes", "renderer");
48950
+ var currentFile = () => join18(cacheRoot(), "current.json");
48951
+ var installedDesktopVersions = () => {
48952
+ try {
48953
+ 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);
48954
+ } catch {
48955
+ return [];
48956
+ }
48957
+ };
48958
+ var selectedDesktopVersion = () => {
48959
+ try {
48960
+ const saved = JSON.parse(readFileSync16(currentFile(), "utf8")).version;
48961
+ if (saved && /^\d+\.\d+\.\d+/.test(saved) && semverNewer(DESKTOP_RENDERER_VERSION, saved) > 0)
48962
+ return saved;
48963
+ } catch {}
48964
+ return DESKTOP_RENDERER_VERSION;
48965
+ };
48966
+ var fetchDesktopManifest = async () => {
48967
+ let manifest;
48968
+ try {
48969
+ const res = await fetch(MANIFEST_URL);
48970
+ if (!res.ok)
48971
+ throw new Error(`HTTP ${res.status}`);
48972
+ manifest = await res.json();
48973
+ } catch (e2) {
48974
+ throw new CliError(`Can't fetch the release manifest (${MANIFEST_URL}): ${e2 instanceof Error ? e2.message : String(e2)}`);
48975
+ }
48976
+ const desktop = manifest.products?.desktop;
48977
+ if (!desktop)
48978
+ throw new CliError("The release manifest has no desktop product.");
48979
+ return desktop;
48980
+ };
48981
+ var downloadDesktop = async (version, dest) => {
48982
+ const key = platformKey();
48983
+ const entry = (await fetchDesktopManifest()).versions.find((v) => v.version === version);
48984
+ const file = entry?.files[key];
48985
+ if (!file) {
48986
+ throw new CliError(`LeCodes Desktop ${version} [${key}] isn't published yet — \`--desktop\` needs it for the ` + `headless render contract.
48987
+ Set LECODES_DESKTOP_EXE to a locally built host, or update lecodes-cli.`);
48988
+ }
48989
+ note(`Downloading the native renderer (LeCodes Desktop ${version}, ${(file.size / 1024 / 1024).toFixed(1)} MB)…`);
48990
+ let bytes;
48991
+ try {
48992
+ const res = await fetch(file.url);
48993
+ if (!res.ok)
48994
+ throw new Error(`HTTP ${res.status}`);
48995
+ bytes = Buffer.from(await res.arrayBuffer());
48996
+ } catch (e2) {
48997
+ throw new CliError(`Download failed (${file.url}): ${e2 instanceof Error ? e2.message : String(e2)}`);
48998
+ }
48999
+ const sha = createHash5("sha256").update(bytes).digest("hex");
49000
+ if (sha !== file.sha256)
49001
+ throw new CliError(`Downloaded archive is corrupt (sha256 mismatch) — try again.`);
49002
+ mkdirSync13(cacheRoot(), { recursive: true });
49003
+ const stage = mkdtempSync(join18(cacheRoot(), "dl-"));
49004
+ try {
49005
+ const archive = join18(stage, file.url.split("/").pop() ?? "archive");
49006
+ writeFileSync14(archive, bytes);
49007
+ const tar = spawnSync3("tar", ["-xf", archive, "-C", stage], { stdio: ["ignore", "ignore", "pipe"] });
49008
+ if (tar.status !== 0)
49009
+ throw new CliError(`Couldn't extract the renderer archive: ${tar.stderr?.toString() ?? "tar failed"}`);
49010
+ rmSync4(archive);
49011
+ if (!findExe(stage))
49012
+ throw new CliError("The downloaded archive doesn't contain the desktop host binary.");
49013
+ try {
49014
+ renameSync2(stage, dest);
49015
+ } catch {
49016
+ if (!findExe(dest))
49017
+ throw new CliError(`Couldn't move the renderer into ${dest}.`);
49018
+ }
49019
+ } finally {
49020
+ rmSync4(stage, { recursive: true, force: true });
49021
+ }
49022
+ };
49023
+ var resolveDesktopExe = async () => {
49024
+ const override = process.env.LECODES_DESKTOP_EXE;
49025
+ if (override) {
49026
+ if (!existsSync18(override))
49027
+ throw new CliError(`LECODES_DESKTOP_EXE points at "${override}" which doesn't exist.`);
49028
+ return resolve7(override);
49029
+ }
49030
+ const version = selectedDesktopVersion();
49031
+ const dir = join18(cacheRoot(), `desktop-${version}`);
49032
+ let exe = existsSync18(dir) ? findExe(dir) : null;
49033
+ if (!exe) {
49034
+ await downloadDesktop(version, dir);
49035
+ exe = findExe(dir);
49036
+ if (!exe)
49037
+ throw new CliError(`Renderer cache at ${dir} is broken — delete the folder and retry.`);
49038
+ }
49039
+ return exe;
49040
+ };
49041
+ var updateDesktop = async () => {
49042
+ const { latest } = await fetchDesktopManifest();
49043
+ if (semverNewer(latest, DESKTOP_RENDERER_VERSION) > 0) {
49044
+ 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.`);
49045
+ }
49046
+ const before = selectedDesktopVersion();
49047
+ const dir = join18(cacheRoot(), `desktop-${latest}`);
49048
+ const hadIt = existsSync18(dir) && findExe(dir) !== null;
49049
+ if (!hadIt)
49050
+ await downloadDesktop(latest, dir);
49051
+ mkdirSync13(cacheRoot(), { recursive: true });
49052
+ writeFileSync14(currentFile(), JSON.stringify({ version: latest }, null, 2));
49053
+ if (latest === before && hadIt)
49054
+ note(`Already up to date: LeCodes Desktop ${latest}.`);
49055
+ else if (latest === before)
49056
+ note(`Re-downloaded LeCodes Desktop ${latest}.`);
49057
+ else
49058
+ note(`Desktop renderer set to ${latest} (was ${before}).`);
49059
+ };
49060
+ var stageBundle = (js, resources) => {
49061
+ const runDir = mkdtempSync(join18(tmpdir(), "lecodes-render-"));
49062
+ writeFileSync14(join18(runDir, "app.js"), js);
49063
+ for (const r2 of resources) {
49064
+ if (r2.absPath && existsSync18(r2.absPath) && statSync5(r2.absPath).isFile()) {
49065
+ copyFileSync(r2.absPath, join18(runDir, r2.name));
49066
+ }
49067
+ }
49068
+ return runDir;
49069
+ };
49070
+ var runDesktopApp = async (js, resources, opts = {}) => {
49071
+ const exe = await resolveDesktopExe();
49072
+ const runDir = stageBundle(js, resources);
49073
+ try {
49074
+ const env = { ...process.env };
49075
+ if (opts.width && opts.height)
49076
+ env.CREATOR_WINDOW_SIZE = `${opts.width}x${opts.height}`;
49077
+ await new Promise((done, fail) => {
49078
+ const child = spawn2(exe, [join18(runDir, "app.js")], { cwd: runDir, env, stdio: ["ignore", "inherit", "inherit"] });
49079
+ child.on("error", (e2) => fail(new CliError(`Can't run the desktop host: ${e2.message}`)));
49080
+ child.on("exit", (code) => {
49081
+ if (code === 0 || code === null)
49082
+ done();
49083
+ else
49084
+ fail(new CliError(`The desktop host exited with code ${code}.`));
49085
+ });
49086
+ });
49087
+ } finally {
49088
+ rmSync4(runDir, { recursive: true, force: true });
49089
+ }
49090
+ };
49091
+ var runDesktopRender = async (js, resources, opts) => {
49092
+ const exe = await resolveDesktopExe();
49093
+ const runDir = stageBundle(js, resources);
49094
+ try {
49095
+ const appJs = join18(runDir, "app.js");
49096
+ const outAbs = resolve7(opts.outPng);
49097
+ await new Promise((done, fail) => {
49098
+ const child = spawn2(exe, [appJs], {
49099
+ cwd: runDir,
49100
+ env: {
49101
+ ...process.env,
49102
+ CREATOR_DUMP_PNG: outAbs,
49103
+ CREATOR_MAX_FRAMES: String(opts.frames),
49104
+ CREATOR_WINDOW_SIZE: `${opts.width}x${opts.height}`,
49105
+ CREATOR_HIDDEN: "1"
49106
+ },
49107
+ stdio: ["ignore", "pipe", "pipe"]
49108
+ });
49109
+ const tail = [];
49110
+ const sink = (d) => {
49111
+ const text = String(d);
49112
+ tail.push(text);
49113
+ while (tail.length > 80)
49114
+ tail.shift();
49115
+ if (opts.logs)
49116
+ process.stderr.write(text);
49117
+ };
49118
+ child.stdout.on("data", sink);
49119
+ child.stderr.on("data", sink);
49120
+ const killer = setTimeout(() => {
49121
+ child.kill();
49122
+ fail(new CliError(`The native render timed out after ${opts.timeoutMs}ms (raise --timeout, or lower --frames).`));
49123
+ }, opts.timeoutMs);
49124
+ child.on("error", (e2) => {
49125
+ clearTimeout(killer);
49126
+ fail(new CliError(`Can't run the desktop renderer: ${e2.message}`));
49127
+ });
49128
+ child.on("exit", (code) => {
49129
+ clearTimeout(killer);
49130
+ if (code === 0 && existsSync18(outAbs))
49131
+ return done();
49132
+ const detail = tail.length > 0 ? `
49133
+ ${tail.join("").trimEnd()}` : "";
49134
+ 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}`));
49135
+ });
49136
+ });
49137
+ } finally {
49138
+ rmSync4(runDir, { recursive: true, force: true });
49139
+ }
49140
+ };
49141
+
49142
+ // src/commands/desktop.ts
49143
+ var run = async (args) => {
49144
+ const { js, resources } = await buildBundle(args);
49145
+ const width = Number(flagStr(args, "width")) || undefined;
49146
+ const height = Number(flagStr(args, "height")) || undefined;
49147
+ note("Launching LeCodes Desktop… (close the window to return)");
49148
+ await runDesktopApp(js, resources, { width, height });
49149
+ };
49150
+ var status = async () => {
49151
+ const selected = selectedDesktopVersion();
49152
+ const installed = installedDesktopVersions();
49153
+ log(`renders with ${selected}${installed.includes(selected) ? "" : c.dim(" (not downloaded yet — fetched on first render)")}`);
49154
+ log(`CLI minimum ${DESKTOP_RENDERER_VERSION}`);
49155
+ log(`downloaded ${installed.length > 0 ? installed.join(", ") : c.dim("none")} ${c.dim("(~/.lecodes/renderer)")}`);
49156
+ if (process.env.LECODES_DESKTOP_EXE)
49157
+ log(`override LECODES_DESKTOP_EXE=${process.env.LECODES_DESKTOP_EXE}`);
49158
+ try {
49159
+ const { latest } = await fetchDesktopManifest();
49160
+ const hint = semverNewer(selected, latest) > 0 ? c.yellow("(newer — run `lecodes desktop update`)") : latest === selected ? c.dim("(up to date)") : c.dim("(older than the CLI's minimum — this desktop release isn't published yet)");
49161
+ log(`published ${latest} ${hint}`);
49162
+ } catch {
49163
+ log(`published ${c.dim("unknown (couldn't reach the release manifest)")}`);
49164
+ }
49165
+ };
49166
+ var desktop = async (args) => {
49167
+ const sub = args._[0];
49168
+ if (sub === undefined)
49169
+ return status();
49170
+ if (sub === "run")
49171
+ return run(args);
49172
+ if (sub === "update")
49173
+ return updateDesktop();
49174
+ throw new CliError(`Unknown subcommand "desktop ${sub}". Use \`lecodes desktop\` (status), \`lecodes desktop run\` or \`lecodes desktop update\`.`);
49175
+ };
49176
+
48832
49177
  // src/index.ts
48833
49178
  init_design();
48834
49179
 
48835
49180
  // src/commands/dev.ts
48836
49181
  init_manifest();
48837
49182
  init_ignore();
48838
- import { existsSync as existsSync19, mkdirSync as mkdirSync14, readFileSync as readFileSync18, watch, writeFileSync as writeFileSync15 } from "node:fs";
49183
+ import { existsSync as existsSync20, mkdirSync as mkdirSync15, readFileSync as readFileSync19, watch, writeFileSync as writeFileSync16 } from "node:fs";
48839
49184
  import { randomBytes as randomBytes2 } from "node:crypto";
48840
49185
  import { networkInterfaces } from "node:os";
48841
- import { basename as basename8, join as join19 } from "node:path";
49186
+ import { basename as basename8, join as join20 } from "node:path";
48842
49187
 
48843
49188
  // src/dev/devServer.ts
48844
49189
  init_localFiles();
48845
49190
  import { createServer as createServer2 } from "node:http";
48846
- import { createReadStream, statSync as statSync5 } from "node:fs";
49191
+ import { createReadStream, statSync as statSync6 } from "node:fs";
48847
49192
  init_projectCompile();
48848
49193
  init_fonts();
48849
49194
  init_assetIcons();
@@ -48961,9 +49306,9 @@ var renderPrelude = (p2) => DEV_PRELUDE_JS.split("__DEV_ORIGIN__").join(p2.origi
48961
49306
  var renderDevScreen = (name) => DEV_SCREEN_TS.split("__DEV_NAME__").join(JSON.stringify(name));
48962
49307
 
48963
49308
  // src/dev/wsServer.ts
48964
- import { createHash as createHash5 } from "node:crypto";
49309
+ import { createHash as createHash6 } from "node:crypto";
48965
49310
  var WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
48966
- var acceptKey = (key) => createHash5("sha1").update(key + WS_GUID).digest("base64");
49311
+ var acceptKey = (key) => createHash6("sha1").update(key + WS_GUID).digest("base64");
48967
49312
  var encodeTextFrame = (text) => {
48968
49313
  const payload = Buffer.from(text, "utf8");
48969
49314
  const len = payload.length;
@@ -49106,19 +49451,19 @@ var contentType = (path2) => {
49106
49451
  const i2 = path2.lastIndexOf(".");
49107
49452
  return i2 >= 0 && MIME[path2.slice(i2 + 1).toLowerCase()] || "application/octet-stream";
49108
49453
  };
49109
- var sendText = (res, status, type, body) => {
49110
- res.writeHead(status, { "content-type": type, "cache-control": "no-store" });
49454
+ var sendText = (res, status2, type, body) => {
49455
+ res.writeHead(status2, { "content-type": type, "cache-control": "no-store" });
49111
49456
  res.end(body);
49112
49457
  };
49113
- var sendJson = (res, status, body) => sendText(res, status, "application/json", JSON.stringify(body));
49114
- var readBody = (req) => new Promise((resolve7) => {
49458
+ var sendJson = (res, status2, body) => sendText(res, status2, "application/json", JSON.stringify(body));
49459
+ var readBody = (req) => new Promise((resolve8) => {
49115
49460
  let body = "";
49116
49461
  req.on("data", (c4) => {
49117
49462
  if (body.length < 256 * 1024)
49118
49463
  body += c4.toString("utf8");
49119
49464
  });
49120
- req.on("end", () => resolve7(body));
49121
- req.on("error", () => resolve7(body));
49465
+ req.on("end", () => resolve8(body));
49466
+ req.on("error", () => resolve8(body));
49122
49467
  });
49123
49468
  var startDevServer = async (options) => {
49124
49469
  const { root, manifest, name, origin, token, entryOverride } = options;
@@ -49158,9 +49503,9 @@ var startDevServer = async (options) => {
49158
49503
  }
49159
49504
  };
49160
49505
  const recompile = () => {
49161
- const run = compileChain.then(compileOnce);
49162
- compileChain = run;
49163
- return run;
49506
+ const run2 = compileChain.then(compileOnce);
49507
+ compileChain = run2;
49508
+ return run2;
49164
49509
  };
49165
49510
  const devJs = await compileProject2({
49166
49511
  entries: [{ path: "/.dev-entry.ts", type: "text", text: renderDevScreen(name) }],
@@ -49218,7 +49563,7 @@ var startDevServer = async (options) => {
49218
49563
  return sendText(res, 400, "text/plain", "Bad path");
49219
49564
  }
49220
49565
  const abs = resolveLocalPath(root, key);
49221
- if (!abs || !statSync5(abs).isFile())
49566
+ if (!abs || !statSync6(abs).isFile())
49222
49567
  return sendText(res, 404, "text/plain", "Not found");
49223
49568
  res.writeHead(200, { "content-type": contentType(key), "cache-control": "no-store" });
49224
49569
  createReadStream(abs).pipe(res);
@@ -49261,11 +49606,11 @@ var startDevServer = async (options) => {
49261
49606
  if (conn)
49262
49607
  clients.add(conn);
49263
49608
  });
49264
- await new Promise((resolve7, reject) => {
49609
+ await new Promise((resolve8, reject) => {
49265
49610
  server.once("error", reject);
49266
49611
  server.listen(options.port, options.bind ?? "0.0.0.0", () => {
49267
49612
  server.removeListener("error", reject);
49268
- resolve7();
49613
+ resolve8();
49269
49614
  });
49270
49615
  });
49271
49616
  const address = server.address();
@@ -49291,35 +49636,46 @@ var DEFAULT_PORT2 = 4499;
49291
49636
  var DEBOUNCE_MS = 120;
49292
49637
  var WATCH_SKIP_DIRS = new Set([".lecodes", ".git", "node_modules"]);
49293
49638
  var WATCH_SKIP_FILES = new Set(["tsconfig.json", "jsconfig.json", ".gitignore", ".lecodesignore", ".DS_Store"]);
49294
- var pickLanAddress = () => {
49639
+ var VIRTUAL_IFACE = /wsl|docker|hyper-v|vethernet|virtualbox|vmware|tap|tun|zerotier|tailscale|loopback/i;
49640
+ var lanCandidates = () => {
49295
49641
  const nets = networkInterfaces();
49296
- for (const name of Object.keys(nets)) {
49297
- for (const net of nets[name] ?? []) {
49298
- if (net.family === "IPv4" && !net.internal)
49299
- return net.address;
49300
- }
49301
- }
49302
- return null;
49642
+ const out = [];
49643
+ for (const iface of Object.keys(nets)) {
49644
+ for (const net of nets[iface] ?? []) {
49645
+ if (net.family !== "IPv4" || net.internal)
49646
+ continue;
49647
+ const a2 = net.address;
49648
+ let rank = 0;
49649
+ if (VIRTUAL_IFACE.test(iface))
49650
+ rank += 4;
49651
+ if (/^172\.(1[6-9]|2\d|3[01])\./.test(a2))
49652
+ rank += 2;
49653
+ if (a2.startsWith("169.254."))
49654
+ rank += 8;
49655
+ out.push({ address: a2, iface, rank });
49656
+ }
49657
+ }
49658
+ return out.sort((x2, y2) => x2.rank - y2.rank).map(({ address, iface }) => ({ address, iface }));
49303
49659
  };
49304
49660
  var loadDevToken = (root) => {
49305
- const path2 = join19(root, LECODES_DIR, "dev.json");
49661
+ const path2 = join20(root, LECODES_DIR, "dev.json");
49306
49662
  try {
49307
- const token2 = JSON.parse(readFileSync18(path2, "utf8")).token;
49663
+ const token2 = JSON.parse(readFileSync19(path2, "utf8")).token;
49308
49664
  if (token2 && /^[a-z0-9]{8,}$/.test(token2))
49309
49665
  return token2;
49310
49666
  } catch {}
49311
49667
  const token = randomBytes2(6).toString("hex");
49312
- mkdirSync14(join19(root, LECODES_DIR), { recursive: true });
49313
- writeFileSync15(path2, JSON.stringify({ token }, null, 2) + `
49668
+ mkdirSync15(join20(root, LECODES_DIR), { recursive: true });
49669
+ writeFileSync16(path2, JSON.stringify({ token }, null, 2) + `
49314
49670
  `);
49315
49671
  return token;
49316
49672
  };
49317
49673
  var printQr = async (url) => {
49318
49674
  try {
49319
49675
  const qrcode = (await Promise.resolve().then(() => __toESM(require_main(), 1))).default;
49320
- await new Promise((resolve7) => qrcode.generate(url, { small: true }, (out) => {
49676
+ await new Promise((resolve8) => qrcode.generate(url, { small: true }, (out) => {
49321
49677
  log(out);
49322
- resolve7();
49678
+ resolve8();
49323
49679
  }));
49324
49680
  } catch {
49325
49681
  note("(couldn't render a QR code — open the URL below on the device)");
@@ -49340,9 +49696,11 @@ var dev = async (args) => {
49340
49696
  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).");
49341
49697
  }
49342
49698
  const port = Number(flagStr(args, "port")) || DEFAULT_PORT2;
49343
- const host = flagStr(args, "host") ?? pickLanAddress();
49699
+ const candidates = lanCandidates();
49700
+ const host = flagStr(args, "host") ?? candidates[0]?.address;
49344
49701
  if (!host)
49345
49702
  throw new CliError("No LAN address found — pass one with --host <ip>.");
49703
+ const alternatives = candidates.filter((c4) => c4.address !== host);
49346
49704
  const origin = `http://${host}:${port}`;
49347
49705
  const token = loadDevToken(root);
49348
49706
  const server = await startDevServer({
@@ -49422,7 +49780,10 @@ ${result.error}`);
49422
49780
  await printQr(server.url);
49423
49781
  log(`Scan the QR with the LeCodes app, or open: ${c.bold(server.url)}`);
49424
49782
  note(`Older desktop builds (no websocket bridge): ${server.url}?t=poll`);
49425
- if (existsSync19(join19(root, LECODES_DIR, "manifest.json")))
49783
+ if (alternatives.length > 0) {
49784
+ note(`Device can't reach it? This machine also has ${alternatives.map((a2) => `${a2.address} (${a2.iface})`).join(", ")} — retry with --host <ip>.`);
49785
+ }
49786
+ if (existsSync20(join20(root, LECODES_DIR, "manifest.json")))
49426
49787
  note(`Token stored in ${LECODES_DIR}/dev.json (delete it to rotate the URL).`);
49427
49788
  log("");
49428
49789
  log(`Watching ${root} — save a file to hot-reload. Ctrl+C to stop.`);
@@ -49436,7 +49797,7 @@ init_localFiles();
49436
49797
 
49437
49798
  // src/serverDiff.ts
49438
49799
  init_localFiles();
49439
- import { readFileSync as readFileSync19 } from "node:fs";
49800
+ import { readFileSync as readFileSync20 } from "node:fs";
49440
49801
  import { basename as basename9 } from "node:path";
49441
49802
  var isTextAsset = (t2) => t2 === "text" || t2 === "shader" || t2 === "scene";
49442
49803
  var computeServerDiff = (project, scan2) => {
@@ -49455,7 +49816,7 @@ var computeServerDiff = (project, scan2) => {
49455
49816
  const local = localByPath.get(path2);
49456
49817
  if (local && !server) {
49457
49818
  const text = isTextKind(classifyKind(basename9(path2)));
49458
- entries.push({ path: path2, status: "added", binary: !text, serverText: "", localText: text ? readFileSync19(local.absPath, "utf8") : undefined });
49819
+ entries.push({ path: path2, status: "added", binary: !text, serverText: "", localText: text ? readFileSync20(local.absPath, "utf8") : undefined });
49459
49820
  continue;
49460
49821
  }
49461
49822
  if (server && !local) {
@@ -49466,12 +49827,12 @@ var computeServerDiff = (project, scan2) => {
49466
49827
  if (server && local) {
49467
49828
  if (isTextAsset(server.type)) {
49468
49829
  const serverText = server.text ?? "";
49469
- const localText = readFileSync19(local.absPath, "utf8");
49830
+ const localText = readFileSync20(local.absPath, "utf8");
49470
49831
  if (serverText !== localText)
49471
49832
  entries.push({ path: path2, status: "modified", binary: false, serverText, localText });
49472
49833
  } else {
49473
49834
  const serverSize = server.file?.size;
49474
- const localSize = Buffer.byteLength(readFileSync19(local.absPath));
49835
+ const localSize = Buffer.byteLength(readFileSync20(local.absPath));
49475
49836
  if (serverSize !== undefined && serverSize !== localSize) {
49476
49837
  entries.push({ path: path2, status: "modified", binary: true });
49477
49838
  }
@@ -49629,8 +49990,8 @@ var statusLabel = (e2) => e2.status === "added" ? c.green(" added ") : e2.st
49629
49990
  init_manifest();
49630
49991
  init_types4();
49631
49992
  init_ignore();
49632
- import { existsSync as existsSync20, mkdirSync as mkdirSync15, writeFileSync as writeFileSync16 } from "node:fs";
49633
- import { basename as basename10, dirname as dirname13, join as join20, resolve as resolve7 } from "node:path";
49993
+ import { existsSync as existsSync21, mkdirSync as mkdirSync16, writeFileSync as writeFileSync17 } from "node:fs";
49994
+ import { basename as basename10, dirname as dirname13, join as join21, resolve as resolve8 } from "node:path";
49634
49995
  init_util();
49635
49996
  var STARTER2 = `// Welcome to LeCodes — the whole SDK is available as globals, no imports needed.
49636
49997
  // Run it live on a device with \`lecodes dev\`, or bundle it with \`lecodes compile -o app.js\`.
@@ -49728,18 +50089,18 @@ Full command list: \`lecodes --help\`.
49728
50089
  `;
49729
50090
  var init2 = async (args) => {
49730
50091
  const dirArg = (args._[0] ?? ".").replace(/[/\\]+$/, "") || ".";
49731
- const root = resolve7(dirArg);
50092
+ const root = resolve8(dirArg);
49732
50093
  const name = basename10(root);
49733
- mkdirSync15(root, { recursive: true });
50094
+ mkdirSync16(root, { recursive: true });
49734
50095
  let created = 0;
49735
50096
  const write = (rel, content) => {
49736
- const abs = join20(root, rel);
49737
- if (existsSync20(abs)) {
50097
+ const abs = join21(root, rel);
50098
+ if (existsSync21(abs)) {
49738
50099
  note(`kept ${rel}`);
49739
50100
  return;
49740
50101
  }
49741
- mkdirSync15(dirname13(abs), { recursive: true });
49742
- writeFileSync16(abs, content);
50102
+ mkdirSync16(dirname13(abs), { recursive: true });
50103
+ writeFileSync17(abs, content);
49743
50104
  log(`${c.green("created")} ${rel}`);
49744
50105
  created++;
49745
50106
  };
@@ -49768,8 +50129,8 @@ init_config();
49768
50129
  init_ignore();
49769
50130
  init_localFiles();
49770
50131
  init_manifest();
49771
- import { existsSync as existsSync21 } from "node:fs";
49772
- import { basename as basename12, join as join21, resolve as resolve8 } from "node:path";
50132
+ import { existsSync as existsSync22 } from "node:fs";
50133
+ import { basename as basename12, join as join22, resolve as resolve9 } from "node:path";
49773
50134
  init_types4();
49774
50135
  init_util();
49775
50136
 
@@ -49779,7 +50140,7 @@ init_config();
49779
50140
  init_manifest();
49780
50141
  init_localFiles();
49781
50142
  import { basename as basename11 } from "node:path";
49782
- import { readFileSync as readFileSync20 } from "node:fs";
50143
+ import { readFileSync as readFileSync21 } from "node:fs";
49783
50144
  init_util();
49784
50145
  var push = async (args) => {
49785
50146
  const root = findProjectRoot(process.cwd());
@@ -49840,9 +50201,9 @@ var push = async (args) => {
49840
50201
  if (source)
49841
50202
  entry.assetId = source.id;
49842
50203
  if (isTextKind(kind)) {
49843
- entry.text = readFileSync20(file.absPath, "utf8");
50204
+ entry.text = readFileSync21(file.absPath, "utf8");
49844
50205
  } else if (!source || source.sha !== file.sha) {
49845
- const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename11(file.path), readFileSync20(file.absPath));
50206
+ const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename11(file.path), readFileSync21(file.absPath));
49846
50207
  entry.blobFileId = blob.id;
49847
50208
  uploaded++;
49848
50209
  }
@@ -49949,10 +50310,10 @@ var link = async (args) => {
49949
50310
  const apiUrl = requireApiUrl(config);
49950
50311
  const token = requireToken(config);
49951
50312
  const dirArg = args._[0];
49952
- const root = dirArg ? resolve8(dirArg) : process.cwd();
49953
- if (!existsSync21(root))
50313
+ const root = dirArg ? resolve9(dirArg) : process.cwd();
50314
+ if (!existsSync22(root))
49954
50315
  throw new CliError(`No such directory: ${root}`);
49955
- if (existsSync21(join21(root, LECODES_DIR, "manifest.json")) && !flagBool(args, "force")) {
50316
+ if (existsSync22(join22(root, LECODES_DIR, "manifest.json")) && !flagBool(args, "force")) {
49956
50317
  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.");
49957
50318
  }
49958
50319
  let enclosing = null;
@@ -50028,8 +50389,8 @@ var login = async (args) => {
50028
50389
  };
50029
50390
 
50030
50391
  // src/commands/render.ts
50031
- import { mkdirSync as mkdirSync16, readFileSync as readFileSync21, writeFileSync as writeFileSync17 } from "node:fs";
50032
- import { dirname as dirname14, resolve as resolve9 } from "node:path";
50392
+ import { mkdirSync as mkdirSync17, readFileSync as readFileSync22, writeFileSync as writeFileSync18 } from "node:fs";
50393
+ import { dirname as dirname14, resolve as resolve10 } from "node:path";
50033
50394
 
50034
50395
  // src/compile/headlessBundle.ts
50035
50396
  init_api();
@@ -50139,9 +50500,9 @@ var requireRenderer = async () => {
50139
50500
  };
50140
50501
  var fileScenarioIO = (dir) => ({
50141
50502
  write: (relPath, data) => {
50142
- const abs = resolve9(dir, relPath);
50143
- mkdirSync16(dirname14(abs), { recursive: true });
50144
- writeFileSync17(abs, data);
50503
+ const abs = resolve10(dir, relPath);
50504
+ mkdirSync17(dirname14(abs), { recursive: true });
50505
+ writeFileSync18(abs, data);
50145
50506
  }
50146
50507
  });
50147
50508
  var resolveFixtures = (value, baseDir) => {
@@ -50149,9 +50510,9 @@ var resolveFixtures = (value, baseDir) => {
50149
50510
  return;
50150
50511
  let map = value;
50151
50512
  if (typeof value === "string") {
50152
- const abs = resolve9(baseDir, value);
50513
+ const abs = resolve10(baseDir, value);
50153
50514
  try {
50154
- map = JSON.parse(readFileSync21(abs, "utf8"));
50515
+ map = JSON.parse(readFileSync22(abs, "utf8"));
50155
50516
  } catch (e2) {
50156
50517
  throw new CliError(`Can't read fixtures "${value}": ${e2 instanceof Error ? e2.message : String(e2)}`);
50157
50518
  }
@@ -50164,7 +50525,7 @@ var resolveFixtures = (value, baseDir) => {
50164
50525
  var readScenarioFile = (path2) => {
50165
50526
  let raw;
50166
50527
  try {
50167
- raw = JSON.parse(readFileSync21(resolve9(path2), "utf8"));
50528
+ raw = JSON.parse(readFileSync22(resolve10(path2), "utf8"));
50168
50529
  } catch (e2) {
50169
50530
  throw new CliError(`Can't read scenario "${path2}": ${e2 instanceof Error ? e2.message : String(e2)}`);
50170
50531
  }
@@ -50177,7 +50538,27 @@ var readScenarioFile = (path2) => {
50177
50538
  throw new CliError(`Scenario "${path2}" has no "steps".`);
50178
50539
  return file;
50179
50540
  };
50541
+ var renderDesktop = async (args) => {
50542
+ const width = Number(flagStr(args, "width")) || 390;
50543
+ const height = Number(flagStr(args, "height")) || 844;
50544
+ const frames = Number(flagStr(args, "frames")) || 60;
50545
+ const timeoutMs = Number(flagStr(args, "timeout")) || 60000;
50546
+ const pngFlag = flagStr(args, "png") ?? "screenshot.png";
50547
+ const { js, resources } = await buildBundle(args);
50548
+ note("Rendering (native)…");
50549
+ await runDesktopRender(js, resources, {
50550
+ width,
50551
+ height,
50552
+ frames,
50553
+ outPng: pngFlag,
50554
+ logs: flagBool(args, "logs"),
50555
+ timeoutMs
50556
+ });
50557
+ note(`Wrote ${pngFlag} (${width}x${height}, native render)`);
50558
+ };
50180
50559
  var render = async (args) => {
50560
+ if (flagBool(args, "desktop"))
50561
+ return renderDesktop(args);
50181
50562
  const scriptFlag = flagStr(args, "script");
50182
50563
  const scenario = scriptFlag !== undefined ? readScenarioFile(scriptFlag) : null;
50183
50564
  const stepCount = scenario && Array.isArray(scenario.steps) ? scenario.steps.length : 0;
@@ -50194,7 +50575,7 @@ var render = async (args) => {
50194
50575
  const settleStr = flagStr(args, "settle") ?? (scenario?.settle !== undefined ? String(scenario.settle) : undefined);
50195
50576
  const timeStr = flagStr(args, "time");
50196
50577
  const onConsole = flagBool(args, "logs") ? (level, text2) => logErr(`${c.dim(`[${level}]`)} ${text2}`) : undefined;
50197
- const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname14(resolve9(scriptFlag)) : ".");
50578
+ const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname14(resolve10(scriptFlag)) : ".");
50198
50579
  const flagFixtures = resolveFixtures(flagStr(args, "fixtures"), ".");
50199
50580
  const fixtures = envelopeFixtures || flagFixtures ? { ...envelopeFixtures, ...flagFixtures } : undefined;
50200
50581
  if (fixtures) {
@@ -50233,7 +50614,7 @@ var render = async (args) => {
50233
50614
  ${c.red(st.error ?? "failed")}`);
50234
50615
  }
50235
50616
  if (!result2.ok && result2.failureArtifacts.length > 0) {
50236
- logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p2) => resolve9(outDir, p2)).join(", ")}`));
50617
+ logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p2) => resolve10(outDir, p2)).join(", ")}`));
50237
50618
  }
50238
50619
  logErr(result2.ok ? c.green(`✓ ${result2.steps.length}/${steps.length} steps passed`) : c.red(`✗ failed at step ${result2.steps.length} of ${steps.length}`));
50239
50620
  return done((cb) => cb(), result2.ok ? 0 : 1);
@@ -50255,7 +50636,7 @@ var render = async (args) => {
50255
50636
  if (isNullSink(pngFlag)) {
50256
50637
  note(`Discarded PNG (${pngFlag})`);
50257
50638
  } else {
50258
- writeFileSync17(resolve9(pngFlag), png2);
50639
+ writeFileSync18(resolve10(pngFlag), png2);
50259
50640
  note(`Wrote ${pngFlag} (${png2.length} bytes, ${width}x${height})`);
50260
50641
  }
50261
50642
  return done((cb) => cb());
@@ -50270,7 +50651,7 @@ var render = async (args) => {
50270
50651
  return done((cb) => cb());
50271
50652
  }
50272
50653
  if (out) {
50273
- writeFileSync17(resolve9(out), result.text);
50654
+ writeFileSync18(resolve10(out), result.text);
50274
50655
  note(`Wrote ${out} (${result.text.length} bytes)`);
50275
50656
  return done((cb) => cb());
50276
50657
  }
@@ -50285,8 +50666,8 @@ init_api();
50285
50666
  init_config();
50286
50667
  init_manifest();
50287
50668
  init_util();
50288
- import { appendFileSync as appendFileSync2, existsSync as existsSync22, readFileSync as readFileSync22, writeFileSync as writeFileSync18 } from "node:fs";
50289
- import { join as join22 } from "node:path";
50669
+ import { appendFileSync as appendFileSync2, existsSync as existsSync23, readFileSync as readFileSync23, writeFileSync as writeFileSync19 } from "node:fs";
50670
+ import { join as join23 } from "node:path";
50290
50671
  var ENV_VAR = "LECODES_PUSH_KEY";
50291
50672
  var context = () => {
50292
50673
  const root = findProjectRoot(process.cwd());
@@ -50430,11 +50811,11 @@ var pnKeysList = async (ctx) => {
50430
50811
  };
50431
50812
  var writeEnv = (root, key) => {
50432
50813
  const touched = [];
50433
- const envPath = join22(root, ".env");
50814
+ const envPath = join23(root, ".env");
50434
50815
  const line = `${ENV_VAR}=${key}`;
50435
- const existing = existsSync22(envPath) ? readFileSync22(envPath, "utf8") : "";
50816
+ const existing = existsSync23(envPath) ? readFileSync23(envPath, "utf8") : "";
50436
50817
  if (new RegExp(`^${ENV_VAR}=`, "m").test(existing)) {
50437
- writeFileSync18(envPath, existing.replace(new RegExp(`^${ENV_VAR}=.*$`, "m"), line));
50818
+ writeFileSync19(envPath, existing.replace(new RegExp(`^${ENV_VAR}=.*$`, "m"), line));
50438
50819
  } else {
50439
50820
  const separator = !existing || existing.endsWith(`
50440
50821
  `) ? "" : `
@@ -50443,8 +50824,8 @@ var writeEnv = (root, key) => {
50443
50824
  `);
50444
50825
  }
50445
50826
  touched.push(".env");
50446
- const ignorePath = join22(root, ".gitignore");
50447
- const ignore = existsSync22(ignorePath) ? readFileSync22(ignorePath, "utf8") : "";
50827
+ const ignorePath = join23(root, ".gitignore");
50828
+ const ignore = existsSync23(ignorePath) ? readFileSync23(ignorePath, "utf8") : "";
50448
50829
  if (!ignore.split(`
50449
50830
  `).some((l2) => l2.trim() === ".env")) {
50450
50831
  const separator = !ignore || ignore.endsWith(`
@@ -50543,8 +50924,8 @@ var pn = async (args) => {
50543
50924
  init_api();
50544
50925
  init_config();
50545
50926
  init_manifest();
50546
- import { existsSync as existsSync23, mkdirSync as mkdirSync17, rmSync as rmSync4, writeFileSync as writeFileSync19 } from "node:fs";
50547
- import { dirname as dirname15, join as join23 } from "node:path";
50927
+ import { existsSync as existsSync24, mkdirSync as mkdirSync18, rmSync as rmSync5, writeFileSync as writeFileSync20 } from "node:fs";
50928
+ import { dirname as dirname15, join as join24 } from "node:path";
50548
50929
  init_types4();
50549
50930
  init_localFiles();
50550
50931
  init_util();
@@ -50567,9 +50948,9 @@ var pull = async (args) => {
50567
50948
  const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a2) => a2.id === id)?.type !== "folder").map(([, path2]) => path2));
50568
50949
  for (const path2 of Object.keys(manifest.files)) {
50569
50950
  if (!remotePaths.has(path2)) {
50570
- const abs = join23(root, ...path2.replace(/^\//, "").split("/"));
50571
- if (existsSync23(abs))
50572
- rmSync4(abs);
50951
+ const abs = join24(root, ...path2.replace(/^\//, "").split("/"));
50952
+ if (existsSync24(abs))
50953
+ rmSync5(abs);
50573
50954
  }
50574
50955
  }
50575
50956
  const files = await materializeAssets(root, project, apiUrl, token, progress);
@@ -50606,18 +50987,18 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
50606
50987
  const files = { ...manifest.files };
50607
50988
  for (const asset of targets) {
50608
50989
  const key = pathById.get(asset.id);
50609
- const abs = join23(root, ...key.replace(/^\//, "").split("/"));
50610
- mkdirSync17(dirname15(abs), { recursive: true });
50990
+ const abs = join24(root, ...key.replace(/^\//, "").split("/"));
50991
+ mkdirSync18(dirname15(abs), { recursive: true });
50611
50992
  let sha;
50612
50993
  if (asset.type === "file") {
50613
50994
  if (!asset.file?.src)
50614
50995
  continue;
50615
50996
  const bytes = await downloadBlob(apiUrl, asset.file.src);
50616
- writeFileSync19(abs, bytes);
50997
+ writeFileSync20(abs, bytes);
50617
50998
  sha = sha256(bytes);
50618
50999
  } else {
50619
51000
  const text = asset.text ?? "";
50620
- writeFileSync19(abs, text);
51001
+ writeFileSync20(abs, text);
50621
51002
  sha = sha256(text);
50622
51003
  }
50623
51004
  files[key] = { id: asset.id, type: kindOf3(asset.type), sha };
@@ -50630,8 +51011,8 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
50630
51011
 
50631
51012
  // src/commands/scene.ts
50632
51013
  init_manifest();
50633
- import { existsSync as existsSync25, readFileSync as readFileSync24, readdirSync as readdirSync10, statSync as statSync6, writeFileSync as writeFileSync20 } from "node:fs";
50634
- import { basename as basename13, dirname as dirname16, join as join25, resolve as resolve10 } from "node:path";
51014
+ import { existsSync as existsSync26, readFileSync as readFileSync25, readdirSync as readdirSync11, statSync as statSync7, writeFileSync as writeFileSync21 } from "node:fs";
51015
+ import { basename as basename13, dirname as dirname16, join as join26, resolve as resolve11 } from "node:path";
50635
51016
 
50636
51017
  // src/compile/sceneCompile.ts
50637
51018
  init_localFiles();
@@ -50639,18 +51020,18 @@ init_ignore();
50639
51020
  init_distRoot();
50640
51021
  init_util();
50641
51022
  init_projectCompile();
50642
- import { existsSync as existsSync24, readFileSync as readFileSync23 } from "node:fs";
50643
- import { join as join24 } from "node:path";
51023
+ import { existsSync as existsSync25, readFileSync as readFileSync24 } from "node:fs";
51024
+ import { join as join25 } from "node:path";
50644
51025
  var TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i;
50645
51026
  var rel = (key) => key.replace(/^\//, "");
50646
51027
  var resolveHarness = (loadSceneHarness2) => {
50647
- const inRepo = join24(distRoot, "..", "projects", "src", "scene-editor");
51028
+ const inRepo = join25(distRoot, "..", "projects", "src", "scene-editor");
50648
51029
  try {
50649
- return loadSceneHarness2(existsSync24(inRepo) ? inRepo : undefined);
51030
+ return loadSceneHarness2(existsSync25(inRepo) ? inRepo : undefined);
50650
51031
  } catch {
50651
- const vendored = join24(distRoot, "runtime", "scene-harness.json");
50652
- if (existsSync24(vendored))
50653
- return JSON.parse(readFileSync23(vendored, "utf8"));
51032
+ const vendored = join25(distRoot, "runtime", "scene-harness.json");
51033
+ if (existsSync25(vendored))
51034
+ return JSON.parse(readFileSync24(vendored, "utf8"));
50654
51035
  throw new CliError("scene-editor harness sources not found — reinstall the CLI or set LECODES_SCENE_HARNESS_DIR.");
50655
51036
  }
50656
51037
  };
@@ -50662,7 +51043,7 @@ var compileSceneEditor = async (opts) => {
50662
51043
  for (const file of scan2.files) {
50663
51044
  const path2 = "./" + rel(file.path);
50664
51045
  if (TEXT_EXT.test(path2)) {
50665
- entries.push({ path: path2, type: "text", text: readFileSync23(file.absPath, "utf8") });
51046
+ entries.push({ path: path2, type: "text", text: readFileSync24(file.absPath, "utf8") });
50666
51047
  } else {
50667
51048
  entries.push({ path: path2, type: "resource", fileSrc: "/project/" + rel(file.path) });
50668
51049
  }
@@ -50704,10 +51085,10 @@ var getContext2 = (args) => {
50704
51085
  manifest = null;
50705
51086
  const pathArg = args._[0];
50706
51087
  if (pathArg) {
50707
- const abs = resolve10(process.cwd(), pathArg);
50708
- if (existsSync25(abs) && statSync6(abs).isDirectory())
51088
+ const abs = resolve11(process.cwd(), pathArg);
51089
+ if (existsSync26(abs) && statSync7(abs).isDirectory())
50709
51090
  root = abs;
50710
- else if (abs.endsWith(".scene.ts") && existsSync25(abs))
51091
+ else if (abs.endsWith(".scene.ts") && existsSync26(abs))
50711
51092
  root = dirname16(abs);
50712
51093
  else
50713
51094
  throw new CliError(`No such folder or .scene.ts: ${pathArg}`);
@@ -50716,12 +51097,12 @@ var getContext2 = (args) => {
50716
51097
  return { root, manifest, name: manifest?.name ?? basename13(root) };
50717
51098
  };
50718
51099
  var writeMcpConfig2 = (root, port) => {
50719
- const path2 = join25(root, ".mcp.json");
51100
+ const path2 = join26(root, ".mcp.json");
50720
51101
  const entry = { type: "http", url: `http://localhost:${port}/mcp` };
50721
51102
  let config = {};
50722
- if (existsSync25(path2)) {
51103
+ if (existsSync26(path2)) {
50723
51104
  try {
50724
- config = JSON.parse(readFileSync24(path2, "utf8"));
51105
+ config = JSON.parse(readFileSync25(path2, "utf8"));
50725
51106
  } catch {
50726
51107
  warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-scene server by hand.");
50727
51108
  return;
@@ -50734,17 +51115,17 @@ var writeMcpConfig2 = (root, port) => {
50734
51115
  return;
50735
51116
  }
50736
51117
  config.mcpServers["lecodes-scene"] = entry;
50737
- writeFileSync20(path2, JSON.stringify(config, null, 2) + `
51118
+ writeFileSync21(path2, JSON.stringify(config, null, 2) + `
50738
51119
  `);
50739
51120
  note(`${existing ? "updated" : "created"} .mcp.json (lecodes-scene MCP → ${entry.url})`);
50740
51121
  };
50741
51122
  var countScenes = (dir) => {
50742
51123
  let n2 = 0;
50743
- for (const name of readdirSync10(dir)) {
51124
+ for (const name of readdirSync11(dir)) {
50744
51125
  if (name === "node_modules" || name.startsWith("."))
50745
51126
  continue;
50746
- const full = join25(dir, name);
50747
- if (statSync6(full).isDirectory())
51127
+ const full = join26(dir, name);
51128
+ if (statSync7(full).isDirectory())
50748
51129
  n2 += countScenes(full);
50749
51130
  else if (name.endsWith(".scene.ts"))
50750
51131
  n2++;
@@ -50788,7 +51169,7 @@ var scene = async (args) => {
50788
51169
  init_manifest();
50789
51170
  init_localFiles();
50790
51171
  init_util();
50791
- var status = async (_args) => {
51172
+ var status2 = async (_args) => {
50792
51173
  const root = findProjectRoot(process.cwd());
50793
51174
  const manifest = readManifest(root);
50794
51175
  const scan2 = scanProject(root, manifest);
@@ -50817,8 +51198,8 @@ var status = async (_args) => {
50817
51198
  };
50818
51199
 
50819
51200
  // src/commands/test.ts
50820
- import { existsSync as existsSync26, readFileSync as readFileSync25, readdirSync as readdirSync11 } from "node:fs";
50821
- import { dirname as dirname17, resolve as resolve11 } from "node:path";
51201
+ import { existsSync as existsSync27, readFileSync as readFileSync26, readdirSync as readdirSync12 } from "node:fs";
51202
+ import { dirname as dirname17, resolve as resolve12 } from "node:path";
50822
51203
  init_util();
50823
51204
  var deriveDesignFlows = (meta, entryScreen) => {
50824
51205
  const edges = meta.edges ?? [];
@@ -50880,7 +51261,7 @@ var slug3 = (s2) => s2.replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "").
50880
51261
  var readFlowFile = (abs, displayName) => {
50881
51262
  let raw;
50882
51263
  try {
50883
- raw = JSON.parse(readFileSync25(abs, "utf8"));
51264
+ raw = JSON.parse(readFileSync26(abs, "utf8"));
50884
51265
  } catch (e2) {
50885
51266
  throw new CliError(`Can't read "${displayName}": ${e2 instanceof Error ? e2.message : String(e2)}`);
50886
51267
  }
@@ -50905,37 +51286,37 @@ var readFlowFile = (abs, displayName) => {
50905
51286
  var test = async (args) => {
50906
51287
  const { js, warnings, useLocalAssets, root } = await compileHeadlessBundle(args);
50907
51288
  const renderer = await requireRenderer();
50908
- const testsDir = resolve11(root, flagStr(args, "dir") ?? "tests");
50909
- const designMetaPath = resolve11(root, flagStr(args, "design-dir") ?? "design", "meta.json");
51289
+ const testsDir = resolve12(root, flagStr(args, "dir") ?? "tests");
51290
+ const designMetaPath = resolve12(root, flagStr(args, "design-dir") ?? "design", "meta.json");
50910
51291
  const asJson = flagBool(args, "json");
50911
51292
  const out = asJson ? logErr : log;
50912
51293
  const cases = [];
50913
51294
  if (args._.length > 0) {
50914
51295
  for (const given of args._) {
50915
- const candidates = [resolve11(given), resolve11(testsDir, given), resolve11(testsDir, `${given}.flow.json`)];
50916
- const abs = candidates.find((p2) => existsSync26(p2));
51296
+ const candidates = [resolve12(given), resolve12(testsDir, given), resolve12(testsDir, `${given}.flow.json`)];
51297
+ const abs = candidates.find((p2) => existsSync27(p2));
50917
51298
  if (!abs)
50918
51299
  throw new CliError(`No such test: "${given}" (looked in ${testsDir}).`);
50919
51300
  cases.push(readFlowFile(abs, given));
50920
51301
  }
50921
- } else if (existsSync26(testsDir)) {
50922
- for (const f3 of readdirSync11(testsDir).filter((f4) => f4.endsWith(".flow.json")).sort()) {
50923
- cases.push(readFlowFile(resolve11(testsDir, f3), f3));
51302
+ } else if (existsSync27(testsDir)) {
51303
+ for (const f3 of readdirSync12(testsDir).filter((f4) => f4.endsWith(".flow.json")).sort()) {
51304
+ cases.push(readFlowFile(resolve12(testsDir, f3), f3));
50924
51305
  }
50925
51306
  }
50926
51307
  const fixturesFlag = flagStr(args, "fixtures");
50927
- const defaultPack = resolve11(testsDir, "fixtures.json");
50928
- const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") : existsSync26(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
51308
+ const defaultPack = resolve12(testsDir, "fixtures.json");
51309
+ const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") : existsSync27(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
50929
51310
  const flowsOnly = flagBool(args, "flows");
50930
51311
  const wantFlows = !flagBool(args, "no-flows") && args._.length === 0;
50931
51312
  if (flowsOnly)
50932
51313
  cases.length = 0;
50933
51314
  let skipped = [];
50934
51315
  let entryScreen = null;
50935
- if ((wantFlows || flowsOnly) && existsSync26(designMetaPath)) {
51316
+ if ((wantFlows || flowsOnly) && existsSync27(designMetaPath)) {
50936
51317
  let meta;
50937
51318
  try {
50938
- meta = JSON.parse(readFileSync25(designMetaPath, "utf8"));
51319
+ meta = JSON.parse(readFileSync26(designMetaPath, "utf8"));
50939
51320
  } catch (e2) {
50940
51321
  throw new CliError(`Can't read ${designMetaPath}: ${e2 instanceof Error ? e2.message : String(e2)}`);
50941
51322
  }
@@ -50967,7 +51348,7 @@ var test = async (args) => {
50967
51348
  }, timeoutMs);
50968
51349
  killer.unref?.();
50969
51350
  const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
50970
- const artifactsRoot = resolve11(root, flagStr(args, "artifacts") ?? resolve11(testsDir, ".artifacts"));
51351
+ const artifactsRoot = resolve12(root, flagStr(args, "artifacts") ?? resolve12(testsDir, ".artifacts"));
50971
51352
  const results = [];
50972
51353
  for (const kase of cases) {
50973
51354
  const flagSettle = flagStr(args, "settle");
@@ -50985,14 +51366,14 @@ var test = async (args) => {
50985
51366
  onConsole
50986
51367
  });
50987
51368
  try {
50988
- const io = fileScenarioIO(resolve11(artifactsRoot, slug3(kase.name)));
51369
+ const io = fileScenarioIO(resolve12(artifactsRoot, slug3(kase.name)));
50989
51370
  const outcome = await renderer.runScenario(session, steps, io);
50990
51371
  result = {
50991
51372
  name: kase.name,
50992
51373
  kind: kase.kind,
50993
51374
  ok: outcome.ok,
50994
51375
  steps: outcome.steps.map((s2) => ({ desc: s2.desc, ok: s2.ok, error: s2.error })),
50995
- artifacts: outcome.failureArtifacts.map((p2) => resolve11(artifactsRoot, slug3(kase.name), p2))
51376
+ artifacts: outcome.failureArtifacts.map((p2) => resolve12(artifactsRoot, slug3(kase.name), p2))
50996
51377
  };
50997
51378
  } finally {
50998
51379
  session.dispose();
@@ -51054,11 +51435,11 @@ init_config();
51054
51435
  init_manifest();
51055
51436
  init_types4();
51056
51437
  init_util();
51057
- import { resolve as resolve12 } from "node:path";
51438
+ import { resolve as resolve13 } from "node:path";
51058
51439
  var label = (bundle) => `${Object.keys(bundle).length} file(s)`;
51059
51440
  var types4 = async (args) => {
51060
51441
  const dirArg = args._[0];
51061
- const root = dirArg ? resolve12(dirArg) : (() => {
51442
+ const root = dirArg ? resolve13(dirArg) : (() => {
51062
51443
  try {
51063
51444
  return findProjectRoot(process.cwd());
51064
51445
  } catch {
@@ -51117,23 +51498,23 @@ var types4 = async (args) => {
51117
51498
  };
51118
51499
 
51119
51500
  // src/index.ts
51120
- import { existsSync as existsSync28, readFileSync as readFileSync27, realpathSync as realpathSync2 } from "node:fs";
51501
+ import { existsSync as existsSync29, readFileSync as readFileSync28, realpathSync as realpathSync2 } from "node:fs";
51121
51502
  import { createRequire as createRequire4 } from "node:module";
51122
- import { delimiter as delimiter2, dirname as dirname19, join as join27 } from "node:path";
51503
+ import { delimiter as delimiter2, dirname as dirname19, join as join28 } from "node:path";
51123
51504
 
51124
51505
  // src/commands/install.ts
51125
51506
  init_util();
51126
51507
  import { execFileSync as execFileSync4 } from "node:child_process";
51127
- import { appendFileSync as appendFileSync3, chmodSync as chmodSync2, cpSync as cpSync3, existsSync as existsSync27, mkdirSync as mkdirSync18, readFileSync as readFileSync26, realpathSync, rmSync as rmSync5, symlinkSync } from "node:fs";
51128
- import { homedir as homedir6 } from "node:os";
51129
- import { basename as basename14, delimiter, dirname as dirname18, join as join26, resolve as resolve13, sep as sep2 } from "node:path";
51508
+ 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";
51509
+ import { homedir as homedir7 } from "node:os";
51510
+ import { basename as basename14, delimiter, dirname as dirname18, join as join27, resolve as resolve14, sep as sep2 } from "node:path";
51130
51511
  var isWin = process.platform === "win32";
51131
- var HOME = homedir6();
51512
+ var HOME = homedir7();
51132
51513
  var safeRealpath = (p2) => {
51133
51514
  try {
51134
51515
  return realpathSync(p2);
51135
51516
  } catch {
51136
- return resolve13(p2);
51517
+ return resolve14(p2);
51137
51518
  }
51138
51519
  };
51139
51520
  var onPath = (dir) => {
@@ -51150,24 +51531,24 @@ var onPath = (dir) => {
51150
51531
  };
51151
51532
  var install = async (args) => {
51152
51533
  const bundle = dirname18(process.execPath);
51153
- const exeName = basename14(process.execPath);
51534
+ const exeName2 = basename14(process.execPath);
51154
51535
  const chiselName = isWin ? "chisel.exe" : "chisel";
51155
- if (!existsSync27(join26(bundle, "runtime")) || !existsSync27(join26(bundle, chiselName))) {
51536
+ if (!existsSync28(join27(bundle, "runtime")) || !existsSync28(join27(bundle, chiselName))) {
51156
51537
  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.");
51157
51538
  }
51158
- const companions = [exeName, chiselName, "runtime", "README.txt"].filter((f3) => existsSync27(join26(bundle, f3)));
51539
+ const companions = [exeName2, chiselName, "runtime", "README.txt"].filter((f3) => existsSync28(join27(bundle, f3)));
51159
51540
  if (isWin)
51160
51541
  return installWindows(args, bundle, companions);
51161
51542
  return installUnix(args, bundle, companions);
51162
51543
  };
51163
51544
  var copyBundle = (bundle, target, companions) => {
51164
- mkdirSync18(target, { recursive: true });
51545
+ mkdirSync19(target, { recursive: true });
51165
51546
  if (safeRealpath(target) === safeRealpath(bundle))
51166
51547
  return;
51167
51548
  for (const f3 of companions) {
51168
- const dest = join26(target, f3);
51169
- rmSync5(dest, { recursive: true, force: true });
51170
- cpSync3(join26(bundle, f3), dest, { recursive: true });
51549
+ const dest = join27(target, f3);
51550
+ rmSync6(dest, { recursive: true, force: true });
51551
+ cpSync3(join27(bundle, f3), dest, { recursive: true });
51171
51552
  }
51172
51553
  };
51173
51554
  var permError = (e2, dir, system) => {
@@ -51179,41 +51560,41 @@ var permError = (e2, dir, system) => {
51179
51560
  };
51180
51561
  var installUnix = (args, bundle, companions) => {
51181
51562
  const system = flagBool(args, "system");
51182
- const libDir = resolve13(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join26(HOME, ".local", "lib", "lecodes-cli")));
51183
- const binDir = resolve13(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join26(HOME, ".local", "bin")));
51184
- const link2 = join26(binDir, "lecodes");
51563
+ const libDir = resolve14(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join27(HOME, ".local", "lib", "lecodes-cli")));
51564
+ const binDir = resolve14(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join27(HOME, ".local", "bin")));
51565
+ const link2 = join27(binDir, "lecodes");
51185
51566
  try {
51186
51567
  copyBundle(bundle, libDir, companions);
51187
- chmodSync2(join26(libDir, "lecodes"), 493);
51188
- chmodSync2(join26(libDir, "chisel"), 493);
51568
+ chmodSync2(join27(libDir, "lecodes"), 493);
51569
+ chmodSync2(join27(libDir, "chisel"), 493);
51189
51570
  } catch (e2) {
51190
51571
  throw permError(e2, libDir, system);
51191
51572
  }
51192
51573
  try {
51193
- mkdirSync18(binDir, { recursive: true });
51194
- rmSync5(link2, { force: true });
51195
- symlinkSync(join26(libDir, "lecodes"), link2);
51574
+ mkdirSync19(binDir, { recursive: true });
51575
+ rmSync6(link2, { force: true });
51576
+ symlinkSync(join27(libDir, "lecodes"), link2);
51196
51577
  } catch (e2) {
51197
51578
  throw permError(e2, binDir, system);
51198
51579
  }
51199
51580
  success(`Installed to ${c.bold(libDir)}`);
51200
- log(` ${c.dim(link2 + " → " + join26(libDir, "lecodes"))}`);
51581
+ log(` ${c.dim(link2 + " → " + join27(libDir, "lecodes"))}`);
51201
51582
  finishPathUnix(args, binDir);
51202
51583
  };
51203
51584
  var shellRc = () => {
51204
51585
  const shell = basename14(process.env.SHELL ?? "");
51205
51586
  if (shell === "zsh")
51206
- return { file: join26(HOME, ".zshrc"), name: "~/.zshrc" };
51587
+ return { file: join27(HOME, ".zshrc"), name: "~/.zshrc" };
51207
51588
  if (shell === "bash")
51208
- return { file: join26(HOME, ".bashrc"), name: "~/.bashrc" };
51209
- return { file: join26(HOME, ".profile"), name: "~/.profile" };
51589
+ return { file: join27(HOME, ".bashrc"), name: "~/.bashrc" };
51590
+ return { file: join27(HOME, ".profile"), name: "~/.profile" };
51210
51591
  };
51211
51592
  var PATH_MARKER = "# Added by `lecodes install`";
51212
51593
  var appendPathToShellRc = (binDir) => {
51213
51594
  const { file, name } = shellRc();
51214
51595
  const line = `export PATH="${binDir}:$PATH"`;
51215
51596
  try {
51216
- const existing = existsSync27(file) ? readFileSync26(file, "utf8") : "";
51597
+ const existing = existsSync28(file) ? readFileSync27(file, "utf8") : "";
51217
51598
  if (existing.includes(line))
51218
51599
  return name;
51219
51600
  appendFileSync3(file, `${existing && !existing.endsWith(`
@@ -51251,7 +51632,7 @@ var finishPathUnix = (args, binDir) => {
51251
51632
  log(` • or install system-wide (no PATH edit): ${c.bold("sudo lecodes install --system")}`);
51252
51633
  };
51253
51634
  var installWindows = (args, bundle, companions) => {
51254
- const target = resolve13(flagStr(args, "dir") ?? join26(process.env.LOCALAPPDATA ?? join26(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
51635
+ const target = resolve14(flagStr(args, "dir") ?? join27(process.env.LOCALAPPDATA ?? join27(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
51255
51636
  try {
51256
51637
  copyBundle(bundle, target, companions);
51257
51638
  } catch (e2) {
@@ -51291,7 +51672,7 @@ init_util();
51291
51672
  var STANDALONE = (() => {
51292
51673
  const dir = dirname19(process.execPath);
51293
51674
  const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
51294
- return existsSync28(join27(dir, "runtime")) && existsSync28(join27(dir, chisel));
51675
+ return existsSync29(join28(dir, "runtime")) && existsSync29(join28(dir, chisel));
51295
51676
  })();
51296
51677
  var INSTALLED = STANDALONE && (() => {
51297
51678
  const exe = process.platform === "win32" ? "lecodes.exe" : "lecodes";
@@ -51306,8 +51687,8 @@ var INSTALLED = STANDALONE && (() => {
51306
51687
  if (!d)
51307
51688
  return false;
51308
51689
  try {
51309
- const cand = join27(d, exe);
51310
- return existsSync28(cand) && realpathSync2(cand) === self2;
51690
+ const cand = join28(d, exe);
51691
+ return existsSync29(cand) && realpathSync2(cand) === self2;
51311
51692
  } catch {
51312
51693
  return false;
51313
51694
  }
@@ -51396,6 +51777,14 @@ ${c.bold("Commands:")}
51396
51777
  --settle <ms> wait for promises/timers/fetch before snapshot
51397
51778
  --wait-network let data fetches hit the real network
51398
51779
  --logs forward the app's console output (to stderr)
51780
+ --desktop render through the NATIVE desktop host instead:
51781
+ real Filament 3D + tgfx UI + 2D, composited.
51782
+ PNG only (--png). Downloads the LeCodes
51783
+ Desktop renderer on first use into
51784
+ ~/.lecodes/renderer (Windows/Linux;
51785
+ LECODES_DESKTOP_EXE overrides)
51786
+ --frames <n> (--desktop) frames to run before the shot —
51787
+ headroom for async loads (default 60)
51399
51788
  test [name…] Run headless flow tests: tests/*.flow.json scenarios, plus flows
51400
51789
  derived from the design graph (design/meta.json activator edges
51401
51790
  — checks the built app against the designed navigation; needs
@@ -51435,6 +51824,17 @@ ${c.bold("Commands:")}
51435
51824
  --logs forward the screens' console output (to stderr)
51436
51825
  design comments Review comments pinned to the board, grouped by screen (open first)
51437
51826
  --all include resolved threads
51827
+ add "<text>" [--screen <id>] [--state <s>] [--element <name>]
51828
+ reply <number> "<text>" · resolve <number> [--reply "<text>"] [--reopen]
51829
+ (--ai signs a write as Claude; sends with the next "lecodes push")
51830
+ desktop [run|update] The native desktop host ('lecodes render --desktop' uses it too):
51831
+ no args shows the version status (selected / downloaded /
51832
+ newest published)
51833
+ run compile the current project and open it in a real
51834
+ desktop window (--width/--height set the window size;
51835
+ --entry overrides the entrypoint)
51836
+ update move to the newest published release
51837
+ (cached in ~/.lecodes/renderer)
51438
51838
  scene [path] LeCodes Scene Editor — visually edit a .scene.ts (tree, inspector,
51439
51839
  gizmo) on local files (needs the optional 'lecodes-3d-editor' package;
51440
51840
  works inside a cloned project or standalone in any folder)
@@ -51498,7 +51898,7 @@ ${c.bold("Commands:")}
51498
51898
  — 'new --env' banks it in .env), revoke <id>
51499
51899
 
51500
51900
  Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKEN).`;
51501
- var commands = { init: init2, login, create, clone: clone3, link, status, diff: diff2, pull, push, compile: compile2, preview: previewLegacy, render, test, types: types4, design, scene, dev, app, pn };
51901
+ var commands = { init: init2, login, create, clone: clone3, link, status: status2, diff: diff2, pull, push, compile: compile2, preview: previewLegacy, render, test, types: types4, design, scene, dev, app, pn, desktop };
51502
51902
  if (STANDALONE)
51503
51903
  commands.install = install;
51504
51904
  var readVersion = () => package_default.version ?? "unknown";
@@ -51512,9 +51912,9 @@ var peerVersion = (name, probe) => {
51512
51912
  const require2 = createRequire4(import.meta.url);
51513
51913
  let dir = dirname19(require2.resolve(probe));
51514
51914
  for (let depth = 0;depth < 16; depth++) {
51515
- const manifest = join27(dir, "package.json");
51516
- if (existsSync28(manifest)) {
51517
- const parsed = JSON.parse(readFileSync27(manifest, "utf8"));
51915
+ const manifest = join28(dir, "package.json");
51916
+ if (existsSync29(manifest)) {
51917
+ const parsed = JSON.parse(readFileSync28(manifest, "utf8"));
51518
51918
  if (parsed.name === name)
51519
51919
  return parsed.version ?? "unknown";
51520
51920
  }
@@ -51532,6 +51932,8 @@ var printVersion = () => {
51532
51932
  const version = peerVersion(name, probe);
51533
51933
  rows.push([name, version ?? c.dim("not installed")]);
51534
51934
  }
51935
+ const desktopVersion = selectedDesktopVersion();
51936
+ rows.push(["lecodes-desktop", installedDesktopVersions().includes(desktopVersion) ? desktopVersion : `${desktopVersion} ${c.dim("(not downloaded — fetched on first `render --desktop`)")}`]);
51535
51937
  const width = Math.max(...rows.map(([label2]) => label2.length));
51536
51938
  for (const [label2, value] of rows)
51537
51939
  log(`${label2.padEnd(width)} ${value}`);
@@ -51547,14 +51949,14 @@ var main = async () => {
51547
51949
  printVersion();
51548
51950
  return;
51549
51951
  }
51550
- const run = commands[command];
51551
- if (!run) {
51952
+ const run2 = commands[command];
51953
+ if (!run2) {
51552
51954
  log(c.red(`Unknown command: ${command}`));
51553
51955
  log(HELP);
51554
51956
  process.exitCode = 1;
51555
51957
  return;
51556
51958
  }
51557
- await run(parseArgs(argv.slice(1)));
51959
+ await run2(parseArgs(argv.slice(1)));
51558
51960
  };
51559
51961
  main().catch((e2) => {
51560
51962
  if (e2 instanceof CliError)