lecodes-cli 0.12.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +637 -235
- 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 +259 -117
- package/runtime/sdk/scene/editorPlugins.ts +23 -17
- package/runtime/sdk/scene/grammar.ts +49 -14
- 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.1",
|
|
45150
45217
|
dependencies: {
|
|
45151
45218
|
"@letary/chisel": "^0.6.0",
|
|
45152
45219
|
jimp: "^1.6.1"
|
|
@@ -45159,9 +45226,9 @@ var package_default = {
|
|
|
45159
45226
|
sdk: "workspace:*"
|
|
45160
45227
|
},
|
|
45161
45228
|
peerDependencies: {
|
|
45162
|
-
"lecodes-design": "^0.
|
|
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();
|
|
@@ -49291,35 +49636,46 @@ var DEFAULT_PORT2 = 4499;
|
|
|
49291
49636
|
var DEBOUNCE_MS = 120;
|
|
49292
49637
|
var WATCH_SKIP_DIRS = new Set([".lecodes", ".git", "node_modules"]);
|
|
49293
49638
|
var WATCH_SKIP_FILES = new Set(["tsconfig.json", "jsconfig.json", ".gitignore", ".lecodesignore", ".DS_Store"]);
|
|
49294
|
-
var
|
|
49639
|
+
var VIRTUAL_IFACE = /wsl|docker|hyper-v|vethernet|virtualbox|vmware|tap|tun|zerotier|tailscale|loopback/i;
|
|
49640
|
+
var lanCandidates = () => {
|
|
49295
49641
|
const nets = networkInterfaces();
|
|
49296
|
-
|
|
49297
|
-
|
|
49298
|
-
|
|
49299
|
-
|
|
49300
|
-
|
|
49301
|
-
|
|
49302
|
-
|
|
49642
|
+
const out = [];
|
|
49643
|
+
for (const iface of Object.keys(nets)) {
|
|
49644
|
+
for (const net of nets[iface] ?? []) {
|
|
49645
|
+
if (net.family !== "IPv4" || net.internal)
|
|
49646
|
+
continue;
|
|
49647
|
+
const a2 = net.address;
|
|
49648
|
+
let rank = 0;
|
|
49649
|
+
if (VIRTUAL_IFACE.test(iface))
|
|
49650
|
+
rank += 4;
|
|
49651
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(a2))
|
|
49652
|
+
rank += 2;
|
|
49653
|
+
if (a2.startsWith("169.254."))
|
|
49654
|
+
rank += 8;
|
|
49655
|
+
out.push({ address: a2, iface, rank });
|
|
49656
|
+
}
|
|
49657
|
+
}
|
|
49658
|
+
return out.sort((x2, y2) => x2.rank - y2.rank).map(({ address, iface }) => ({ address, iface }));
|
|
49303
49659
|
};
|
|
49304
49660
|
var loadDevToken = (root) => {
|
|
49305
|
-
const path2 =
|
|
49661
|
+
const path2 = join20(root, LECODES_DIR, "dev.json");
|
|
49306
49662
|
try {
|
|
49307
|
-
const token2 = JSON.parse(
|
|
49663
|
+
const token2 = JSON.parse(readFileSync19(path2, "utf8")).token;
|
|
49308
49664
|
if (token2 && /^[a-z0-9]{8,}$/.test(token2))
|
|
49309
49665
|
return token2;
|
|
49310
49666
|
} catch {}
|
|
49311
49667
|
const token = randomBytes2(6).toString("hex");
|
|
49312
|
-
|
|
49313
|
-
|
|
49668
|
+
mkdirSync15(join20(root, LECODES_DIR), { recursive: true });
|
|
49669
|
+
writeFileSync16(path2, JSON.stringify({ token }, null, 2) + `
|
|
49314
49670
|
`);
|
|
49315
49671
|
return token;
|
|
49316
49672
|
};
|
|
49317
49673
|
var printQr = async (url) => {
|
|
49318
49674
|
try {
|
|
49319
49675
|
const qrcode = (await Promise.resolve().then(() => __toESM(require_main(), 1))).default;
|
|
49320
|
-
await new Promise((
|
|
49676
|
+
await new Promise((resolve8) => qrcode.generate(url, { small: true }, (out) => {
|
|
49321
49677
|
log(out);
|
|
49322
|
-
|
|
49678
|
+
resolve8();
|
|
49323
49679
|
}));
|
|
49324
49680
|
} catch {
|
|
49325
49681
|
note("(couldn't render a QR code — open the URL below on the device)");
|
|
@@ -49340,9 +49696,11 @@ var dev = async (args) => {
|
|
|
49340
49696
|
warnErr("Running under Bun: the websocket reload channel doesn't work here — run `lecodes dev` with Node (or use ?t=poll on the device URL).");
|
|
49341
49697
|
}
|
|
49342
49698
|
const port = Number(flagStr(args, "port")) || DEFAULT_PORT2;
|
|
49343
|
-
const
|
|
49699
|
+
const candidates = lanCandidates();
|
|
49700
|
+
const host = flagStr(args, "host") ?? candidates[0]?.address;
|
|
49344
49701
|
if (!host)
|
|
49345
49702
|
throw new CliError("No LAN address found — pass one with --host <ip>.");
|
|
49703
|
+
const alternatives = candidates.filter((c4) => c4.address !== host);
|
|
49346
49704
|
const origin = `http://${host}:${port}`;
|
|
49347
49705
|
const token = loadDevToken(root);
|
|
49348
49706
|
const server = await startDevServer({
|
|
@@ -49422,7 +49780,10 @@ ${result.error}`);
|
|
|
49422
49780
|
await printQr(server.url);
|
|
49423
49781
|
log(`Scan the QR with the LeCodes app, or open: ${c.bold(server.url)}`);
|
|
49424
49782
|
note(`Older desktop builds (no websocket bridge): ${server.url}?t=poll`);
|
|
49425
|
-
if (
|
|
49783
|
+
if (alternatives.length > 0) {
|
|
49784
|
+
note(`Device can't reach it? This machine also has ${alternatives.map((a2) => `${a2.address} (${a2.iface})`).join(", ")} — retry with --host <ip>.`);
|
|
49785
|
+
}
|
|
49786
|
+
if (existsSync20(join20(root, LECODES_DIR, "manifest.json")))
|
|
49426
49787
|
note(`Token stored in ${LECODES_DIR}/dev.json (delete it to rotate the URL).`);
|
|
49427
49788
|
log("");
|
|
49428
49789
|
log(`Watching ${root} — save a file to hot-reload. Ctrl+C to stop.`);
|
|
@@ -49436,7 +49797,7 @@ init_localFiles();
|
|
|
49436
49797
|
|
|
49437
49798
|
// src/serverDiff.ts
|
|
49438
49799
|
init_localFiles();
|
|
49439
|
-
import { readFileSync as
|
|
49800
|
+
import { readFileSync as readFileSync20 } from "node:fs";
|
|
49440
49801
|
import { basename as basename9 } from "node:path";
|
|
49441
49802
|
var isTextAsset = (t2) => t2 === "text" || t2 === "shader" || t2 === "scene";
|
|
49442
49803
|
var computeServerDiff = (project, scan2) => {
|
|
@@ -49455,7 +49816,7 @@ var computeServerDiff = (project, scan2) => {
|
|
|
49455
49816
|
const local = localByPath.get(path2);
|
|
49456
49817
|
if (local && !server) {
|
|
49457
49818
|
const text = isTextKind(classifyKind(basename9(path2)));
|
|
49458
|
-
entries.push({ path: path2, status: "added", binary: !text, serverText: "", localText: text ?
|
|
49819
|
+
entries.push({ path: path2, status: "added", binary: !text, serverText: "", localText: text ? readFileSync20(local.absPath, "utf8") : undefined });
|
|
49459
49820
|
continue;
|
|
49460
49821
|
}
|
|
49461
49822
|
if (server && !local) {
|
|
@@ -49466,12 +49827,12 @@ var computeServerDiff = (project, scan2) => {
|
|
|
49466
49827
|
if (server && local) {
|
|
49467
49828
|
if (isTextAsset(server.type)) {
|
|
49468
49829
|
const serverText = server.text ?? "";
|
|
49469
|
-
const localText =
|
|
49830
|
+
const localText = readFileSync20(local.absPath, "utf8");
|
|
49470
49831
|
if (serverText !== localText)
|
|
49471
49832
|
entries.push({ path: path2, status: "modified", binary: false, serverText, localText });
|
|
49472
49833
|
} else {
|
|
49473
49834
|
const serverSize = server.file?.size;
|
|
49474
|
-
const localSize = Buffer.byteLength(
|
|
49835
|
+
const localSize = Buffer.byteLength(readFileSync20(local.absPath));
|
|
49475
49836
|
if (serverSize !== undefined && serverSize !== localSize) {
|
|
49476
49837
|
entries.push({ path: path2, status: "modified", binary: true });
|
|
49477
49838
|
}
|
|
@@ -49629,8 +49990,8 @@ var statusLabel = (e2) => e2.status === "added" ? c.green(" added ") : e2.st
|
|
|
49629
49990
|
init_manifest();
|
|
49630
49991
|
init_types4();
|
|
49631
49992
|
init_ignore();
|
|
49632
|
-
import { existsSync as
|
|
49633
|
-
import { basename as basename10, dirname as dirname13, join as
|
|
49993
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync16, writeFileSync as writeFileSync17 } from "node:fs";
|
|
49994
|
+
import { basename as basename10, dirname as dirname13, join as join21, resolve as resolve8 } from "node:path";
|
|
49634
49995
|
init_util();
|
|
49635
49996
|
var STARTER2 = `// Welcome to LeCodes — the whole SDK is available as globals, no imports needed.
|
|
49636
49997
|
// Run it live on a device with \`lecodes dev\`, or bundle it with \`lecodes compile -o app.js\`.
|
|
@@ -49728,18 +50089,18 @@ Full command list: \`lecodes --help\`.
|
|
|
49728
50089
|
`;
|
|
49729
50090
|
var init2 = async (args) => {
|
|
49730
50091
|
const dirArg = (args._[0] ?? ".").replace(/[/\\]+$/, "") || ".";
|
|
49731
|
-
const root =
|
|
50092
|
+
const root = resolve8(dirArg);
|
|
49732
50093
|
const name = basename10(root);
|
|
49733
|
-
|
|
50094
|
+
mkdirSync16(root, { recursive: true });
|
|
49734
50095
|
let created = 0;
|
|
49735
50096
|
const write = (rel, content) => {
|
|
49736
|
-
const abs =
|
|
49737
|
-
if (
|
|
50097
|
+
const abs = join21(root, rel);
|
|
50098
|
+
if (existsSync21(abs)) {
|
|
49738
50099
|
note(`kept ${rel}`);
|
|
49739
50100
|
return;
|
|
49740
50101
|
}
|
|
49741
|
-
|
|
49742
|
-
|
|
50102
|
+
mkdirSync16(dirname13(abs), { recursive: true });
|
|
50103
|
+
writeFileSync17(abs, content);
|
|
49743
50104
|
log(`${c.green("created")} ${rel}`);
|
|
49744
50105
|
created++;
|
|
49745
50106
|
};
|
|
@@ -49768,8 +50129,8 @@ init_config();
|
|
|
49768
50129
|
init_ignore();
|
|
49769
50130
|
init_localFiles();
|
|
49770
50131
|
init_manifest();
|
|
49771
|
-
import { existsSync as
|
|
49772
|
-
import { basename as basename12, join as
|
|
50132
|
+
import { existsSync as existsSync22 } from "node:fs";
|
|
50133
|
+
import { basename as basename12, join as join22, resolve as resolve9 } from "node:path";
|
|
49773
50134
|
init_types4();
|
|
49774
50135
|
init_util();
|
|
49775
50136
|
|
|
@@ -49779,7 +50140,7 @@ init_config();
|
|
|
49779
50140
|
init_manifest();
|
|
49780
50141
|
init_localFiles();
|
|
49781
50142
|
import { basename as basename11 } from "node:path";
|
|
49782
|
-
import { readFileSync as
|
|
50143
|
+
import { readFileSync as readFileSync21 } from "node:fs";
|
|
49783
50144
|
init_util();
|
|
49784
50145
|
var push = async (args) => {
|
|
49785
50146
|
const root = findProjectRoot(process.cwd());
|
|
@@ -49840,9 +50201,9 @@ var push = async (args) => {
|
|
|
49840
50201
|
if (source)
|
|
49841
50202
|
entry.assetId = source.id;
|
|
49842
50203
|
if (isTextKind(kind)) {
|
|
49843
|
-
entry.text =
|
|
50204
|
+
entry.text = readFileSync21(file.absPath, "utf8");
|
|
49844
50205
|
} else if (!source || source.sha !== file.sha) {
|
|
49845
|
-
const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename11(file.path),
|
|
50206
|
+
const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename11(file.path), readFileSync21(file.absPath));
|
|
49846
50207
|
entry.blobFileId = blob.id;
|
|
49847
50208
|
uploaded++;
|
|
49848
50209
|
}
|
|
@@ -49949,10 +50310,10 @@ var link = async (args) => {
|
|
|
49949
50310
|
const apiUrl = requireApiUrl(config);
|
|
49950
50311
|
const token = requireToken(config);
|
|
49951
50312
|
const dirArg = args._[0];
|
|
49952
|
-
const root = dirArg ?
|
|
49953
|
-
if (!
|
|
50313
|
+
const root = dirArg ? resolve9(dirArg) : process.cwd();
|
|
50314
|
+
if (!existsSync22(root))
|
|
49954
50315
|
throw new CliError(`No such directory: ${root}`);
|
|
49955
|
-
if (
|
|
50316
|
+
if (existsSync22(join22(root, LECODES_DIR, "manifest.json")) && !flagBool(args, "force")) {
|
|
49956
50317
|
throw new CliError("This folder is already linked (.lecodes/manifest.json exists) — use `lecodes push`, or `lecodes link --force` to re-link it to another project.");
|
|
49957
50318
|
}
|
|
49958
50319
|
let enclosing = null;
|
|
@@ -50028,8 +50389,8 @@ var login = async (args) => {
|
|
|
50028
50389
|
};
|
|
50029
50390
|
|
|
50030
50391
|
// src/commands/render.ts
|
|
50031
|
-
import { mkdirSync as
|
|
50032
|
-
import { dirname as dirname14, resolve as
|
|
50392
|
+
import { mkdirSync as mkdirSync17, readFileSync as readFileSync22, writeFileSync as writeFileSync18 } from "node:fs";
|
|
50393
|
+
import { dirname as dirname14, resolve as resolve10 } from "node:path";
|
|
50033
50394
|
|
|
50034
50395
|
// src/compile/headlessBundle.ts
|
|
50035
50396
|
init_api();
|
|
@@ -50139,9 +50500,9 @@ var requireRenderer = async () => {
|
|
|
50139
50500
|
};
|
|
50140
50501
|
var fileScenarioIO = (dir) => ({
|
|
50141
50502
|
write: (relPath, data) => {
|
|
50142
|
-
const abs =
|
|
50143
|
-
|
|
50144
|
-
|
|
50503
|
+
const abs = resolve10(dir, relPath);
|
|
50504
|
+
mkdirSync17(dirname14(abs), { recursive: true });
|
|
50505
|
+
writeFileSync18(abs, data);
|
|
50145
50506
|
}
|
|
50146
50507
|
});
|
|
50147
50508
|
var resolveFixtures = (value, baseDir) => {
|
|
@@ -50149,9 +50510,9 @@ var resolveFixtures = (value, baseDir) => {
|
|
|
50149
50510
|
return;
|
|
50150
50511
|
let map = value;
|
|
50151
50512
|
if (typeof value === "string") {
|
|
50152
|
-
const abs =
|
|
50513
|
+
const abs = resolve10(baseDir, value);
|
|
50153
50514
|
try {
|
|
50154
|
-
map = JSON.parse(
|
|
50515
|
+
map = JSON.parse(readFileSync22(abs, "utf8"));
|
|
50155
50516
|
} catch (e2) {
|
|
50156
50517
|
throw new CliError(`Can't read fixtures "${value}": ${e2 instanceof Error ? e2.message : String(e2)}`);
|
|
50157
50518
|
}
|
|
@@ -50164,7 +50525,7 @@ var resolveFixtures = (value, baseDir) => {
|
|
|
50164
50525
|
var readScenarioFile = (path2) => {
|
|
50165
50526
|
let raw;
|
|
50166
50527
|
try {
|
|
50167
|
-
raw = JSON.parse(
|
|
50528
|
+
raw = JSON.parse(readFileSync22(resolve10(path2), "utf8"));
|
|
50168
50529
|
} catch (e2) {
|
|
50169
50530
|
throw new CliError(`Can't read scenario "${path2}": ${e2 instanceof Error ? e2.message : String(e2)}`);
|
|
50170
50531
|
}
|
|
@@ -50177,7 +50538,27 @@ var readScenarioFile = (path2) => {
|
|
|
50177
50538
|
throw new CliError(`Scenario "${path2}" has no "steps".`);
|
|
50178
50539
|
return file;
|
|
50179
50540
|
};
|
|
50541
|
+
var renderDesktop = async (args) => {
|
|
50542
|
+
const width = Number(flagStr(args, "width")) || 390;
|
|
50543
|
+
const height = Number(flagStr(args, "height")) || 844;
|
|
50544
|
+
const frames = Number(flagStr(args, "frames")) || 60;
|
|
50545
|
+
const timeoutMs = Number(flagStr(args, "timeout")) || 60000;
|
|
50546
|
+
const pngFlag = flagStr(args, "png") ?? "screenshot.png";
|
|
50547
|
+
const { js, resources } = await buildBundle(args);
|
|
50548
|
+
note("Rendering (native)…");
|
|
50549
|
+
await runDesktopRender(js, resources, {
|
|
50550
|
+
width,
|
|
50551
|
+
height,
|
|
50552
|
+
frames,
|
|
50553
|
+
outPng: pngFlag,
|
|
50554
|
+
logs: flagBool(args, "logs"),
|
|
50555
|
+
timeoutMs
|
|
50556
|
+
});
|
|
50557
|
+
note(`Wrote ${pngFlag} (${width}x${height}, native render)`);
|
|
50558
|
+
};
|
|
50180
50559
|
var render = async (args) => {
|
|
50560
|
+
if (flagBool(args, "desktop"))
|
|
50561
|
+
return renderDesktop(args);
|
|
50181
50562
|
const scriptFlag = flagStr(args, "script");
|
|
50182
50563
|
const scenario = scriptFlag !== undefined ? readScenarioFile(scriptFlag) : null;
|
|
50183
50564
|
const stepCount = scenario && Array.isArray(scenario.steps) ? scenario.steps.length : 0;
|
|
@@ -50194,7 +50575,7 @@ var render = async (args) => {
|
|
|
50194
50575
|
const settleStr = flagStr(args, "settle") ?? (scenario?.settle !== undefined ? String(scenario.settle) : undefined);
|
|
50195
50576
|
const timeStr = flagStr(args, "time");
|
|
50196
50577
|
const onConsole = flagBool(args, "logs") ? (level, text2) => logErr(`${c.dim(`[${level}]`)} ${text2}`) : undefined;
|
|
50197
|
-
const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname14(
|
|
50578
|
+
const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname14(resolve10(scriptFlag)) : ".");
|
|
50198
50579
|
const flagFixtures = resolveFixtures(flagStr(args, "fixtures"), ".");
|
|
50199
50580
|
const fixtures = envelopeFixtures || flagFixtures ? { ...envelopeFixtures, ...flagFixtures } : undefined;
|
|
50200
50581
|
if (fixtures) {
|
|
@@ -50233,7 +50614,7 @@ var render = async (args) => {
|
|
|
50233
50614
|
${c.red(st.error ?? "failed")}`);
|
|
50234
50615
|
}
|
|
50235
50616
|
if (!result2.ok && result2.failureArtifacts.length > 0) {
|
|
50236
|
-
logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p2) =>
|
|
50617
|
+
logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p2) => resolve10(outDir, p2)).join(", ")}`));
|
|
50237
50618
|
}
|
|
50238
50619
|
logErr(result2.ok ? c.green(`✓ ${result2.steps.length}/${steps.length} steps passed`) : c.red(`✗ failed at step ${result2.steps.length} of ${steps.length}`));
|
|
50239
50620
|
return done((cb) => cb(), result2.ok ? 0 : 1);
|
|
@@ -50255,7 +50636,7 @@ var render = async (args) => {
|
|
|
50255
50636
|
if (isNullSink(pngFlag)) {
|
|
50256
50637
|
note(`Discarded PNG (${pngFlag})`);
|
|
50257
50638
|
} else {
|
|
50258
|
-
|
|
50639
|
+
writeFileSync18(resolve10(pngFlag), png2);
|
|
50259
50640
|
note(`Wrote ${pngFlag} (${png2.length} bytes, ${width}x${height})`);
|
|
50260
50641
|
}
|
|
50261
50642
|
return done((cb) => cb());
|
|
@@ -50270,7 +50651,7 @@ var render = async (args) => {
|
|
|
50270
50651
|
return done((cb) => cb());
|
|
50271
50652
|
}
|
|
50272
50653
|
if (out) {
|
|
50273
|
-
|
|
50654
|
+
writeFileSync18(resolve10(out), result.text);
|
|
50274
50655
|
note(`Wrote ${out} (${result.text.length} bytes)`);
|
|
50275
50656
|
return done((cb) => cb());
|
|
50276
50657
|
}
|
|
@@ -50285,8 +50666,8 @@ init_api();
|
|
|
50285
50666
|
init_config();
|
|
50286
50667
|
init_manifest();
|
|
50287
50668
|
init_util();
|
|
50288
|
-
import { appendFileSync as appendFileSync2, existsSync as
|
|
50289
|
-
import { join as
|
|
50669
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync23, readFileSync as readFileSync23, writeFileSync as writeFileSync19 } from "node:fs";
|
|
50670
|
+
import { join as join23 } from "node:path";
|
|
50290
50671
|
var ENV_VAR = "LECODES_PUSH_KEY";
|
|
50291
50672
|
var context = () => {
|
|
50292
50673
|
const root = findProjectRoot(process.cwd());
|
|
@@ -50430,11 +50811,11 @@ var pnKeysList = async (ctx) => {
|
|
|
50430
50811
|
};
|
|
50431
50812
|
var writeEnv = (root, key) => {
|
|
50432
50813
|
const touched = [];
|
|
50433
|
-
const envPath =
|
|
50814
|
+
const envPath = join23(root, ".env");
|
|
50434
50815
|
const line = `${ENV_VAR}=${key}`;
|
|
50435
|
-
const existing =
|
|
50816
|
+
const existing = existsSync23(envPath) ? readFileSync23(envPath, "utf8") : "";
|
|
50436
50817
|
if (new RegExp(`^${ENV_VAR}=`, "m").test(existing)) {
|
|
50437
|
-
|
|
50818
|
+
writeFileSync19(envPath, existing.replace(new RegExp(`^${ENV_VAR}=.*$`, "m"), line));
|
|
50438
50819
|
} else {
|
|
50439
50820
|
const separator = !existing || existing.endsWith(`
|
|
50440
50821
|
`) ? "" : `
|
|
@@ -50443,8 +50824,8 @@ var writeEnv = (root, key) => {
|
|
|
50443
50824
|
`);
|
|
50444
50825
|
}
|
|
50445
50826
|
touched.push(".env");
|
|
50446
|
-
const ignorePath =
|
|
50447
|
-
const ignore =
|
|
50827
|
+
const ignorePath = join23(root, ".gitignore");
|
|
50828
|
+
const ignore = existsSync23(ignorePath) ? readFileSync23(ignorePath, "utf8") : "";
|
|
50448
50829
|
if (!ignore.split(`
|
|
50449
50830
|
`).some((l2) => l2.trim() === ".env")) {
|
|
50450
50831
|
const separator = !ignore || ignore.endsWith(`
|
|
@@ -50543,8 +50924,8 @@ var pn = async (args) => {
|
|
|
50543
50924
|
init_api();
|
|
50544
50925
|
init_config();
|
|
50545
50926
|
init_manifest();
|
|
50546
|
-
import { existsSync as
|
|
50547
|
-
import { dirname as dirname15, join as
|
|
50927
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync18, rmSync as rmSync5, writeFileSync as writeFileSync20 } from "node:fs";
|
|
50928
|
+
import { dirname as dirname15, join as join24 } from "node:path";
|
|
50548
50929
|
init_types4();
|
|
50549
50930
|
init_localFiles();
|
|
50550
50931
|
init_util();
|
|
@@ -50567,9 +50948,9 @@ var pull = async (args) => {
|
|
|
50567
50948
|
const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a2) => a2.id === id)?.type !== "folder").map(([, path2]) => path2));
|
|
50568
50949
|
for (const path2 of Object.keys(manifest.files)) {
|
|
50569
50950
|
if (!remotePaths.has(path2)) {
|
|
50570
|
-
const abs =
|
|
50571
|
-
if (
|
|
50572
|
-
|
|
50951
|
+
const abs = join24(root, ...path2.replace(/^\//, "").split("/"));
|
|
50952
|
+
if (existsSync24(abs))
|
|
50953
|
+
rmSync5(abs);
|
|
50573
50954
|
}
|
|
50574
50955
|
}
|
|
50575
50956
|
const files = await materializeAssets(root, project, apiUrl, token, progress);
|
|
@@ -50606,18 +50987,18 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
|
50606
50987
|
const files = { ...manifest.files };
|
|
50607
50988
|
for (const asset of targets) {
|
|
50608
50989
|
const key = pathById.get(asset.id);
|
|
50609
|
-
const abs =
|
|
50610
|
-
|
|
50990
|
+
const abs = join24(root, ...key.replace(/^\//, "").split("/"));
|
|
50991
|
+
mkdirSync18(dirname15(abs), { recursive: true });
|
|
50611
50992
|
let sha;
|
|
50612
50993
|
if (asset.type === "file") {
|
|
50613
50994
|
if (!asset.file?.src)
|
|
50614
50995
|
continue;
|
|
50615
50996
|
const bytes = await downloadBlob(apiUrl, asset.file.src);
|
|
50616
|
-
|
|
50997
|
+
writeFileSync20(abs, bytes);
|
|
50617
50998
|
sha = sha256(bytes);
|
|
50618
50999
|
} else {
|
|
50619
51000
|
const text = asset.text ?? "";
|
|
50620
|
-
|
|
51001
|
+
writeFileSync20(abs, text);
|
|
50621
51002
|
sha = sha256(text);
|
|
50622
51003
|
}
|
|
50623
51004
|
files[key] = { id: asset.id, type: kindOf3(asset.type), sha };
|
|
@@ -50630,8 +51011,8 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
|
50630
51011
|
|
|
50631
51012
|
// src/commands/scene.ts
|
|
50632
51013
|
init_manifest();
|
|
50633
|
-
import { existsSync as
|
|
50634
|
-
import { basename as basename13, dirname as dirname16, join as
|
|
51014
|
+
import { existsSync as existsSync26, readFileSync as readFileSync25, readdirSync as readdirSync11, statSync as statSync7, writeFileSync as writeFileSync21 } from "node:fs";
|
|
51015
|
+
import { basename as basename13, dirname as dirname16, join as join26, resolve as resolve11 } from "node:path";
|
|
50635
51016
|
|
|
50636
51017
|
// src/compile/sceneCompile.ts
|
|
50637
51018
|
init_localFiles();
|
|
@@ -50639,18 +51020,18 @@ init_ignore();
|
|
|
50639
51020
|
init_distRoot();
|
|
50640
51021
|
init_util();
|
|
50641
51022
|
init_projectCompile();
|
|
50642
|
-
import { existsSync as
|
|
50643
|
-
import { join as
|
|
51023
|
+
import { existsSync as existsSync25, readFileSync as readFileSync24 } from "node:fs";
|
|
51024
|
+
import { join as join25 } from "node:path";
|
|
50644
51025
|
var TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i;
|
|
50645
51026
|
var rel = (key) => key.replace(/^\//, "");
|
|
50646
51027
|
var resolveHarness = (loadSceneHarness2) => {
|
|
50647
|
-
const inRepo =
|
|
51028
|
+
const inRepo = join25(distRoot, "..", "projects", "src", "scene-editor");
|
|
50648
51029
|
try {
|
|
50649
|
-
return loadSceneHarness2(
|
|
51030
|
+
return loadSceneHarness2(existsSync25(inRepo) ? inRepo : undefined);
|
|
50650
51031
|
} catch {
|
|
50651
|
-
const vendored =
|
|
50652
|
-
if (
|
|
50653
|
-
return JSON.parse(
|
|
51032
|
+
const vendored = join25(distRoot, "runtime", "scene-harness.json");
|
|
51033
|
+
if (existsSync25(vendored))
|
|
51034
|
+
return JSON.parse(readFileSync24(vendored, "utf8"));
|
|
50654
51035
|
throw new CliError("scene-editor harness sources not found — reinstall the CLI or set LECODES_SCENE_HARNESS_DIR.");
|
|
50655
51036
|
}
|
|
50656
51037
|
};
|
|
@@ -50662,7 +51043,7 @@ var compileSceneEditor = async (opts) => {
|
|
|
50662
51043
|
for (const file of scan2.files) {
|
|
50663
51044
|
const path2 = "./" + rel(file.path);
|
|
50664
51045
|
if (TEXT_EXT.test(path2)) {
|
|
50665
|
-
entries.push({ path: path2, type: "text", text:
|
|
51046
|
+
entries.push({ path: path2, type: "text", text: readFileSync24(file.absPath, "utf8") });
|
|
50666
51047
|
} else {
|
|
50667
51048
|
entries.push({ path: path2, type: "resource", fileSrc: "/project/" + rel(file.path) });
|
|
50668
51049
|
}
|
|
@@ -50704,10 +51085,10 @@ var getContext2 = (args) => {
|
|
|
50704
51085
|
manifest = null;
|
|
50705
51086
|
const pathArg = args._[0];
|
|
50706
51087
|
if (pathArg) {
|
|
50707
|
-
const abs =
|
|
50708
|
-
if (
|
|
51088
|
+
const abs = resolve11(process.cwd(), pathArg);
|
|
51089
|
+
if (existsSync26(abs) && statSync7(abs).isDirectory())
|
|
50709
51090
|
root = abs;
|
|
50710
|
-
else if (abs.endsWith(".scene.ts") &&
|
|
51091
|
+
else if (abs.endsWith(".scene.ts") && existsSync26(abs))
|
|
50711
51092
|
root = dirname16(abs);
|
|
50712
51093
|
else
|
|
50713
51094
|
throw new CliError(`No such folder or .scene.ts: ${pathArg}`);
|
|
@@ -50716,12 +51097,12 @@ var getContext2 = (args) => {
|
|
|
50716
51097
|
return { root, manifest, name: manifest?.name ?? basename13(root) };
|
|
50717
51098
|
};
|
|
50718
51099
|
var writeMcpConfig2 = (root, port) => {
|
|
50719
|
-
const path2 =
|
|
51100
|
+
const path2 = join26(root, ".mcp.json");
|
|
50720
51101
|
const entry = { type: "http", url: `http://localhost:${port}/mcp` };
|
|
50721
51102
|
let config = {};
|
|
50722
|
-
if (
|
|
51103
|
+
if (existsSync26(path2)) {
|
|
50723
51104
|
try {
|
|
50724
|
-
config = JSON.parse(
|
|
51105
|
+
config = JSON.parse(readFileSync25(path2, "utf8"));
|
|
50725
51106
|
} catch {
|
|
50726
51107
|
warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-scene server by hand.");
|
|
50727
51108
|
return;
|
|
@@ -50734,17 +51115,17 @@ var writeMcpConfig2 = (root, port) => {
|
|
|
50734
51115
|
return;
|
|
50735
51116
|
}
|
|
50736
51117
|
config.mcpServers["lecodes-scene"] = entry;
|
|
50737
|
-
|
|
51118
|
+
writeFileSync21(path2, JSON.stringify(config, null, 2) + `
|
|
50738
51119
|
`);
|
|
50739
51120
|
note(`${existing ? "updated" : "created"} .mcp.json (lecodes-scene MCP → ${entry.url})`);
|
|
50740
51121
|
};
|
|
50741
51122
|
var countScenes = (dir) => {
|
|
50742
51123
|
let n2 = 0;
|
|
50743
|
-
for (const name of
|
|
51124
|
+
for (const name of readdirSync11(dir)) {
|
|
50744
51125
|
if (name === "node_modules" || name.startsWith("."))
|
|
50745
51126
|
continue;
|
|
50746
|
-
const full =
|
|
50747
|
-
if (
|
|
51127
|
+
const full = join26(dir, name);
|
|
51128
|
+
if (statSync7(full).isDirectory())
|
|
50748
51129
|
n2 += countScenes(full);
|
|
50749
51130
|
else if (name.endsWith(".scene.ts"))
|
|
50750
51131
|
n2++;
|
|
@@ -50788,7 +51169,7 @@ var scene = async (args) => {
|
|
|
50788
51169
|
init_manifest();
|
|
50789
51170
|
init_localFiles();
|
|
50790
51171
|
init_util();
|
|
50791
|
-
var
|
|
51172
|
+
var status2 = async (_args) => {
|
|
50792
51173
|
const root = findProjectRoot(process.cwd());
|
|
50793
51174
|
const manifest = readManifest(root);
|
|
50794
51175
|
const scan2 = scanProject(root, manifest);
|
|
@@ -50817,8 +51198,8 @@ var status = async (_args) => {
|
|
|
50817
51198
|
};
|
|
50818
51199
|
|
|
50819
51200
|
// src/commands/test.ts
|
|
50820
|
-
import { existsSync as
|
|
50821
|
-
import { dirname as dirname17, resolve as
|
|
51201
|
+
import { existsSync as existsSync27, readFileSync as readFileSync26, readdirSync as readdirSync12 } from "node:fs";
|
|
51202
|
+
import { dirname as dirname17, resolve as resolve12 } from "node:path";
|
|
50822
51203
|
init_util();
|
|
50823
51204
|
var deriveDesignFlows = (meta, entryScreen) => {
|
|
50824
51205
|
const edges = meta.edges ?? [];
|
|
@@ -50880,7 +51261,7 @@ var slug3 = (s2) => s2.replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "").
|
|
|
50880
51261
|
var readFlowFile = (abs, displayName) => {
|
|
50881
51262
|
let raw;
|
|
50882
51263
|
try {
|
|
50883
|
-
raw = JSON.parse(
|
|
51264
|
+
raw = JSON.parse(readFileSync26(abs, "utf8"));
|
|
50884
51265
|
} catch (e2) {
|
|
50885
51266
|
throw new CliError(`Can't read "${displayName}": ${e2 instanceof Error ? e2.message : String(e2)}`);
|
|
50886
51267
|
}
|
|
@@ -50905,37 +51286,37 @@ var readFlowFile = (abs, displayName) => {
|
|
|
50905
51286
|
var test = async (args) => {
|
|
50906
51287
|
const { js, warnings, useLocalAssets, root } = await compileHeadlessBundle(args);
|
|
50907
51288
|
const renderer = await requireRenderer();
|
|
50908
|
-
const testsDir =
|
|
50909
|
-
const designMetaPath =
|
|
51289
|
+
const testsDir = resolve12(root, flagStr(args, "dir") ?? "tests");
|
|
51290
|
+
const designMetaPath = resolve12(root, flagStr(args, "design-dir") ?? "design", "meta.json");
|
|
50910
51291
|
const asJson = flagBool(args, "json");
|
|
50911
51292
|
const out = asJson ? logErr : log;
|
|
50912
51293
|
const cases = [];
|
|
50913
51294
|
if (args._.length > 0) {
|
|
50914
51295
|
for (const given of args._) {
|
|
50915
|
-
const candidates = [
|
|
50916
|
-
const abs = candidates.find((p2) =>
|
|
51296
|
+
const candidates = [resolve12(given), resolve12(testsDir, given), resolve12(testsDir, `${given}.flow.json`)];
|
|
51297
|
+
const abs = candidates.find((p2) => existsSync27(p2));
|
|
50917
51298
|
if (!abs)
|
|
50918
51299
|
throw new CliError(`No such test: "${given}" (looked in ${testsDir}).`);
|
|
50919
51300
|
cases.push(readFlowFile(abs, given));
|
|
50920
51301
|
}
|
|
50921
|
-
} else if (
|
|
50922
|
-
for (const f3 of
|
|
50923
|
-
cases.push(readFlowFile(
|
|
51302
|
+
} else if (existsSync27(testsDir)) {
|
|
51303
|
+
for (const f3 of readdirSync12(testsDir).filter((f4) => f4.endsWith(".flow.json")).sort()) {
|
|
51304
|
+
cases.push(readFlowFile(resolve12(testsDir, f3), f3));
|
|
50924
51305
|
}
|
|
50925
51306
|
}
|
|
50926
51307
|
const fixturesFlag = flagStr(args, "fixtures");
|
|
50927
|
-
const defaultPack =
|
|
50928
|
-
const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") :
|
|
51308
|
+
const defaultPack = resolve12(testsDir, "fixtures.json");
|
|
51309
|
+
const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") : existsSync27(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
|
|
50929
51310
|
const flowsOnly = flagBool(args, "flows");
|
|
50930
51311
|
const wantFlows = !flagBool(args, "no-flows") && args._.length === 0;
|
|
50931
51312
|
if (flowsOnly)
|
|
50932
51313
|
cases.length = 0;
|
|
50933
51314
|
let skipped = [];
|
|
50934
51315
|
let entryScreen = null;
|
|
50935
|
-
if ((wantFlows || flowsOnly) &&
|
|
51316
|
+
if ((wantFlows || flowsOnly) && existsSync27(designMetaPath)) {
|
|
50936
51317
|
let meta;
|
|
50937
51318
|
try {
|
|
50938
|
-
meta = JSON.parse(
|
|
51319
|
+
meta = JSON.parse(readFileSync26(designMetaPath, "utf8"));
|
|
50939
51320
|
} catch (e2) {
|
|
50940
51321
|
throw new CliError(`Can't read ${designMetaPath}: ${e2 instanceof Error ? e2.message : String(e2)}`);
|
|
50941
51322
|
}
|
|
@@ -50967,7 +51348,7 @@ var test = async (args) => {
|
|
|
50967
51348
|
}, timeoutMs);
|
|
50968
51349
|
killer.unref?.();
|
|
50969
51350
|
const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
|
|
50970
|
-
const artifactsRoot =
|
|
51351
|
+
const artifactsRoot = resolve12(root, flagStr(args, "artifacts") ?? resolve12(testsDir, ".artifacts"));
|
|
50971
51352
|
const results = [];
|
|
50972
51353
|
for (const kase of cases) {
|
|
50973
51354
|
const flagSettle = flagStr(args, "settle");
|
|
@@ -50985,14 +51366,14 @@ var test = async (args) => {
|
|
|
50985
51366
|
onConsole
|
|
50986
51367
|
});
|
|
50987
51368
|
try {
|
|
50988
|
-
const io = fileScenarioIO(
|
|
51369
|
+
const io = fileScenarioIO(resolve12(artifactsRoot, slug3(kase.name)));
|
|
50989
51370
|
const outcome = await renderer.runScenario(session, steps, io);
|
|
50990
51371
|
result = {
|
|
50991
51372
|
name: kase.name,
|
|
50992
51373
|
kind: kase.kind,
|
|
50993
51374
|
ok: outcome.ok,
|
|
50994
51375
|
steps: outcome.steps.map((s2) => ({ desc: s2.desc, ok: s2.ok, error: s2.error })),
|
|
50995
|
-
artifacts: outcome.failureArtifacts.map((p2) =>
|
|
51376
|
+
artifacts: outcome.failureArtifacts.map((p2) => resolve12(artifactsRoot, slug3(kase.name), p2))
|
|
50996
51377
|
};
|
|
50997
51378
|
} finally {
|
|
50998
51379
|
session.dispose();
|
|
@@ -51054,11 +51435,11 @@ init_config();
|
|
|
51054
51435
|
init_manifest();
|
|
51055
51436
|
init_types4();
|
|
51056
51437
|
init_util();
|
|
51057
|
-
import { resolve as
|
|
51438
|
+
import { resolve as resolve13 } from "node:path";
|
|
51058
51439
|
var label = (bundle) => `${Object.keys(bundle).length} file(s)`;
|
|
51059
51440
|
var types4 = async (args) => {
|
|
51060
51441
|
const dirArg = args._[0];
|
|
51061
|
-
const root = dirArg ?
|
|
51442
|
+
const root = dirArg ? resolve13(dirArg) : (() => {
|
|
51062
51443
|
try {
|
|
51063
51444
|
return findProjectRoot(process.cwd());
|
|
51064
51445
|
} catch {
|
|
@@ -51117,23 +51498,23 @@ var types4 = async (args) => {
|
|
|
51117
51498
|
};
|
|
51118
51499
|
|
|
51119
51500
|
// src/index.ts
|
|
51120
|
-
import { existsSync as
|
|
51501
|
+
import { existsSync as existsSync29, readFileSync as readFileSync28, realpathSync as realpathSync2 } from "node:fs";
|
|
51121
51502
|
import { createRequire as createRequire4 } from "node:module";
|
|
51122
|
-
import { delimiter as delimiter2, dirname as dirname19, join as
|
|
51503
|
+
import { delimiter as delimiter2, dirname as dirname19, join as join28 } from "node:path";
|
|
51123
51504
|
|
|
51124
51505
|
// src/commands/install.ts
|
|
51125
51506
|
init_util();
|
|
51126
51507
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
51127
|
-
import { appendFileSync as appendFileSync3, chmodSync as chmodSync2, cpSync as cpSync3, existsSync as
|
|
51128
|
-
import { homedir as
|
|
51129
|
-
import { basename as basename14, delimiter, dirname as dirname18, join as
|
|
51508
|
+
import { appendFileSync as appendFileSync3, chmodSync as chmodSync2, cpSync as cpSync3, existsSync as existsSync28, mkdirSync as mkdirSync19, readFileSync as readFileSync27, realpathSync, rmSync as rmSync6, symlinkSync } from "node:fs";
|
|
51509
|
+
import { homedir as homedir7 } from "node:os";
|
|
51510
|
+
import { basename as basename14, delimiter, dirname as dirname18, join as join27, resolve as resolve14, sep as sep2 } from "node:path";
|
|
51130
51511
|
var isWin = process.platform === "win32";
|
|
51131
|
-
var HOME =
|
|
51512
|
+
var HOME = homedir7();
|
|
51132
51513
|
var safeRealpath = (p2) => {
|
|
51133
51514
|
try {
|
|
51134
51515
|
return realpathSync(p2);
|
|
51135
51516
|
} catch {
|
|
51136
|
-
return
|
|
51517
|
+
return resolve14(p2);
|
|
51137
51518
|
}
|
|
51138
51519
|
};
|
|
51139
51520
|
var onPath = (dir) => {
|
|
@@ -51150,24 +51531,24 @@ var onPath = (dir) => {
|
|
|
51150
51531
|
};
|
|
51151
51532
|
var install = async (args) => {
|
|
51152
51533
|
const bundle = dirname18(process.execPath);
|
|
51153
|
-
const
|
|
51534
|
+
const exeName2 = basename14(process.execPath);
|
|
51154
51535
|
const chiselName = isWin ? "chisel.exe" : "chisel";
|
|
51155
|
-
if (!
|
|
51536
|
+
if (!existsSync28(join27(bundle, "runtime")) || !existsSync28(join27(bundle, chiselName))) {
|
|
51156
51537
|
throw new CliError("`lecodes install` only works from the packaged release build (the archive containing lecodes + " + "chisel + runtime/). You appear to be running from source or an npm install — install globally " + "with your package manager there instead.");
|
|
51157
51538
|
}
|
|
51158
|
-
const companions = [
|
|
51539
|
+
const companions = [exeName2, chiselName, "runtime", "README.txt"].filter((f3) => existsSync28(join27(bundle, f3)));
|
|
51159
51540
|
if (isWin)
|
|
51160
51541
|
return installWindows(args, bundle, companions);
|
|
51161
51542
|
return installUnix(args, bundle, companions);
|
|
51162
51543
|
};
|
|
51163
51544
|
var copyBundle = (bundle, target, companions) => {
|
|
51164
|
-
|
|
51545
|
+
mkdirSync19(target, { recursive: true });
|
|
51165
51546
|
if (safeRealpath(target) === safeRealpath(bundle))
|
|
51166
51547
|
return;
|
|
51167
51548
|
for (const f3 of companions) {
|
|
51168
|
-
const dest =
|
|
51169
|
-
|
|
51170
|
-
cpSync3(
|
|
51549
|
+
const dest = join27(target, f3);
|
|
51550
|
+
rmSync6(dest, { recursive: true, force: true });
|
|
51551
|
+
cpSync3(join27(bundle, f3), dest, { recursive: true });
|
|
51171
51552
|
}
|
|
51172
51553
|
};
|
|
51173
51554
|
var permError = (e2, dir, system) => {
|
|
@@ -51179,41 +51560,41 @@ var permError = (e2, dir, system) => {
|
|
|
51179
51560
|
};
|
|
51180
51561
|
var installUnix = (args, bundle, companions) => {
|
|
51181
51562
|
const system = flagBool(args, "system");
|
|
51182
|
-
const libDir =
|
|
51183
|
-
const binDir =
|
|
51184
|
-
const link2 =
|
|
51563
|
+
const libDir = resolve14(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join27(HOME, ".local", "lib", "lecodes-cli")));
|
|
51564
|
+
const binDir = resolve14(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join27(HOME, ".local", "bin")));
|
|
51565
|
+
const link2 = join27(binDir, "lecodes");
|
|
51185
51566
|
try {
|
|
51186
51567
|
copyBundle(bundle, libDir, companions);
|
|
51187
|
-
chmodSync2(
|
|
51188
|
-
chmodSync2(
|
|
51568
|
+
chmodSync2(join27(libDir, "lecodes"), 493);
|
|
51569
|
+
chmodSync2(join27(libDir, "chisel"), 493);
|
|
51189
51570
|
} catch (e2) {
|
|
51190
51571
|
throw permError(e2, libDir, system);
|
|
51191
51572
|
}
|
|
51192
51573
|
try {
|
|
51193
|
-
|
|
51194
|
-
|
|
51195
|
-
symlinkSync(
|
|
51574
|
+
mkdirSync19(binDir, { recursive: true });
|
|
51575
|
+
rmSync6(link2, { force: true });
|
|
51576
|
+
symlinkSync(join27(libDir, "lecodes"), link2);
|
|
51196
51577
|
} catch (e2) {
|
|
51197
51578
|
throw permError(e2, binDir, system);
|
|
51198
51579
|
}
|
|
51199
51580
|
success(`Installed to ${c.bold(libDir)}`);
|
|
51200
|
-
log(` ${c.dim(link2 + " → " +
|
|
51581
|
+
log(` ${c.dim(link2 + " → " + join27(libDir, "lecodes"))}`);
|
|
51201
51582
|
finishPathUnix(args, binDir);
|
|
51202
51583
|
};
|
|
51203
51584
|
var shellRc = () => {
|
|
51204
51585
|
const shell = basename14(process.env.SHELL ?? "");
|
|
51205
51586
|
if (shell === "zsh")
|
|
51206
|
-
return { file:
|
|
51587
|
+
return { file: join27(HOME, ".zshrc"), name: "~/.zshrc" };
|
|
51207
51588
|
if (shell === "bash")
|
|
51208
|
-
return { file:
|
|
51209
|
-
return { file:
|
|
51589
|
+
return { file: join27(HOME, ".bashrc"), name: "~/.bashrc" };
|
|
51590
|
+
return { file: join27(HOME, ".profile"), name: "~/.profile" };
|
|
51210
51591
|
};
|
|
51211
51592
|
var PATH_MARKER = "# Added by `lecodes install`";
|
|
51212
51593
|
var appendPathToShellRc = (binDir) => {
|
|
51213
51594
|
const { file, name } = shellRc();
|
|
51214
51595
|
const line = `export PATH="${binDir}:$PATH"`;
|
|
51215
51596
|
try {
|
|
51216
|
-
const existing =
|
|
51597
|
+
const existing = existsSync28(file) ? readFileSync27(file, "utf8") : "";
|
|
51217
51598
|
if (existing.includes(line))
|
|
51218
51599
|
return name;
|
|
51219
51600
|
appendFileSync3(file, `${existing && !existing.endsWith(`
|
|
@@ -51251,7 +51632,7 @@ var finishPathUnix = (args, binDir) => {
|
|
|
51251
51632
|
log(` • or install system-wide (no PATH edit): ${c.bold("sudo lecodes install --system")}`);
|
|
51252
51633
|
};
|
|
51253
51634
|
var installWindows = (args, bundle, companions) => {
|
|
51254
|
-
const target =
|
|
51635
|
+
const target = resolve14(flagStr(args, "dir") ?? join27(process.env.LOCALAPPDATA ?? join27(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
|
|
51255
51636
|
try {
|
|
51256
51637
|
copyBundle(bundle, target, companions);
|
|
51257
51638
|
} catch (e2) {
|
|
@@ -51291,7 +51672,7 @@ init_util();
|
|
|
51291
51672
|
var STANDALONE = (() => {
|
|
51292
51673
|
const dir = dirname19(process.execPath);
|
|
51293
51674
|
const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
|
|
51294
|
-
return
|
|
51675
|
+
return existsSync29(join28(dir, "runtime")) && existsSync29(join28(dir, chisel));
|
|
51295
51676
|
})();
|
|
51296
51677
|
var INSTALLED = STANDALONE && (() => {
|
|
51297
51678
|
const exe = process.platform === "win32" ? "lecodes.exe" : "lecodes";
|
|
@@ -51306,8 +51687,8 @@ var INSTALLED = STANDALONE && (() => {
|
|
|
51306
51687
|
if (!d)
|
|
51307
51688
|
return false;
|
|
51308
51689
|
try {
|
|
51309
|
-
const cand =
|
|
51310
|
-
return
|
|
51690
|
+
const cand = join28(d, exe);
|
|
51691
|
+
return existsSync29(cand) && realpathSync2(cand) === self2;
|
|
51311
51692
|
} catch {
|
|
51312
51693
|
return false;
|
|
51313
51694
|
}
|
|
@@ -51396,6 +51777,14 @@ ${c.bold("Commands:")}
|
|
|
51396
51777
|
--settle <ms> wait for promises/timers/fetch before snapshot
|
|
51397
51778
|
--wait-network let data fetches hit the real network
|
|
51398
51779
|
--logs forward the app's console output (to stderr)
|
|
51780
|
+
--desktop render through the NATIVE desktop host instead:
|
|
51781
|
+
real Filament 3D + tgfx UI + 2D, composited.
|
|
51782
|
+
PNG only (--png). Downloads the LeCodes
|
|
51783
|
+
Desktop renderer on first use into
|
|
51784
|
+
~/.lecodes/renderer (Windows/Linux;
|
|
51785
|
+
LECODES_DESKTOP_EXE overrides)
|
|
51786
|
+
--frames <n> (--desktop) frames to run before the shot —
|
|
51787
|
+
headroom for async loads (default 60)
|
|
51399
51788
|
test [name…] Run headless flow tests: tests/*.flow.json scenarios, plus flows
|
|
51400
51789
|
derived from the design graph (design/meta.json activator edges
|
|
51401
51790
|
— checks the built app against the designed navigation; needs
|
|
@@ -51435,6 +51824,17 @@ ${c.bold("Commands:")}
|
|
|
51435
51824
|
--logs forward the screens' console output (to stderr)
|
|
51436
51825
|
design comments Review comments pinned to the board, grouped by screen (open first)
|
|
51437
51826
|
--all include resolved threads
|
|
51827
|
+
add "<text>" [--screen <id>] [--state <s>] [--element <name>]
|
|
51828
|
+
reply <number> "<text>" · resolve <number> [--reply "<text>"] [--reopen]
|
|
51829
|
+
(--ai signs a write as Claude; sends with the next "lecodes push")
|
|
51830
|
+
desktop [run|update] The native desktop host ('lecodes render --desktop' uses it too):
|
|
51831
|
+
no args shows the version status (selected / downloaded /
|
|
51832
|
+
newest published)
|
|
51833
|
+
run compile the current project and open it in a real
|
|
51834
|
+
desktop window (--width/--height set the window size;
|
|
51835
|
+
--entry overrides the entrypoint)
|
|
51836
|
+
update move to the newest published release
|
|
51837
|
+
(cached in ~/.lecodes/renderer)
|
|
51438
51838
|
scene [path] LeCodes Scene Editor — visually edit a .scene.ts (tree, inspector,
|
|
51439
51839
|
gizmo) on local files (needs the optional 'lecodes-3d-editor' package;
|
|
51440
51840
|
works inside a cloned project or standalone in any folder)
|
|
@@ -51498,7 +51898,7 @@ ${c.bold("Commands:")}
|
|
|
51498
51898
|
— 'new --env' banks it in .env), revoke <id>
|
|
51499
51899
|
|
|
51500
51900
|
Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKEN).`;
|
|
51501
|
-
var commands = { init: init2, login, create, clone: clone3, link, status, diff: diff2, pull, push, compile: compile2, preview: previewLegacy, render, test, types: types4, design, scene, dev, app, pn };
|
|
51901
|
+
var commands = { init: init2, login, create, clone: clone3, link, status: status2, diff: diff2, pull, push, compile: compile2, preview: previewLegacy, render, test, types: types4, design, scene, dev, app, pn, desktop };
|
|
51502
51902
|
if (STANDALONE)
|
|
51503
51903
|
commands.install = install;
|
|
51504
51904
|
var readVersion = () => package_default.version ?? "unknown";
|
|
@@ -51512,9 +51912,9 @@ var peerVersion = (name, probe) => {
|
|
|
51512
51912
|
const require2 = createRequire4(import.meta.url);
|
|
51513
51913
|
let dir = dirname19(require2.resolve(probe));
|
|
51514
51914
|
for (let depth = 0;depth < 16; depth++) {
|
|
51515
|
-
const manifest =
|
|
51516
|
-
if (
|
|
51517
|
-
const parsed = JSON.parse(
|
|
51915
|
+
const manifest = join28(dir, "package.json");
|
|
51916
|
+
if (existsSync29(manifest)) {
|
|
51917
|
+
const parsed = JSON.parse(readFileSync28(manifest, "utf8"));
|
|
51518
51918
|
if (parsed.name === name)
|
|
51519
51919
|
return parsed.version ?? "unknown";
|
|
51520
51920
|
}
|
|
@@ -51532,6 +51932,8 @@ var printVersion = () => {
|
|
|
51532
51932
|
const version = peerVersion(name, probe);
|
|
51533
51933
|
rows.push([name, version ?? c.dim("not installed")]);
|
|
51534
51934
|
}
|
|
51935
|
+
const desktopVersion = selectedDesktopVersion();
|
|
51936
|
+
rows.push(["lecodes-desktop", installedDesktopVersions().includes(desktopVersion) ? desktopVersion : `${desktopVersion} ${c.dim("(not downloaded — fetched on first `render --desktop`)")}`]);
|
|
51535
51937
|
const width = Math.max(...rows.map(([label2]) => label2.length));
|
|
51536
51938
|
for (const [label2, value] of rows)
|
|
51537
51939
|
log(`${label2.padEnd(width)} ${value}`);
|
|
@@ -51547,14 +51949,14 @@ var main = async () => {
|
|
|
51547
51949
|
printVersion();
|
|
51548
51950
|
return;
|
|
51549
51951
|
}
|
|
51550
|
-
const
|
|
51551
|
-
if (!
|
|
51952
|
+
const run2 = commands[command];
|
|
51953
|
+
if (!run2) {
|
|
51552
51954
|
log(c.red(`Unknown command: ${command}`));
|
|
51553
51955
|
log(HELP);
|
|
51554
51956
|
process.exitCode = 1;
|
|
51555
51957
|
return;
|
|
51556
51958
|
}
|
|
51557
|
-
await
|
|
51959
|
+
await run2(parseArgs(argv.slice(1)));
|
|
51558
51960
|
};
|
|
51559
51961
|
main().catch((e2) => {
|
|
51560
51962
|
if (e2 instanceof CliError)
|