lecodes-cli 0.12.0 → 0.13.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
@@ -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.0",
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();
@@ -49302,24 +49647,24 @@ var pickLanAddress = () => {
49302
49647
  return null;
49303
49648
  };
49304
49649
  var loadDevToken = (root) => {
49305
- const path2 = join19(root, LECODES_DIR, "dev.json");
49650
+ const path2 = join20(root, LECODES_DIR, "dev.json");
49306
49651
  try {
49307
- const token2 = JSON.parse(readFileSync18(path2, "utf8")).token;
49652
+ const token2 = JSON.parse(readFileSync19(path2, "utf8")).token;
49308
49653
  if (token2 && /^[a-z0-9]{8,}$/.test(token2))
49309
49654
  return token2;
49310
49655
  } catch {}
49311
49656
  const token = randomBytes2(6).toString("hex");
49312
- mkdirSync14(join19(root, LECODES_DIR), { recursive: true });
49313
- writeFileSync15(path2, JSON.stringify({ token }, null, 2) + `
49657
+ mkdirSync15(join20(root, LECODES_DIR), { recursive: true });
49658
+ writeFileSync16(path2, JSON.stringify({ token }, null, 2) + `
49314
49659
  `);
49315
49660
  return token;
49316
49661
  };
49317
49662
  var printQr = async (url) => {
49318
49663
  try {
49319
49664
  const qrcode = (await Promise.resolve().then(() => __toESM(require_main(), 1))).default;
49320
- await new Promise((resolve7) => qrcode.generate(url, { small: true }, (out) => {
49665
+ await new Promise((resolve8) => qrcode.generate(url, { small: true }, (out) => {
49321
49666
  log(out);
49322
- resolve7();
49667
+ resolve8();
49323
49668
  }));
49324
49669
  } catch {
49325
49670
  note("(couldn't render a QR code — open the URL below on the device)");
@@ -49422,7 +49767,7 @@ ${result.error}`);
49422
49767
  await printQr(server.url);
49423
49768
  log(`Scan the QR with the LeCodes app, or open: ${c.bold(server.url)}`);
49424
49769
  note(`Older desktop builds (no websocket bridge): ${server.url}?t=poll`);
49425
- if (existsSync19(join19(root, LECODES_DIR, "manifest.json")))
49770
+ if (existsSync20(join20(root, LECODES_DIR, "manifest.json")))
49426
49771
  note(`Token stored in ${LECODES_DIR}/dev.json (delete it to rotate the URL).`);
49427
49772
  log("");
49428
49773
  log(`Watching ${root} — save a file to hot-reload. Ctrl+C to stop.`);
@@ -49436,7 +49781,7 @@ init_localFiles();
49436
49781
 
49437
49782
  // src/serverDiff.ts
49438
49783
  init_localFiles();
49439
- import { readFileSync as readFileSync19 } from "node:fs";
49784
+ import { readFileSync as readFileSync20 } from "node:fs";
49440
49785
  import { basename as basename9 } from "node:path";
49441
49786
  var isTextAsset = (t2) => t2 === "text" || t2 === "shader" || t2 === "scene";
49442
49787
  var computeServerDiff = (project, scan2) => {
@@ -49455,7 +49800,7 @@ var computeServerDiff = (project, scan2) => {
49455
49800
  const local = localByPath.get(path2);
49456
49801
  if (local && !server) {
49457
49802
  const text = isTextKind(classifyKind(basename9(path2)));
49458
- entries.push({ path: path2, status: "added", binary: !text, serverText: "", localText: text ? readFileSync19(local.absPath, "utf8") : undefined });
49803
+ entries.push({ path: path2, status: "added", binary: !text, serverText: "", localText: text ? readFileSync20(local.absPath, "utf8") : undefined });
49459
49804
  continue;
49460
49805
  }
49461
49806
  if (server && !local) {
@@ -49466,12 +49811,12 @@ var computeServerDiff = (project, scan2) => {
49466
49811
  if (server && local) {
49467
49812
  if (isTextAsset(server.type)) {
49468
49813
  const serverText = server.text ?? "";
49469
- const localText = readFileSync19(local.absPath, "utf8");
49814
+ const localText = readFileSync20(local.absPath, "utf8");
49470
49815
  if (serverText !== localText)
49471
49816
  entries.push({ path: path2, status: "modified", binary: false, serverText, localText });
49472
49817
  } else {
49473
49818
  const serverSize = server.file?.size;
49474
- const localSize = Buffer.byteLength(readFileSync19(local.absPath));
49819
+ const localSize = Buffer.byteLength(readFileSync20(local.absPath));
49475
49820
  if (serverSize !== undefined && serverSize !== localSize) {
49476
49821
  entries.push({ path: path2, status: "modified", binary: true });
49477
49822
  }
@@ -49629,8 +49974,8 @@ var statusLabel = (e2) => e2.status === "added" ? c.green(" added ") : e2.st
49629
49974
  init_manifest();
49630
49975
  init_types4();
49631
49976
  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";
49977
+ import { existsSync as existsSync21, mkdirSync as mkdirSync16, writeFileSync as writeFileSync17 } from "node:fs";
49978
+ import { basename as basename10, dirname as dirname13, join as join21, resolve as resolve8 } from "node:path";
49634
49979
  init_util();
49635
49980
  var STARTER2 = `// Welcome to LeCodes — the whole SDK is available as globals, no imports needed.
49636
49981
  // Run it live on a device with \`lecodes dev\`, or bundle it with \`lecodes compile -o app.js\`.
@@ -49728,18 +50073,18 @@ Full command list: \`lecodes --help\`.
49728
50073
  `;
49729
50074
  var init2 = async (args) => {
49730
50075
  const dirArg = (args._[0] ?? ".").replace(/[/\\]+$/, "") || ".";
49731
- const root = resolve7(dirArg);
50076
+ const root = resolve8(dirArg);
49732
50077
  const name = basename10(root);
49733
- mkdirSync15(root, { recursive: true });
50078
+ mkdirSync16(root, { recursive: true });
49734
50079
  let created = 0;
49735
50080
  const write = (rel, content) => {
49736
- const abs = join20(root, rel);
49737
- if (existsSync20(abs)) {
50081
+ const abs = join21(root, rel);
50082
+ if (existsSync21(abs)) {
49738
50083
  note(`kept ${rel}`);
49739
50084
  return;
49740
50085
  }
49741
- mkdirSync15(dirname13(abs), { recursive: true });
49742
- writeFileSync16(abs, content);
50086
+ mkdirSync16(dirname13(abs), { recursive: true });
50087
+ writeFileSync17(abs, content);
49743
50088
  log(`${c.green("created")} ${rel}`);
49744
50089
  created++;
49745
50090
  };
@@ -49768,8 +50113,8 @@ init_config();
49768
50113
  init_ignore();
49769
50114
  init_localFiles();
49770
50115
  init_manifest();
49771
- import { existsSync as existsSync21 } from "node:fs";
49772
- import { basename as basename12, join as join21, resolve as resolve8 } from "node:path";
50116
+ import { existsSync as existsSync22 } from "node:fs";
50117
+ import { basename as basename12, join as join22, resolve as resolve9 } from "node:path";
49773
50118
  init_types4();
49774
50119
  init_util();
49775
50120
 
@@ -49779,7 +50124,7 @@ init_config();
49779
50124
  init_manifest();
49780
50125
  init_localFiles();
49781
50126
  import { basename as basename11 } from "node:path";
49782
- import { readFileSync as readFileSync20 } from "node:fs";
50127
+ import { readFileSync as readFileSync21 } from "node:fs";
49783
50128
  init_util();
49784
50129
  var push = async (args) => {
49785
50130
  const root = findProjectRoot(process.cwd());
@@ -49840,9 +50185,9 @@ var push = async (args) => {
49840
50185
  if (source)
49841
50186
  entry.assetId = source.id;
49842
50187
  if (isTextKind(kind)) {
49843
- entry.text = readFileSync20(file.absPath, "utf8");
50188
+ entry.text = readFileSync21(file.absPath, "utf8");
49844
50189
  } else if (!source || source.sha !== file.sha) {
49845
- const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename11(file.path), readFileSync20(file.absPath));
50190
+ const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename11(file.path), readFileSync21(file.absPath));
49846
50191
  entry.blobFileId = blob.id;
49847
50192
  uploaded++;
49848
50193
  }
@@ -49949,10 +50294,10 @@ var link = async (args) => {
49949
50294
  const apiUrl = requireApiUrl(config);
49950
50295
  const token = requireToken(config);
49951
50296
  const dirArg = args._[0];
49952
- const root = dirArg ? resolve8(dirArg) : process.cwd();
49953
- if (!existsSync21(root))
50297
+ const root = dirArg ? resolve9(dirArg) : process.cwd();
50298
+ if (!existsSync22(root))
49954
50299
  throw new CliError(`No such directory: ${root}`);
49955
- if (existsSync21(join21(root, LECODES_DIR, "manifest.json")) && !flagBool(args, "force")) {
50300
+ if (existsSync22(join22(root, LECODES_DIR, "manifest.json")) && !flagBool(args, "force")) {
49956
50301
  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
50302
  }
49958
50303
  let enclosing = null;
@@ -50028,8 +50373,8 @@ var login = async (args) => {
50028
50373
  };
50029
50374
 
50030
50375
  // 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";
50376
+ import { mkdirSync as mkdirSync17, readFileSync as readFileSync22, writeFileSync as writeFileSync18 } from "node:fs";
50377
+ import { dirname as dirname14, resolve as resolve10 } from "node:path";
50033
50378
 
50034
50379
  // src/compile/headlessBundle.ts
50035
50380
  init_api();
@@ -50139,9 +50484,9 @@ var requireRenderer = async () => {
50139
50484
  };
50140
50485
  var fileScenarioIO = (dir) => ({
50141
50486
  write: (relPath, data) => {
50142
- const abs = resolve9(dir, relPath);
50143
- mkdirSync16(dirname14(abs), { recursive: true });
50144
- writeFileSync17(abs, data);
50487
+ const abs = resolve10(dir, relPath);
50488
+ mkdirSync17(dirname14(abs), { recursive: true });
50489
+ writeFileSync18(abs, data);
50145
50490
  }
50146
50491
  });
50147
50492
  var resolveFixtures = (value, baseDir) => {
@@ -50149,9 +50494,9 @@ var resolveFixtures = (value, baseDir) => {
50149
50494
  return;
50150
50495
  let map = value;
50151
50496
  if (typeof value === "string") {
50152
- const abs = resolve9(baseDir, value);
50497
+ const abs = resolve10(baseDir, value);
50153
50498
  try {
50154
- map = JSON.parse(readFileSync21(abs, "utf8"));
50499
+ map = JSON.parse(readFileSync22(abs, "utf8"));
50155
50500
  } catch (e2) {
50156
50501
  throw new CliError(`Can't read fixtures "${value}": ${e2 instanceof Error ? e2.message : String(e2)}`);
50157
50502
  }
@@ -50164,7 +50509,7 @@ var resolveFixtures = (value, baseDir) => {
50164
50509
  var readScenarioFile = (path2) => {
50165
50510
  let raw;
50166
50511
  try {
50167
- raw = JSON.parse(readFileSync21(resolve9(path2), "utf8"));
50512
+ raw = JSON.parse(readFileSync22(resolve10(path2), "utf8"));
50168
50513
  } catch (e2) {
50169
50514
  throw new CliError(`Can't read scenario "${path2}": ${e2 instanceof Error ? e2.message : String(e2)}`);
50170
50515
  }
@@ -50177,7 +50522,27 @@ var readScenarioFile = (path2) => {
50177
50522
  throw new CliError(`Scenario "${path2}" has no "steps".`);
50178
50523
  return file;
50179
50524
  };
50525
+ var renderDesktop = async (args) => {
50526
+ const width = Number(flagStr(args, "width")) || 390;
50527
+ const height = Number(flagStr(args, "height")) || 844;
50528
+ const frames = Number(flagStr(args, "frames")) || 60;
50529
+ const timeoutMs = Number(flagStr(args, "timeout")) || 60000;
50530
+ const pngFlag = flagStr(args, "png") ?? "screenshot.png";
50531
+ const { js, resources } = await buildBundle(args);
50532
+ note("Rendering (native)…");
50533
+ await runDesktopRender(js, resources, {
50534
+ width,
50535
+ height,
50536
+ frames,
50537
+ outPng: pngFlag,
50538
+ logs: flagBool(args, "logs"),
50539
+ timeoutMs
50540
+ });
50541
+ note(`Wrote ${pngFlag} (${width}x${height}, native render)`);
50542
+ };
50180
50543
  var render = async (args) => {
50544
+ if (flagBool(args, "desktop"))
50545
+ return renderDesktop(args);
50181
50546
  const scriptFlag = flagStr(args, "script");
50182
50547
  const scenario = scriptFlag !== undefined ? readScenarioFile(scriptFlag) : null;
50183
50548
  const stepCount = scenario && Array.isArray(scenario.steps) ? scenario.steps.length : 0;
@@ -50194,7 +50559,7 @@ var render = async (args) => {
50194
50559
  const settleStr = flagStr(args, "settle") ?? (scenario?.settle !== undefined ? String(scenario.settle) : undefined);
50195
50560
  const timeStr = flagStr(args, "time");
50196
50561
  const onConsole = flagBool(args, "logs") ? (level, text2) => logErr(`${c.dim(`[${level}]`)} ${text2}`) : undefined;
50197
- const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname14(resolve9(scriptFlag)) : ".");
50562
+ const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname14(resolve10(scriptFlag)) : ".");
50198
50563
  const flagFixtures = resolveFixtures(flagStr(args, "fixtures"), ".");
50199
50564
  const fixtures = envelopeFixtures || flagFixtures ? { ...envelopeFixtures, ...flagFixtures } : undefined;
50200
50565
  if (fixtures) {
@@ -50233,7 +50598,7 @@ var render = async (args) => {
50233
50598
  ${c.red(st.error ?? "failed")}`);
50234
50599
  }
50235
50600
  if (!result2.ok && result2.failureArtifacts.length > 0) {
50236
- logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p2) => resolve9(outDir, p2)).join(", ")}`));
50601
+ logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p2) => resolve10(outDir, p2)).join(", ")}`));
50237
50602
  }
50238
50603
  logErr(result2.ok ? c.green(`✓ ${result2.steps.length}/${steps.length} steps passed`) : c.red(`✗ failed at step ${result2.steps.length} of ${steps.length}`));
50239
50604
  return done((cb) => cb(), result2.ok ? 0 : 1);
@@ -50255,7 +50620,7 @@ var render = async (args) => {
50255
50620
  if (isNullSink(pngFlag)) {
50256
50621
  note(`Discarded PNG (${pngFlag})`);
50257
50622
  } else {
50258
- writeFileSync17(resolve9(pngFlag), png2);
50623
+ writeFileSync18(resolve10(pngFlag), png2);
50259
50624
  note(`Wrote ${pngFlag} (${png2.length} bytes, ${width}x${height})`);
50260
50625
  }
50261
50626
  return done((cb) => cb());
@@ -50270,7 +50635,7 @@ var render = async (args) => {
50270
50635
  return done((cb) => cb());
50271
50636
  }
50272
50637
  if (out) {
50273
- writeFileSync17(resolve9(out), result.text);
50638
+ writeFileSync18(resolve10(out), result.text);
50274
50639
  note(`Wrote ${out} (${result.text.length} bytes)`);
50275
50640
  return done((cb) => cb());
50276
50641
  }
@@ -50285,8 +50650,8 @@ init_api();
50285
50650
  init_config();
50286
50651
  init_manifest();
50287
50652
  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";
50653
+ import { appendFileSync as appendFileSync2, existsSync as existsSync23, readFileSync as readFileSync23, writeFileSync as writeFileSync19 } from "node:fs";
50654
+ import { join as join23 } from "node:path";
50290
50655
  var ENV_VAR = "LECODES_PUSH_KEY";
50291
50656
  var context = () => {
50292
50657
  const root = findProjectRoot(process.cwd());
@@ -50430,11 +50795,11 @@ var pnKeysList = async (ctx) => {
50430
50795
  };
50431
50796
  var writeEnv = (root, key) => {
50432
50797
  const touched = [];
50433
- const envPath = join22(root, ".env");
50798
+ const envPath = join23(root, ".env");
50434
50799
  const line = `${ENV_VAR}=${key}`;
50435
- const existing = existsSync22(envPath) ? readFileSync22(envPath, "utf8") : "";
50800
+ const existing = existsSync23(envPath) ? readFileSync23(envPath, "utf8") : "";
50436
50801
  if (new RegExp(`^${ENV_VAR}=`, "m").test(existing)) {
50437
- writeFileSync18(envPath, existing.replace(new RegExp(`^${ENV_VAR}=.*$`, "m"), line));
50802
+ writeFileSync19(envPath, existing.replace(new RegExp(`^${ENV_VAR}=.*$`, "m"), line));
50438
50803
  } else {
50439
50804
  const separator = !existing || existing.endsWith(`
50440
50805
  `) ? "" : `
@@ -50443,8 +50808,8 @@ var writeEnv = (root, key) => {
50443
50808
  `);
50444
50809
  }
50445
50810
  touched.push(".env");
50446
- const ignorePath = join22(root, ".gitignore");
50447
- const ignore = existsSync22(ignorePath) ? readFileSync22(ignorePath, "utf8") : "";
50811
+ const ignorePath = join23(root, ".gitignore");
50812
+ const ignore = existsSync23(ignorePath) ? readFileSync23(ignorePath, "utf8") : "";
50448
50813
  if (!ignore.split(`
50449
50814
  `).some((l2) => l2.trim() === ".env")) {
50450
50815
  const separator = !ignore || ignore.endsWith(`
@@ -50543,8 +50908,8 @@ var pn = async (args) => {
50543
50908
  init_api();
50544
50909
  init_config();
50545
50910
  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";
50911
+ import { existsSync as existsSync24, mkdirSync as mkdirSync18, rmSync as rmSync5, writeFileSync as writeFileSync20 } from "node:fs";
50912
+ import { dirname as dirname15, join as join24 } from "node:path";
50548
50913
  init_types4();
50549
50914
  init_localFiles();
50550
50915
  init_util();
@@ -50567,9 +50932,9 @@ var pull = async (args) => {
50567
50932
  const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a2) => a2.id === id)?.type !== "folder").map(([, path2]) => path2));
50568
50933
  for (const path2 of Object.keys(manifest.files)) {
50569
50934
  if (!remotePaths.has(path2)) {
50570
- const abs = join23(root, ...path2.replace(/^\//, "").split("/"));
50571
- if (existsSync23(abs))
50572
- rmSync4(abs);
50935
+ const abs = join24(root, ...path2.replace(/^\//, "").split("/"));
50936
+ if (existsSync24(abs))
50937
+ rmSync5(abs);
50573
50938
  }
50574
50939
  }
50575
50940
  const files = await materializeAssets(root, project, apiUrl, token, progress);
@@ -50606,18 +50971,18 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
50606
50971
  const files = { ...manifest.files };
50607
50972
  for (const asset of targets) {
50608
50973
  const key = pathById.get(asset.id);
50609
- const abs = join23(root, ...key.replace(/^\//, "").split("/"));
50610
- mkdirSync17(dirname15(abs), { recursive: true });
50974
+ const abs = join24(root, ...key.replace(/^\//, "").split("/"));
50975
+ mkdirSync18(dirname15(abs), { recursive: true });
50611
50976
  let sha;
50612
50977
  if (asset.type === "file") {
50613
50978
  if (!asset.file?.src)
50614
50979
  continue;
50615
50980
  const bytes = await downloadBlob(apiUrl, asset.file.src);
50616
- writeFileSync19(abs, bytes);
50981
+ writeFileSync20(abs, bytes);
50617
50982
  sha = sha256(bytes);
50618
50983
  } else {
50619
50984
  const text = asset.text ?? "";
50620
- writeFileSync19(abs, text);
50985
+ writeFileSync20(abs, text);
50621
50986
  sha = sha256(text);
50622
50987
  }
50623
50988
  files[key] = { id: asset.id, type: kindOf3(asset.type), sha };
@@ -50630,8 +50995,8 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
50630
50995
 
50631
50996
  // src/commands/scene.ts
50632
50997
  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";
50998
+ import { existsSync as existsSync26, readFileSync as readFileSync25, readdirSync as readdirSync11, statSync as statSync7, writeFileSync as writeFileSync21 } from "node:fs";
50999
+ import { basename as basename13, dirname as dirname16, join as join26, resolve as resolve11 } from "node:path";
50635
51000
 
50636
51001
  // src/compile/sceneCompile.ts
50637
51002
  init_localFiles();
@@ -50639,18 +51004,18 @@ init_ignore();
50639
51004
  init_distRoot();
50640
51005
  init_util();
50641
51006
  init_projectCompile();
50642
- import { existsSync as existsSync24, readFileSync as readFileSync23 } from "node:fs";
50643
- import { join as join24 } from "node:path";
51007
+ import { existsSync as existsSync25, readFileSync as readFileSync24 } from "node:fs";
51008
+ import { join as join25 } from "node:path";
50644
51009
  var TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i;
50645
51010
  var rel = (key) => key.replace(/^\//, "");
50646
51011
  var resolveHarness = (loadSceneHarness2) => {
50647
- const inRepo = join24(distRoot, "..", "projects", "src", "scene-editor");
51012
+ const inRepo = join25(distRoot, "..", "projects", "src", "scene-editor");
50648
51013
  try {
50649
- return loadSceneHarness2(existsSync24(inRepo) ? inRepo : undefined);
51014
+ return loadSceneHarness2(existsSync25(inRepo) ? inRepo : undefined);
50650
51015
  } catch {
50651
- const vendored = join24(distRoot, "runtime", "scene-harness.json");
50652
- if (existsSync24(vendored))
50653
- return JSON.parse(readFileSync23(vendored, "utf8"));
51016
+ const vendored = join25(distRoot, "runtime", "scene-harness.json");
51017
+ if (existsSync25(vendored))
51018
+ return JSON.parse(readFileSync24(vendored, "utf8"));
50654
51019
  throw new CliError("scene-editor harness sources not found — reinstall the CLI or set LECODES_SCENE_HARNESS_DIR.");
50655
51020
  }
50656
51021
  };
@@ -50662,7 +51027,7 @@ var compileSceneEditor = async (opts) => {
50662
51027
  for (const file of scan2.files) {
50663
51028
  const path2 = "./" + rel(file.path);
50664
51029
  if (TEXT_EXT.test(path2)) {
50665
- entries.push({ path: path2, type: "text", text: readFileSync23(file.absPath, "utf8") });
51030
+ entries.push({ path: path2, type: "text", text: readFileSync24(file.absPath, "utf8") });
50666
51031
  } else {
50667
51032
  entries.push({ path: path2, type: "resource", fileSrc: "/project/" + rel(file.path) });
50668
51033
  }
@@ -50704,10 +51069,10 @@ var getContext2 = (args) => {
50704
51069
  manifest = null;
50705
51070
  const pathArg = args._[0];
50706
51071
  if (pathArg) {
50707
- const abs = resolve10(process.cwd(), pathArg);
50708
- if (existsSync25(abs) && statSync6(abs).isDirectory())
51072
+ const abs = resolve11(process.cwd(), pathArg);
51073
+ if (existsSync26(abs) && statSync7(abs).isDirectory())
50709
51074
  root = abs;
50710
- else if (abs.endsWith(".scene.ts") && existsSync25(abs))
51075
+ else if (abs.endsWith(".scene.ts") && existsSync26(abs))
50711
51076
  root = dirname16(abs);
50712
51077
  else
50713
51078
  throw new CliError(`No such folder or .scene.ts: ${pathArg}`);
@@ -50716,12 +51081,12 @@ var getContext2 = (args) => {
50716
51081
  return { root, manifest, name: manifest?.name ?? basename13(root) };
50717
51082
  };
50718
51083
  var writeMcpConfig2 = (root, port) => {
50719
- const path2 = join25(root, ".mcp.json");
51084
+ const path2 = join26(root, ".mcp.json");
50720
51085
  const entry = { type: "http", url: `http://localhost:${port}/mcp` };
50721
51086
  let config = {};
50722
- if (existsSync25(path2)) {
51087
+ if (existsSync26(path2)) {
50723
51088
  try {
50724
- config = JSON.parse(readFileSync24(path2, "utf8"));
51089
+ config = JSON.parse(readFileSync25(path2, "utf8"));
50725
51090
  } catch {
50726
51091
  warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-scene server by hand.");
50727
51092
  return;
@@ -50734,17 +51099,17 @@ var writeMcpConfig2 = (root, port) => {
50734
51099
  return;
50735
51100
  }
50736
51101
  config.mcpServers["lecodes-scene"] = entry;
50737
- writeFileSync20(path2, JSON.stringify(config, null, 2) + `
51102
+ writeFileSync21(path2, JSON.stringify(config, null, 2) + `
50738
51103
  `);
50739
51104
  note(`${existing ? "updated" : "created"} .mcp.json (lecodes-scene MCP → ${entry.url})`);
50740
51105
  };
50741
51106
  var countScenes = (dir) => {
50742
51107
  let n2 = 0;
50743
- for (const name of readdirSync10(dir)) {
51108
+ for (const name of readdirSync11(dir)) {
50744
51109
  if (name === "node_modules" || name.startsWith("."))
50745
51110
  continue;
50746
- const full = join25(dir, name);
50747
- if (statSync6(full).isDirectory())
51111
+ const full = join26(dir, name);
51112
+ if (statSync7(full).isDirectory())
50748
51113
  n2 += countScenes(full);
50749
51114
  else if (name.endsWith(".scene.ts"))
50750
51115
  n2++;
@@ -50788,7 +51153,7 @@ var scene = async (args) => {
50788
51153
  init_manifest();
50789
51154
  init_localFiles();
50790
51155
  init_util();
50791
- var status = async (_args) => {
51156
+ var status2 = async (_args) => {
50792
51157
  const root = findProjectRoot(process.cwd());
50793
51158
  const manifest = readManifest(root);
50794
51159
  const scan2 = scanProject(root, manifest);
@@ -50817,8 +51182,8 @@ var status = async (_args) => {
50817
51182
  };
50818
51183
 
50819
51184
  // 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";
51185
+ import { existsSync as existsSync27, readFileSync as readFileSync26, readdirSync as readdirSync12 } from "node:fs";
51186
+ import { dirname as dirname17, resolve as resolve12 } from "node:path";
50822
51187
  init_util();
50823
51188
  var deriveDesignFlows = (meta, entryScreen) => {
50824
51189
  const edges = meta.edges ?? [];
@@ -50880,7 +51245,7 @@ var slug3 = (s2) => s2.replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "").
50880
51245
  var readFlowFile = (abs, displayName) => {
50881
51246
  let raw;
50882
51247
  try {
50883
- raw = JSON.parse(readFileSync25(abs, "utf8"));
51248
+ raw = JSON.parse(readFileSync26(abs, "utf8"));
50884
51249
  } catch (e2) {
50885
51250
  throw new CliError(`Can't read "${displayName}": ${e2 instanceof Error ? e2.message : String(e2)}`);
50886
51251
  }
@@ -50905,37 +51270,37 @@ var readFlowFile = (abs, displayName) => {
50905
51270
  var test = async (args) => {
50906
51271
  const { js, warnings, useLocalAssets, root } = await compileHeadlessBundle(args);
50907
51272
  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");
51273
+ const testsDir = resolve12(root, flagStr(args, "dir") ?? "tests");
51274
+ const designMetaPath = resolve12(root, flagStr(args, "design-dir") ?? "design", "meta.json");
50910
51275
  const asJson = flagBool(args, "json");
50911
51276
  const out = asJson ? logErr : log;
50912
51277
  const cases = [];
50913
51278
  if (args._.length > 0) {
50914
51279
  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));
51280
+ const candidates = [resolve12(given), resolve12(testsDir, given), resolve12(testsDir, `${given}.flow.json`)];
51281
+ const abs = candidates.find((p2) => existsSync27(p2));
50917
51282
  if (!abs)
50918
51283
  throw new CliError(`No such test: "${given}" (looked in ${testsDir}).`);
50919
51284
  cases.push(readFlowFile(abs, given));
50920
51285
  }
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));
51286
+ } else if (existsSync27(testsDir)) {
51287
+ for (const f3 of readdirSync12(testsDir).filter((f4) => f4.endsWith(".flow.json")).sort()) {
51288
+ cases.push(readFlowFile(resolve12(testsDir, f3), f3));
50924
51289
  }
50925
51290
  }
50926
51291
  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;
51292
+ const defaultPack = resolve12(testsDir, "fixtures.json");
51293
+ const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") : existsSync27(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
50929
51294
  const flowsOnly = flagBool(args, "flows");
50930
51295
  const wantFlows = !flagBool(args, "no-flows") && args._.length === 0;
50931
51296
  if (flowsOnly)
50932
51297
  cases.length = 0;
50933
51298
  let skipped = [];
50934
51299
  let entryScreen = null;
50935
- if ((wantFlows || flowsOnly) && existsSync26(designMetaPath)) {
51300
+ if ((wantFlows || flowsOnly) && existsSync27(designMetaPath)) {
50936
51301
  let meta;
50937
51302
  try {
50938
- meta = JSON.parse(readFileSync25(designMetaPath, "utf8"));
51303
+ meta = JSON.parse(readFileSync26(designMetaPath, "utf8"));
50939
51304
  } catch (e2) {
50940
51305
  throw new CliError(`Can't read ${designMetaPath}: ${e2 instanceof Error ? e2.message : String(e2)}`);
50941
51306
  }
@@ -50967,7 +51332,7 @@ var test = async (args) => {
50967
51332
  }, timeoutMs);
50968
51333
  killer.unref?.();
50969
51334
  const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
50970
- const artifactsRoot = resolve11(root, flagStr(args, "artifacts") ?? resolve11(testsDir, ".artifacts"));
51335
+ const artifactsRoot = resolve12(root, flagStr(args, "artifacts") ?? resolve12(testsDir, ".artifacts"));
50971
51336
  const results = [];
50972
51337
  for (const kase of cases) {
50973
51338
  const flagSettle = flagStr(args, "settle");
@@ -50985,14 +51350,14 @@ var test = async (args) => {
50985
51350
  onConsole
50986
51351
  });
50987
51352
  try {
50988
- const io = fileScenarioIO(resolve11(artifactsRoot, slug3(kase.name)));
51353
+ const io = fileScenarioIO(resolve12(artifactsRoot, slug3(kase.name)));
50989
51354
  const outcome = await renderer.runScenario(session, steps, io);
50990
51355
  result = {
50991
51356
  name: kase.name,
50992
51357
  kind: kase.kind,
50993
51358
  ok: outcome.ok,
50994
51359
  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))
51360
+ artifacts: outcome.failureArtifacts.map((p2) => resolve12(artifactsRoot, slug3(kase.name), p2))
50996
51361
  };
50997
51362
  } finally {
50998
51363
  session.dispose();
@@ -51054,11 +51419,11 @@ init_config();
51054
51419
  init_manifest();
51055
51420
  init_types4();
51056
51421
  init_util();
51057
- import { resolve as resolve12 } from "node:path";
51422
+ import { resolve as resolve13 } from "node:path";
51058
51423
  var label = (bundle) => `${Object.keys(bundle).length} file(s)`;
51059
51424
  var types4 = async (args) => {
51060
51425
  const dirArg = args._[0];
51061
- const root = dirArg ? resolve12(dirArg) : (() => {
51426
+ const root = dirArg ? resolve13(dirArg) : (() => {
51062
51427
  try {
51063
51428
  return findProjectRoot(process.cwd());
51064
51429
  } catch {
@@ -51117,23 +51482,23 @@ var types4 = async (args) => {
51117
51482
  };
51118
51483
 
51119
51484
  // src/index.ts
51120
- import { existsSync as existsSync28, readFileSync as readFileSync27, realpathSync as realpathSync2 } from "node:fs";
51485
+ import { existsSync as existsSync29, readFileSync as readFileSync28, realpathSync as realpathSync2 } from "node:fs";
51121
51486
  import { createRequire as createRequire4 } from "node:module";
51122
- import { delimiter as delimiter2, dirname as dirname19, join as join27 } from "node:path";
51487
+ import { delimiter as delimiter2, dirname as dirname19, join as join28 } from "node:path";
51123
51488
 
51124
51489
  // src/commands/install.ts
51125
51490
  init_util();
51126
51491
  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";
51492
+ 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";
51493
+ import { homedir as homedir7 } from "node:os";
51494
+ import { basename as basename14, delimiter, dirname as dirname18, join as join27, resolve as resolve14, sep as sep2 } from "node:path";
51130
51495
  var isWin = process.platform === "win32";
51131
- var HOME = homedir6();
51496
+ var HOME = homedir7();
51132
51497
  var safeRealpath = (p2) => {
51133
51498
  try {
51134
51499
  return realpathSync(p2);
51135
51500
  } catch {
51136
- return resolve13(p2);
51501
+ return resolve14(p2);
51137
51502
  }
51138
51503
  };
51139
51504
  var onPath = (dir) => {
@@ -51150,24 +51515,24 @@ var onPath = (dir) => {
51150
51515
  };
51151
51516
  var install = async (args) => {
51152
51517
  const bundle = dirname18(process.execPath);
51153
- const exeName = basename14(process.execPath);
51518
+ const exeName2 = basename14(process.execPath);
51154
51519
  const chiselName = isWin ? "chisel.exe" : "chisel";
51155
- if (!existsSync27(join26(bundle, "runtime")) || !existsSync27(join26(bundle, chiselName))) {
51520
+ if (!existsSync28(join27(bundle, "runtime")) || !existsSync28(join27(bundle, chiselName))) {
51156
51521
  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
51522
  }
51158
- const companions = [exeName, chiselName, "runtime", "README.txt"].filter((f3) => existsSync27(join26(bundle, f3)));
51523
+ const companions = [exeName2, chiselName, "runtime", "README.txt"].filter((f3) => existsSync28(join27(bundle, f3)));
51159
51524
  if (isWin)
51160
51525
  return installWindows(args, bundle, companions);
51161
51526
  return installUnix(args, bundle, companions);
51162
51527
  };
51163
51528
  var copyBundle = (bundle, target, companions) => {
51164
- mkdirSync18(target, { recursive: true });
51529
+ mkdirSync19(target, { recursive: true });
51165
51530
  if (safeRealpath(target) === safeRealpath(bundle))
51166
51531
  return;
51167
51532
  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 });
51533
+ const dest = join27(target, f3);
51534
+ rmSync6(dest, { recursive: true, force: true });
51535
+ cpSync3(join27(bundle, f3), dest, { recursive: true });
51171
51536
  }
51172
51537
  };
51173
51538
  var permError = (e2, dir, system) => {
@@ -51179,41 +51544,41 @@ var permError = (e2, dir, system) => {
51179
51544
  };
51180
51545
  var installUnix = (args, bundle, companions) => {
51181
51546
  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");
51547
+ const libDir = resolve14(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join27(HOME, ".local", "lib", "lecodes-cli")));
51548
+ const binDir = resolve14(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join27(HOME, ".local", "bin")));
51549
+ const link2 = join27(binDir, "lecodes");
51185
51550
  try {
51186
51551
  copyBundle(bundle, libDir, companions);
51187
- chmodSync2(join26(libDir, "lecodes"), 493);
51188
- chmodSync2(join26(libDir, "chisel"), 493);
51552
+ chmodSync2(join27(libDir, "lecodes"), 493);
51553
+ chmodSync2(join27(libDir, "chisel"), 493);
51189
51554
  } catch (e2) {
51190
51555
  throw permError(e2, libDir, system);
51191
51556
  }
51192
51557
  try {
51193
- mkdirSync18(binDir, { recursive: true });
51194
- rmSync5(link2, { force: true });
51195
- symlinkSync(join26(libDir, "lecodes"), link2);
51558
+ mkdirSync19(binDir, { recursive: true });
51559
+ rmSync6(link2, { force: true });
51560
+ symlinkSync(join27(libDir, "lecodes"), link2);
51196
51561
  } catch (e2) {
51197
51562
  throw permError(e2, binDir, system);
51198
51563
  }
51199
51564
  success(`Installed to ${c.bold(libDir)}`);
51200
- log(` ${c.dim(link2 + " → " + join26(libDir, "lecodes"))}`);
51565
+ log(` ${c.dim(link2 + " → " + join27(libDir, "lecodes"))}`);
51201
51566
  finishPathUnix(args, binDir);
51202
51567
  };
51203
51568
  var shellRc = () => {
51204
51569
  const shell = basename14(process.env.SHELL ?? "");
51205
51570
  if (shell === "zsh")
51206
- return { file: join26(HOME, ".zshrc"), name: "~/.zshrc" };
51571
+ return { file: join27(HOME, ".zshrc"), name: "~/.zshrc" };
51207
51572
  if (shell === "bash")
51208
- return { file: join26(HOME, ".bashrc"), name: "~/.bashrc" };
51209
- return { file: join26(HOME, ".profile"), name: "~/.profile" };
51573
+ return { file: join27(HOME, ".bashrc"), name: "~/.bashrc" };
51574
+ return { file: join27(HOME, ".profile"), name: "~/.profile" };
51210
51575
  };
51211
51576
  var PATH_MARKER = "# Added by `lecodes install`";
51212
51577
  var appendPathToShellRc = (binDir) => {
51213
51578
  const { file, name } = shellRc();
51214
51579
  const line = `export PATH="${binDir}:$PATH"`;
51215
51580
  try {
51216
- const existing = existsSync27(file) ? readFileSync26(file, "utf8") : "";
51581
+ const existing = existsSync28(file) ? readFileSync27(file, "utf8") : "";
51217
51582
  if (existing.includes(line))
51218
51583
  return name;
51219
51584
  appendFileSync3(file, `${existing && !existing.endsWith(`
@@ -51251,7 +51616,7 @@ var finishPathUnix = (args, binDir) => {
51251
51616
  log(` • or install system-wide (no PATH edit): ${c.bold("sudo lecodes install --system")}`);
51252
51617
  };
51253
51618
  var installWindows = (args, bundle, companions) => {
51254
- const target = resolve13(flagStr(args, "dir") ?? join26(process.env.LOCALAPPDATA ?? join26(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
51619
+ const target = resolve14(flagStr(args, "dir") ?? join27(process.env.LOCALAPPDATA ?? join27(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
51255
51620
  try {
51256
51621
  copyBundle(bundle, target, companions);
51257
51622
  } catch (e2) {
@@ -51291,7 +51656,7 @@ init_util();
51291
51656
  var STANDALONE = (() => {
51292
51657
  const dir = dirname19(process.execPath);
51293
51658
  const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
51294
- return existsSync28(join27(dir, "runtime")) && existsSync28(join27(dir, chisel));
51659
+ return existsSync29(join28(dir, "runtime")) && existsSync29(join28(dir, chisel));
51295
51660
  })();
51296
51661
  var INSTALLED = STANDALONE && (() => {
51297
51662
  const exe = process.platform === "win32" ? "lecodes.exe" : "lecodes";
@@ -51306,8 +51671,8 @@ var INSTALLED = STANDALONE && (() => {
51306
51671
  if (!d)
51307
51672
  return false;
51308
51673
  try {
51309
- const cand = join27(d, exe);
51310
- return existsSync28(cand) && realpathSync2(cand) === self2;
51674
+ const cand = join28(d, exe);
51675
+ return existsSync29(cand) && realpathSync2(cand) === self2;
51311
51676
  } catch {
51312
51677
  return false;
51313
51678
  }
@@ -51396,6 +51761,14 @@ ${c.bold("Commands:")}
51396
51761
  --settle <ms> wait for promises/timers/fetch before snapshot
51397
51762
  --wait-network let data fetches hit the real network
51398
51763
  --logs forward the app's console output (to stderr)
51764
+ --desktop render through the NATIVE desktop host instead:
51765
+ real Filament 3D + tgfx UI + 2D, composited.
51766
+ PNG only (--png). Downloads the LeCodes
51767
+ Desktop renderer on first use into
51768
+ ~/.lecodes/renderer (Windows/Linux;
51769
+ LECODES_DESKTOP_EXE overrides)
51770
+ --frames <n> (--desktop) frames to run before the shot —
51771
+ headroom for async loads (default 60)
51399
51772
  test [name…] Run headless flow tests: tests/*.flow.json scenarios, plus flows
51400
51773
  derived from the design graph (design/meta.json activator edges
51401
51774
  — checks the built app against the designed navigation; needs
@@ -51435,6 +51808,17 @@ ${c.bold("Commands:")}
51435
51808
  --logs forward the screens' console output (to stderr)
51436
51809
  design comments Review comments pinned to the board, grouped by screen (open first)
51437
51810
  --all include resolved threads
51811
+ add "<text>" [--screen <id>] [--state <s>] [--element <name>]
51812
+ reply <number> "<text>" · resolve <number> [--reply "<text>"] [--reopen]
51813
+ (--ai signs a write as Claude; sends with the next "lecodes push")
51814
+ desktop [run|update] The native desktop host ('lecodes render --desktop' uses it too):
51815
+ no args shows the version status (selected / downloaded /
51816
+ newest published)
51817
+ run compile the current project and open it in a real
51818
+ desktop window (--width/--height set the window size;
51819
+ --entry overrides the entrypoint)
51820
+ update move to the newest published release
51821
+ (cached in ~/.lecodes/renderer)
51438
51822
  scene [path] LeCodes Scene Editor — visually edit a .scene.ts (tree, inspector,
51439
51823
  gizmo) on local files (needs the optional 'lecodes-3d-editor' package;
51440
51824
  works inside a cloned project or standalone in any folder)
@@ -51498,7 +51882,7 @@ ${c.bold("Commands:")}
51498
51882
  — 'new --env' banks it in .env), revoke <id>
51499
51883
 
51500
51884
  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 };
51885
+ 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
51886
  if (STANDALONE)
51503
51887
  commands.install = install;
51504
51888
  var readVersion = () => package_default.version ?? "unknown";
@@ -51512,9 +51896,9 @@ var peerVersion = (name, probe) => {
51512
51896
  const require2 = createRequire4(import.meta.url);
51513
51897
  let dir = dirname19(require2.resolve(probe));
51514
51898
  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"));
51899
+ const manifest = join28(dir, "package.json");
51900
+ if (existsSync29(manifest)) {
51901
+ const parsed = JSON.parse(readFileSync28(manifest, "utf8"));
51518
51902
  if (parsed.name === name)
51519
51903
  return parsed.version ?? "unknown";
51520
51904
  }
@@ -51532,6 +51916,8 @@ var printVersion = () => {
51532
51916
  const version = peerVersion(name, probe);
51533
51917
  rows.push([name, version ?? c.dim("not installed")]);
51534
51918
  }
51919
+ const desktopVersion = selectedDesktopVersion();
51920
+ rows.push(["lecodes-desktop", installedDesktopVersions().includes(desktopVersion) ? desktopVersion : `${desktopVersion} ${c.dim("(not downloaded — fetched on first `render --desktop`)")}`]);
51535
51921
  const width = Math.max(...rows.map(([label2]) => label2.length));
51536
51922
  for (const [label2, value] of rows)
51537
51923
  log(`${label2.padEnd(width)} ${value}`);
@@ -51547,14 +51933,14 @@ var main = async () => {
51547
51933
  printVersion();
51548
51934
  return;
51549
51935
  }
51550
- const run = commands[command];
51551
- if (!run) {
51936
+ const run2 = commands[command];
51937
+ if (!run2) {
51552
51938
  log(c.red(`Unknown command: ${command}`));
51553
51939
  log(HELP);
51554
51940
  process.exitCode = 1;
51555
51941
  return;
51556
51942
  }
51557
- await run(parseArgs(argv.slice(1)));
51943
+ await run2(parseArgs(argv.slice(1)));
51558
51944
  };
51559
51945
  main().catch((e2) => {
51560
51946
  if (e2 instanceof CliError)