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 +612 -226
- package/package.json +4 -4
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk/g2/autotile.ts +39 -0
- package/runtime/sdk/gl/Camera.ts +40 -0
- package/runtime/sdk/inject.ts +1 -0
- package/runtime/sdk/scene/defineScene.ts +43 -13
- package/runtime/sdk/scene/editorPlugins.ts +1 -1
- package/runtime/sdk-types.json +1 -1
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
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
|
43013
|
+
let resolve8;
|
|
43014
43014
|
let reject;
|
|
43015
43015
|
const tokenPromise = new Promise((res, rej) => {
|
|
43016
|
-
|
|
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
|
-
|
|
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\`").
|
|
43473
|
-
the \`design_comments\` tool (or \`lecodes design comments\`), which is the closest
|
|
43474
|
-
has to a backlog. **Open threads are the work.** A comment tells you which file 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
|
|
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\`
|
|
43486
|
-
while it runs
|
|
43487
|
-
|
|
43488
|
-
|
|
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
|
|
43541
|
-
import { basename as basename7, join as
|
|
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:
|
|
43563
|
+
designDir: join19(root, dirName),
|
|
43559
43564
|
designKey: "/" + dirName.replace(/\\/g, "/")
|
|
43560
43565
|
};
|
|
43561
43566
|
}, loadDesignServer = async (ctx, needs = []) => {
|
|
43562
|
-
if (!
|
|
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
|
|
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 =
|
|
43595
|
-
if (!
|
|
43599
|
+
const dir = join19(designDir, "screens");
|
|
43600
|
+
if (!existsSync19(dir))
|
|
43596
43601
|
return [];
|
|
43597
|
-
return
|
|
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(
|
|
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 =
|
|
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 (
|
|
43614
|
+
if (existsSync19(path2)) {
|
|
43610
43615
|
try {
|
|
43611
|
-
config = JSON.parse(
|
|
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
|
-
|
|
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
|
-
[
|
|
43634
|
-
[
|
|
43635
|
-
[
|
|
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
|
-
|
|
43638
|
-
|
|
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 =
|
|
43642
|
-
if (
|
|
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
|
-
|
|
43651
|
+
writeFileSync15(abs, content);
|
|
43647
43652
|
log(`${c.green("created")} ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
|
|
43648
43653
|
created++;
|
|
43649
43654
|
}
|
|
43650
|
-
const ignorePath =
|
|
43655
|
+
const ignorePath = join19(ctx.root, ".lecodesignore");
|
|
43651
43656
|
const ignoreLines = [`${ctx.dirName}/.snapshots/`, `${ctx.dirName}/comments/`];
|
|
43652
|
-
const current =
|
|
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
|
-
|
|
43668
|
-
|
|
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") ??
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
43767
|
-
log(`${c.green("✓")} ${tag} → ${
|
|
43771
|
+
writeFileSync15(join19(outDir, `${base}.json`), result.text);
|
|
43772
|
+
log(`${c.green("✓")} ${tag} → ${join19(outDir, `${base}.json`)}`);
|
|
43768
43773
|
if (pngResult) {
|
|
43769
|
-
|
|
43770
|
-
log(`${c.green("✓")} ${tag} → ${
|
|
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 (!
|
|
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
|
-
|
|
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.
|
|
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.
|
|
45163
|
-
"lecodes-renderer": "^0.
|
|
45164
|
-
"lecodes-3d-editor": "^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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
49309
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
48965
49310
|
var WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
48966
|
-
var acceptKey = (key) =>
|
|
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,
|
|
49110
|
-
res.writeHead(
|
|
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,
|
|
49114
|
-
var readBody = (req) => new Promise((
|
|
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", () =>
|
|
49121
|
-
req.on("error", () =>
|
|
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
|
|
49162
|
-
compileChain =
|
|
49163
|
-
return
|
|
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 || !
|
|
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((
|
|
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
|
-
|
|
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 =
|
|
49650
|
+
const path2 = join20(root, LECODES_DIR, "dev.json");
|
|
49306
49651
|
try {
|
|
49307
|
-
const token2 = JSON.parse(
|
|
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
|
-
|
|
49313
|
-
|
|
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((
|
|
49665
|
+
await new Promise((resolve8) => qrcode.generate(url, { small: true }, (out) => {
|
|
49321
49666
|
log(out);
|
|
49322
|
-
|
|
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 (
|
|
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
|
|
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 ?
|
|
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 =
|
|
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(
|
|
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
|
|
49633
|
-
import { basename as basename10, dirname as dirname13, join as
|
|
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 =
|
|
50076
|
+
const root = resolve8(dirArg);
|
|
49732
50077
|
const name = basename10(root);
|
|
49733
|
-
|
|
50078
|
+
mkdirSync16(root, { recursive: true });
|
|
49734
50079
|
let created = 0;
|
|
49735
50080
|
const write = (rel, content) => {
|
|
49736
|
-
const abs =
|
|
49737
|
-
if (
|
|
50081
|
+
const abs = join21(root, rel);
|
|
50082
|
+
if (existsSync21(abs)) {
|
|
49738
50083
|
note(`kept ${rel}`);
|
|
49739
50084
|
return;
|
|
49740
50085
|
}
|
|
49741
|
-
|
|
49742
|
-
|
|
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
|
|
49772
|
-
import { basename as basename12, join as
|
|
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
|
|
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 =
|
|
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),
|
|
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 ?
|
|
49953
|
-
if (!
|
|
50297
|
+
const root = dirArg ? resolve9(dirArg) : process.cwd();
|
|
50298
|
+
if (!existsSync22(root))
|
|
49954
50299
|
throw new CliError(`No such directory: ${root}`);
|
|
49955
|
-
if (
|
|
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
|
|
50032
|
-
import { dirname as dirname14, resolve as
|
|
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 =
|
|
50143
|
-
|
|
50144
|
-
|
|
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 =
|
|
50497
|
+
const abs = resolve10(baseDir, value);
|
|
50153
50498
|
try {
|
|
50154
|
-
map = JSON.parse(
|
|
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(
|
|
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(
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
50289
|
-
import { join as
|
|
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 =
|
|
50798
|
+
const envPath = join23(root, ".env");
|
|
50434
50799
|
const line = `${ENV_VAR}=${key}`;
|
|
50435
|
-
const existing =
|
|
50800
|
+
const existing = existsSync23(envPath) ? readFileSync23(envPath, "utf8") : "";
|
|
50436
50801
|
if (new RegExp(`^${ENV_VAR}=`, "m").test(existing)) {
|
|
50437
|
-
|
|
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 =
|
|
50447
|
-
const ignore =
|
|
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
|
|
50547
|
-
import { dirname as dirname15, join as
|
|
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 =
|
|
50571
|
-
if (
|
|
50572
|
-
|
|
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 =
|
|
50610
|
-
|
|
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
|
-
|
|
50981
|
+
writeFileSync20(abs, bytes);
|
|
50617
50982
|
sha = sha256(bytes);
|
|
50618
50983
|
} else {
|
|
50619
50984
|
const text = asset.text ?? "";
|
|
50620
|
-
|
|
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
|
|
50634
|
-
import { basename as basename13, dirname as dirname16, join as
|
|
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
|
|
50643
|
-
import { join as
|
|
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 =
|
|
51012
|
+
const inRepo = join25(distRoot, "..", "projects", "src", "scene-editor");
|
|
50648
51013
|
try {
|
|
50649
|
-
return loadSceneHarness2(
|
|
51014
|
+
return loadSceneHarness2(existsSync25(inRepo) ? inRepo : undefined);
|
|
50650
51015
|
} catch {
|
|
50651
|
-
const vendored =
|
|
50652
|
-
if (
|
|
50653
|
-
return JSON.parse(
|
|
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:
|
|
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 =
|
|
50708
|
-
if (
|
|
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") &&
|
|
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 =
|
|
51084
|
+
const path2 = join26(root, ".mcp.json");
|
|
50720
51085
|
const entry = { type: "http", url: `http://localhost:${port}/mcp` };
|
|
50721
51086
|
let config = {};
|
|
50722
|
-
if (
|
|
51087
|
+
if (existsSync26(path2)) {
|
|
50723
51088
|
try {
|
|
50724
|
-
config = JSON.parse(
|
|
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
|
-
|
|
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
|
|
51108
|
+
for (const name of readdirSync11(dir)) {
|
|
50744
51109
|
if (name === "node_modules" || name.startsWith("."))
|
|
50745
51110
|
continue;
|
|
50746
|
-
const full =
|
|
50747
|
-
if (
|
|
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
|
|
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
|
|
50821
|
-
import { dirname as dirname17, resolve as
|
|
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(
|
|
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 =
|
|
50909
|
-
const designMetaPath =
|
|
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 = [
|
|
50916
|
-
const abs = candidates.find((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 (
|
|
50922
|
-
for (const f3 of
|
|
50923
|
-
cases.push(readFlowFile(
|
|
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 =
|
|
50928
|
-
const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") :
|
|
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) &&
|
|
51300
|
+
if ((wantFlows || flowsOnly) && existsSync27(designMetaPath)) {
|
|
50936
51301
|
let meta;
|
|
50937
51302
|
try {
|
|
50938
|
-
meta = JSON.parse(
|
|
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 =
|
|
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(
|
|
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) =>
|
|
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
|
|
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 ?
|
|
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
|
|
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
|
|
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
|
|
51128
|
-
import { homedir as
|
|
51129
|
-
import { basename as basename14, delimiter, dirname as dirname18, join as
|
|
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 =
|
|
51496
|
+
var HOME = homedir7();
|
|
51132
51497
|
var safeRealpath = (p2) => {
|
|
51133
51498
|
try {
|
|
51134
51499
|
return realpathSync(p2);
|
|
51135
51500
|
} catch {
|
|
51136
|
-
return
|
|
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
|
|
51518
|
+
const exeName2 = basename14(process.execPath);
|
|
51154
51519
|
const chiselName = isWin ? "chisel.exe" : "chisel";
|
|
51155
|
-
if (!
|
|
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 = [
|
|
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
|
-
|
|
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 =
|
|
51169
|
-
|
|
51170
|
-
cpSync3(
|
|
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 =
|
|
51183
|
-
const binDir =
|
|
51184
|
-
const link2 =
|
|
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(
|
|
51188
|
-
chmodSync2(
|
|
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
|
-
|
|
51194
|
-
|
|
51195
|
-
symlinkSync(
|
|
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 + " → " +
|
|
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:
|
|
51571
|
+
return { file: join27(HOME, ".zshrc"), name: "~/.zshrc" };
|
|
51207
51572
|
if (shell === "bash")
|
|
51208
|
-
return { file:
|
|
51209
|
-
return { file:
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
51310
|
-
return
|
|
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 =
|
|
51516
|
-
if (
|
|
51517
|
-
const parsed = JSON.parse(
|
|
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
|
|
51551
|
-
if (!
|
|
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
|
|
51943
|
+
await run2(parseArgs(argv.slice(1)));
|
|
51558
51944
|
};
|
|
51559
51945
|
main().catch((e2) => {
|
|
51560
51946
|
if (e2 instanceof CliError)
|