lecodes-cli 0.16.2 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -120,7 +120,7 @@ var useColor, paint = (code, s) => useColor ? `\x1B[${code}m${s}\x1B[0m` : s, c,
120
120
  if (!answer)
121
121
  return fallback;
122
122
  return answer === "y" || answer === "yes";
123
- }, parseArgs = (argv) => {
123
+ }, isNegativeNumber = (s) => /^-(\d|\.\d)/.test(s), isValue = (s) => !s.startsWith("-") || isNegativeNumber(s), parseArgs = (argv) => {
124
124
  const out = { _: [], flags: {} };
125
125
  for (let i = 0;i < argv.length; i++) {
126
126
  const arg = argv[i];
@@ -132,15 +132,15 @@ var useColor, paint = (code, s) => useColor ? `\x1B[${code}m${s}\x1B[0m` : s, c,
132
132
  }
133
133
  const key = arg.slice(2);
134
134
  const next = argv[i + 1];
135
- if (next !== undefined && !next.startsWith("-")) {
135
+ if (next !== undefined && isValue(next)) {
136
136
  out.flags[key] = next;
137
137
  i++;
138
138
  } else
139
139
  out.flags[key] = true;
140
- } else if (arg.startsWith("-") && arg.length > 1) {
140
+ } else if (arg.startsWith("-") && arg.length > 1 && !isNegativeNumber(arg)) {
141
141
  const key = arg.slice(1);
142
142
  const next = argv[i + 1];
143
- if (next !== undefined && !next.startsWith("-")) {
143
+ if (next !== undefined && isValue(next)) {
144
144
  out.flags[key] = next;
145
145
  i++;
146
146
  } else
@@ -388,6 +388,34 @@ design/comments/
388
388
  const file = join3(root, ".lecodesignore");
389
389
  if (!existsSync2(file))
390
390
  writeFileSync3(file, DEFAULT_IGNORE_FILE);
391
+ }, DEFAULT_GITIGNORE = `# LeCodes per-checkout state: generated IDE types + this machine's sync cursor
392
+ # (.lecodes/manifest.json). Regenerate with \`lecodes types\` / re-attach with \`lecodes link\`.
393
+ .lecodes/
394
+
395
+ # Render / preview outputs and test failure frames
396
+ screenshot.png
397
+ shots/
398
+ tests/.artifacts/
399
+
400
+ node_modules/
401
+ `, ensureGitignore = (root) => {
402
+ const file = join3(root, ".gitignore");
403
+ if (!existsSync2(file)) {
404
+ writeFileSync3(file, DEFAULT_GITIGNORE);
405
+ return "created";
406
+ }
407
+ const text = readFileSync3(file, "utf8");
408
+ const covered = text.split(/\r?\n/).some((l) => /^\/?\.lecodes\/?\s*$/.test(l.trim()));
409
+ if (covered)
410
+ return null;
411
+ const sep = text.length === 0 || text.endsWith(`
412
+ `) ? "" : `
413
+ `;
414
+ writeFileSync3(file, `${text}${sep}
415
+ # LeCodes per-checkout state (generated types + sync cursor) — not source
416
+ .lecodes/
417
+ `);
418
+ return "updated";
391
419
  };
392
420
  var init_ignore = __esm(() => {
393
421
  BUILTIN_PATTERNS = ["node_modules/", ".git/", "_*", "*.log", ".DS_Store", ".artifacts/"];
@@ -84194,8 +84222,8 @@ ${lanes.join(`
84194
84222
  if (node === undefined || node.parent.kind !== 307 || !isInternalModuleImportEqualsDeclaration(node)) {
84195
84223
  return false;
84196
84224
  }
84197
- const isValue = isAliasResolvedToValue(getSymbolOfDeclaration(node));
84198
- return isValue && node.moduleReference && !nodeIsMissing(node.moduleReference);
84225
+ const isValue2 = isAliasResolvedToValue(getSymbolOfDeclaration(node));
84226
+ return isValue2 && node.moduleReference && !nodeIsMissing(node.moduleReference);
84199
84227
  }
84200
84228
  function isAliasResolvedToValue(symbol, excludeTypeOnlyValues) {
84201
84229
  if (!symbol) {
@@ -215816,7 +215844,7 @@ var require_main = __commonJS((exports, module) => {
215816
215844
  // package.json
215817
215845
  var package_default = {
215818
215846
  name: "lecodes-cli",
215819
- version: "0.16.2",
215847
+ version: "0.17.1",
215820
215848
  dependencies: {
215821
215849
  "@letary/chisel": "^0.8.0",
215822
215850
  jimp: "^1.6.1"
@@ -215827,11 +215855,12 @@ var package_default = {
215827
215855
  "qrcode-terminal": "^0.12.0",
215828
215856
  "lecodes-3d-editor": "workspace:*",
215829
215857
  "lecodes-assets": "workspace:*",
215830
- sdk: "workspace:*"
215858
+ sdk: "workspace:*",
215859
+ "viewer-lite": "workspace:*"
215831
215860
  },
215832
215861
  peerDependencies: {
215833
215862
  "lecodes-design": "^0.8.1",
215834
- "lecodes-renderer": "^0.8.4",
215863
+ "lecodes-renderer": "^0.8.6",
215835
215864
  "lecodes-3d-editor": "^0.3.0",
215836
215865
  "lecodes-assets": "^0.1.0"
215837
215866
  },
@@ -215891,8 +215920,8 @@ import { basename as basename6, dirname as dirname12, join as join17, resolve as
215891
215920
  init_api();
215892
215921
  init_config();
215893
215922
  init_manifest();
215894
- import { writeFileSync as writeFileSync7 } from "node:fs";
215895
- import { basename as basename3, resolve } from "node:path";
215923
+ import { writeFileSync as writeFileSync8 } from "node:fs";
215924
+ import { basename as basename3, resolve as resolve2 } from "node:path";
215896
215925
 
215897
215926
  // src/compile/collect.ts
215898
215927
  init_localFiles();
@@ -216005,6 +216034,226 @@ init_projectCompile();
216005
216034
  init_fonts();
216006
216035
  init_assetIcons();
216007
216036
  init_ignore();
216037
+
216038
+ // src/commands/appShared.ts
216039
+ init_manifest();
216040
+ init_ignore();
216041
+ init_util();
216042
+ import { execFileSync } from "node:child_process";
216043
+ import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "node:fs";
216044
+ import { homedir as homedir3 } from "node:os";
216045
+ import { dirname as dirname8, join as join12, resolve } from "node:path";
216046
+ var FIRST_PARTY_PLUGINS = {
216047
+ camera: "letary/lecodes-plugins",
216048
+ "qr-scanner": "letary/lecodes-plugins",
216049
+ geolocation: "letary/lecodes-plugins",
216050
+ push: "letary/lecodes-plugins"
216051
+ };
216052
+ var readAppConfigAt = (root) => readJson(join12(root, "app.json"));
216053
+ var DESKTOP_DEFAULT_WINDOW = { width: 1280, height: 720 };
216054
+ var desktopDefaultViewport = (root) => {
216055
+ const cfg = readAppConfigAt(root);
216056
+ if (!cfg?.desktop || cfg.ios || cfg.android)
216057
+ return null;
216058
+ return {
216059
+ width: cfg.desktop.window?.width ?? DESKTOP_DEFAULT_WINDOW.width,
216060
+ height: cfg.desktop.window?.height ?? DESKTOP_DEFAULT_WINDOW.height
216061
+ };
216062
+ };
216063
+ var readJson = (path) => {
216064
+ if (!existsSync10(path))
216065
+ return null;
216066
+ try {
216067
+ return JSON.parse(readFileSync11(path, "utf8").replace(/^\uFEFF/, ""));
216068
+ } catch {
216069
+ throw new CliError(`Invalid JSON in ${path}`);
216070
+ }
216071
+ };
216072
+ var writeJson = (path, value) => writeFileSync7(path, JSON.stringify(value, null, 2) + `
216073
+ `);
216074
+ var projectRoot = () => {
216075
+ try {
216076
+ return findProjectRoot(process.cwd());
216077
+ } catch {
216078
+ return process.cwd();
216079
+ }
216080
+ };
216081
+ var git = (cwd, ...args) => execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
216082
+ var slugify = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "app";
216083
+ var runTool = (cwd, cmd, args) => {
216084
+ try {
216085
+ return execFileSync(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 128 * 1024 * 1024 });
216086
+ } catch (e) {
216087
+ const err = e;
216088
+ const tail = ((err.stdout ?? "") + `
216089
+ ` + (err.stderr ?? "")).split(`
216090
+ `).filter((l) => l.trim()).slice(-25).join(`
216091
+ `);
216092
+ throw new CliError(`${cmd} ${args[0] ?? ""} failed:
216093
+ ${tail}`);
216094
+ }
216095
+ };
216096
+ var ensureShellIgnored = (root, dir) => {
216097
+ const ignorePath = join12(root, ".lecodesignore");
216098
+ const ignore = existsSync10(ignorePath) ? readFileSync11(ignorePath, "utf8") : DEFAULT_IGNORE_FILE;
216099
+ if (new RegExp(`^${dir}\\/$`, "m").test(ignore))
216100
+ return;
216101
+ const comment = ignore.includes("# Native app shells") ? "" : "# Native app shells — generated by `lecodes app init`, never pushed to LeCodes.\n";
216102
+ writeFileSync7(ignorePath, ignore.replace(/\n*$/, `
216103
+ `) + `
216104
+ ` + comment + `${dir}/
216105
+ `);
216106
+ log(`${c.green("updated")} .lecodesignore (+ ${dir}/)`);
216107
+ };
216108
+ var SDK_VARIANTS = ["core", "2d", "3d", "full"];
216109
+ var detectEngines = (bundleJs) => {
216110
+ const lines = [];
216111
+ for (const line of bundleJs.split(`
216112
+ `)) {
216113
+ if (!line.startsWith("//"))
216114
+ break;
216115
+ lines.push(line);
216116
+ }
216117
+ const header = lines.join(`
216118
+ `);
216119
+ return { gl: !/^\/\/ gl: disable$/m.test(header), g2: /^\/\/ 2d: enable$/m.test(header) };
216120
+ };
216121
+ var resolveVariant = (requested, bundleJs, configKey = "ios.variant") => {
216122
+ const uses = detectEngines(bundleJs);
216123
+ const detected = uses.gl && uses.g2 ? "full" : uses.gl ? "3d" : uses.g2 ? "2d" : "core";
216124
+ if (requested === "auto")
216125
+ return { variant: detected, detected };
216126
+ const variant = requested;
216127
+ const missing = [
216128
+ uses.gl && variant !== "3d" && variant !== "full" ? "3D" : null,
216129
+ uses.g2 && variant !== "2d" && variant !== "full" ? "2D" : null
216130
+ ].filter(Boolean);
216131
+ if (missing.length > 0) {
216132
+ warn(`project uses the ${missing.join(" + ")} engine but ${configKey} forces "${variant}" — those scenes won't run on device; remove ${configKey} to auto-select "${detected}".`);
216133
+ }
216134
+ return { variant, detected };
216135
+ };
216136
+ var VERSION_RE = /^\d+\.\d+\.\d+$/;
216137
+ var cmpVersion = (a, b) => {
216138
+ const pa = a.split(".").map(Number);
216139
+ const pb = b.split(".").map(Number);
216140
+ for (let i = 0;i < 3; i++) {
216141
+ if ((pa[i] ?? 0) !== (pb[i] ?? 0))
216142
+ return (pa[i] ?? 0) - (pb[i] ?? 0);
216143
+ }
216144
+ return 0;
216145
+ };
216146
+ var fetchLatestSdkVersion = async (repo) => {
216147
+ const url = `https://api.github.com/repos/${repo}/releases/latest`;
216148
+ const res = await fetch(url, { headers: { accept: "application/vnd.github+json", "user-agent": "lecodes-cli" } });
216149
+ if (!res.ok)
216150
+ throw new CliError(`Could not read the latest ${repo} release (HTTP ${res.status}) — ${url}`);
216151
+ const tag = (await res.json()).tag_name?.replace(/^v/, "") ?? "";
216152
+ if (!VERSION_RE.test(tag))
216153
+ throw new CliError(`${repo} latest release has no x.y.z tag (got "${tag}")`);
216154
+ return tag;
216155
+ };
216156
+ var resolveSdkVersion = async (pin, fallback, opts) => {
216157
+ if (pin !== "latest") {
216158
+ if (!VERSION_RE.test(pin)) {
216159
+ throw new CliError(`app.json ${opts.configKey} must be "latest" or an exact x.y.z release (got "${pin}").`);
216160
+ }
216161
+ if (cmpVersion(pin, opts.minVersion) < 0) {
216162
+ throw new CliError(`app.json ${opts.configKey} ${pin} is older than the ${opts.minVersion} these generated shells need.`);
216163
+ }
216164
+ return pin;
216165
+ }
216166
+ try {
216167
+ return await fetchLatestSdkVersion(opts.repo);
216168
+ } catch (e) {
216169
+ const kept = fallback ?? opts.minVersion;
216170
+ warn(`Could not resolve the latest SDK (${e.message}) — staying on ${kept}.`);
216171
+ return kept;
216172
+ }
216173
+ };
216174
+ var ensureRepo = (repo, wantTag, lock) => {
216175
+ const override = process.env.LECODES_PLUGINS_DIR;
216176
+ if (override) {
216177
+ note(`Using local plugin checkout ${override} (LECODES_PLUGINS_DIR)`);
216178
+ return { dir: resolve(override), tag: "local", commit: "local" };
216179
+ }
216180
+ const cache = join12(homedir3(), ".lecodes", "plugins", repo.replace("/", "__"));
216181
+ const url = `https://github.com/${repo}.git`;
216182
+ if (!existsSync10(join12(cache, ".git"))) {
216183
+ mkdirSync6(dirname8(cache), { recursive: true });
216184
+ note(`Cloning ${url}…`);
216185
+ git(dirname8(cache), "clone", "--quiet", url, cache);
216186
+ }
216187
+ let tag = lock?.tag ?? wantTag;
216188
+ let commit = lock?.commit;
216189
+ if (commit) {
216190
+ try {
216191
+ git(cache, "cat-file", "-e", `${commit}^{commit}`);
216192
+ } catch {
216193
+ git(cache, "fetch", "--quiet", "--tags", "origin");
216194
+ }
216195
+ } else {
216196
+ git(cache, "fetch", "--quiet", "--tags", "origin");
216197
+ if (!tag) {
216198
+ tag = git(cache, "tag", "--list", "v*", "--sort=-version:refname").split(`
216199
+ `)[0] ?? "";
216200
+ if (!tag)
216201
+ throw new CliError(`${repo} has no v* version tags to install from`);
216202
+ }
216203
+ commit = git(cache, "rev-list", "-n", "1", tag);
216204
+ }
216205
+ git(cache, "checkout", "--quiet", "--detach", commit);
216206
+ return { dir: cache, tag: tag ?? "unknown", commit };
216207
+ };
216208
+ var resolvePlugin = (id, spec, lock, platform) => {
216209
+ let repo;
216210
+ let subdir;
216211
+ let wantTag = null;
216212
+ if (spec.startsWith("github:")) {
216213
+ const [repoPart, tagPart] = spec.slice("github:".length).split("@");
216214
+ repo = repoPart;
216215
+ wantTag = tagPart ?? null;
216216
+ subdir = "";
216217
+ } else {
216218
+ repo = FIRST_PARTY_PLUGINS[id];
216219
+ if (!repo) {
216220
+ throw new CliError(`Unknown plugin "${id}" — known: ${Object.keys(FIRST_PARTY_PLUGINS).join(", ")}, or use a github:owner/repo spec.`);
216221
+ }
216222
+ subdir = `plugins/${id}`;
216223
+ }
216224
+ const checkout = ensureRepo(repo, wantTag, lock?.repo === repo ? lock : undefined);
216225
+ const dir = subdir ? join12(checkout.dir, subdir) : checkout.dir;
216226
+ const manifest = readJson(join12(dir, "lecodes-plugin.json"));
216227
+ if (!manifest)
216228
+ throw new CliError(`Plugin "${id}": no lecodes-plugin.json in ${dir}`);
216229
+ if (manifest.id !== id)
216230
+ throw new CliError(`Plugin "${id}": manifest declares id "${manifest.id}"`);
216231
+ if (!manifest[platform]) {
216232
+ throw new CliError(`Plugin "${id}" does not support ${platform === "ios" ? "iOS" : "Android"} (manifest "${platform}" is null)`);
216233
+ }
216234
+ if (spec.startsWith("github:")) {
216235
+ const names = [...manifest.provides?.views ?? [], ...manifest.provides?.services ?? []];
216236
+ for (const name of names) {
216237
+ if (!name.includes("."))
216238
+ warn(`Plugin "${id}" provides bare channel name "${name}" — third-party names should be vendor-prefixed (e.g. "acme.${name}").`);
216239
+ }
216240
+ }
216241
+ return {
216242
+ id,
216243
+ dir,
216244
+ manifest,
216245
+ lock: { repo, tag: checkout.tag, commit: checkout.commit, version: manifest.version ?? "unknown" }
216246
+ };
216247
+ };
216248
+ var latestPluginTag = (repo) => {
216249
+ try {
216250
+ return ensureRepo(repo, null, undefined).tag;
216251
+ } catch {
216252
+ return null;
216253
+ }
216254
+ };
216255
+
216256
+ // src/commands/compile.ts
216008
216257
  init_util();
216009
216258
  var compile2 = async (args) => {
216010
216259
  await compileImpl(args);
@@ -216016,7 +216265,7 @@ var compileImpl = async (args, shell) => {
216016
216265
  if (out && isNullSink(out)) {
216017
216266
  note(`Discarded output (${out})`);
216018
216267
  } else if (out) {
216019
- writeFileSync7(resolve(out), js);
216268
+ writeFileSync8(resolve2(out), js);
216020
216269
  note(`Wrote ${out} (${js.length} bytes)`);
216021
216270
  } else {
216022
216271
  process.stdout.write(js);
@@ -216027,7 +216276,7 @@ var compileImpl = async (args, shell) => {
216027
216276
  }
216028
216277
  return resources;
216029
216278
  };
216030
- var buildBundle = async (args, shell) => {
216279
+ var buildBundle = async (args, shell, opts = {}) => {
216031
216280
  let root;
216032
216281
  let manifest;
216033
216282
  try {
@@ -216046,19 +216295,21 @@ var buildBundle = async (args, shell) => {
216046
216295
  const config = loadConfig();
216047
216296
  const apiUrl = requireApiUrl(config);
216048
216297
  const token = requireToken(config);
216298
+ const publicUrlFlag = flagStr(args, "public-url");
216299
+ localAssets = !!shell || !!opts.localAssets && publicUrlFlag === undefined && !flagBool(args, "remote-assets");
216049
216300
  note("Resolving project assets…");
216050
216301
  const project = await getProject(apiUrl, token, manifest.uuid);
216051
- ({ entries, warnings } = collectEntries(root, manifest, project, { warnMissingUrl: !shell }));
216302
+ ({ entries, warnings } = collectEntries(root, manifest, project, { warnMissingUrl: !localAssets }));
216052
216303
  name = manifest.name;
216053
- publicUrl = flagStr(args, "public-url") ?? apiUrl;
216054
- localAssets = !!shell;
216055
- if (shell && shell.remotePatterns.length > 0) {
216056
- const isRemote = buildGlobMatcher(shell.remotePatterns);
216304
+ publicUrl = publicUrlFlag ?? apiUrl;
216305
+ const remotePatterns = shell?.remotePatterns ?? (localAssets ? readAppConfigAt(root)?.resources?.remote ?? [] : []);
216306
+ if (localAssets && remotePatterns.length > 0) {
216307
+ const isRemote = buildGlobMatcher(remotePatterns);
216057
216308
  for (const e of entries) {
216058
216309
  if (e.type !== "resource" || !isRemote(e.path))
216059
216310
  continue;
216060
216311
  if (!e.fileSrc) {
216061
- warnErr(`${e.path} matches resources.remote but has no server URL yet — bundling it (push, then re-sync).`);
216312
+ warnErr(`${e.path} matches resources.remote but has no server URL yet — ${shell ? "bundling it (push, then re-sync)" : "reading it from the working tree"}.`);
216062
216313
  continue;
216063
216314
  }
216064
216315
  e.remote = true;
@@ -216068,13 +216319,16 @@ var buildBundle = async (args, shell) => {
216068
216319
  const publicUrlFlag = flagStr(args, "public-url");
216069
216320
  localAssets = shell ? true : publicUrlFlag === undefined;
216070
216321
  publicUrl = publicUrlFlag ?? "";
216322
+ if (opts.localAssets && flagBool(args, "remote-assets")) {
216323
+ warnErr("This is a local project (no server) — assets always load from the working tree.");
216324
+ }
216071
216325
  ({ entries, warnings } = collectLocalEntries(root, null, ""));
216072
216326
  name = basename3(root);
216073
216327
  if (shell && shell.remotePatterns.length > 0) {
216074
216328
  warnErr("resources.remote needs a server-backed project (a streamed asset needs a server URL) — bundling everything.");
216075
216329
  }
216076
216330
  }
216077
- const fontsHeader = await applyProjectFonts(entries, manifest && !shell ? { mode: "cdn", root, publicUrl } : {
216331
+ const fontsHeader = await applyProjectFonts(entries, manifest && !localAssets ? { mode: "cdn", root, publicUrl } : {
216078
216332
  mode: "vendor",
216079
216333
  root,
216080
216334
  vendorDir: root,
@@ -216957,224 +217211,6 @@ buildNumber) — edit it there, then re-run \`lecodes app sync\`. Plugins are ma
216957
217211
  \`lecodes app plugin add <id | github:owner/repo[@tag]>\` and pinned in \`../plugins.lock.json\`.
216958
217212
  `;
216959
217213
 
216960
- // src/commands/appShared.ts
216961
- init_manifest();
216962
- init_ignore();
216963
- init_util();
216964
- import { execFileSync } from "node:child_process";
216965
- import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "node:fs";
216966
- import { homedir as homedir3 } from "node:os";
216967
- import { dirname as dirname8, join as join12, resolve as resolve2 } from "node:path";
216968
- var FIRST_PARTY_PLUGINS = {
216969
- camera: "letary/lecodes-plugins",
216970
- "qr-scanner": "letary/lecodes-plugins",
216971
- geolocation: "letary/lecodes-plugins",
216972
- push: "letary/lecodes-plugins"
216973
- };
216974
- var readAppConfigAt = (root) => readJson(join12(root, "app.json"));
216975
- var DESKTOP_DEFAULT_WINDOW = { width: 1280, height: 720 };
216976
- var desktopDefaultViewport = (root) => {
216977
- const cfg = readAppConfigAt(root);
216978
- if (!cfg?.desktop || cfg.ios || cfg.android)
216979
- return null;
216980
- return {
216981
- width: cfg.desktop.window?.width ?? DESKTOP_DEFAULT_WINDOW.width,
216982
- height: cfg.desktop.window?.height ?? DESKTOP_DEFAULT_WINDOW.height
216983
- };
216984
- };
216985
- var readJson = (path) => {
216986
- if (!existsSync10(path))
216987
- return null;
216988
- try {
216989
- return JSON.parse(readFileSync11(path, "utf8").replace(/^\uFEFF/, ""));
216990
- } catch {
216991
- throw new CliError(`Invalid JSON in ${path}`);
216992
- }
216993
- };
216994
- var writeJson = (path, value) => writeFileSync8(path, JSON.stringify(value, null, 2) + `
216995
- `);
216996
- var projectRoot = () => {
216997
- try {
216998
- return findProjectRoot(process.cwd());
216999
- } catch {
217000
- return process.cwd();
217001
- }
217002
- };
217003
- var git = (cwd, ...args) => execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
217004
- var slugify = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "app";
217005
- var runTool = (cwd, cmd, args) => {
217006
- try {
217007
- return execFileSync(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 128 * 1024 * 1024 });
217008
- } catch (e) {
217009
- const err = e;
217010
- const tail = ((err.stdout ?? "") + `
217011
- ` + (err.stderr ?? "")).split(`
217012
- `).filter((l) => l.trim()).slice(-25).join(`
217013
- `);
217014
- throw new CliError(`${cmd} ${args[0] ?? ""} failed:
217015
- ${tail}`);
217016
- }
217017
- };
217018
- var ensureShellIgnored = (root, dir) => {
217019
- const ignorePath = join12(root, ".lecodesignore");
217020
- const ignore = existsSync10(ignorePath) ? readFileSync11(ignorePath, "utf8") : DEFAULT_IGNORE_FILE;
217021
- if (new RegExp(`^${dir}\\/$`, "m").test(ignore))
217022
- return;
217023
- const comment = ignore.includes("# Native app shells") ? "" : "# Native app shells — generated by `lecodes app init`, never pushed to LeCodes.\n";
217024
- writeFileSync8(ignorePath, ignore.replace(/\n*$/, `
217025
- `) + `
217026
- ` + comment + `${dir}/
217027
- `);
217028
- log(`${c.green("updated")} .lecodesignore (+ ${dir}/)`);
217029
- };
217030
- var SDK_VARIANTS = ["core", "2d", "3d", "full"];
217031
- var detectEngines = (bundleJs) => {
217032
- const lines = [];
217033
- for (const line of bundleJs.split(`
217034
- `)) {
217035
- if (!line.startsWith("//"))
217036
- break;
217037
- lines.push(line);
217038
- }
217039
- const header = lines.join(`
217040
- `);
217041
- return { gl: !/^\/\/ gl: disable$/m.test(header), g2: /^\/\/ 2d: enable$/m.test(header) };
217042
- };
217043
- var resolveVariant = (requested, bundleJs, configKey = "ios.variant") => {
217044
- const uses = detectEngines(bundleJs);
217045
- const detected = uses.gl && uses.g2 ? "full" : uses.gl ? "3d" : uses.g2 ? "2d" : "core";
217046
- if (requested === "auto")
217047
- return { variant: detected, detected };
217048
- const variant = requested;
217049
- const missing = [
217050
- uses.gl && variant !== "3d" && variant !== "full" ? "3D" : null,
217051
- uses.g2 && variant !== "2d" && variant !== "full" ? "2D" : null
217052
- ].filter(Boolean);
217053
- if (missing.length > 0) {
217054
- warn(`project uses the ${missing.join(" + ")} engine but ${configKey} forces "${variant}" — those scenes won't run on device; remove ${configKey} to auto-select "${detected}".`);
217055
- }
217056
- return { variant, detected };
217057
- };
217058
- var VERSION_RE = /^\d+\.\d+\.\d+$/;
217059
- var cmpVersion = (a, b) => {
217060
- const pa = a.split(".").map(Number);
217061
- const pb = b.split(".").map(Number);
217062
- for (let i = 0;i < 3; i++) {
217063
- if ((pa[i] ?? 0) !== (pb[i] ?? 0))
217064
- return (pa[i] ?? 0) - (pb[i] ?? 0);
217065
- }
217066
- return 0;
217067
- };
217068
- var fetchLatestSdkVersion = async (repo) => {
217069
- const url = `https://api.github.com/repos/${repo}/releases/latest`;
217070
- const res = await fetch(url, { headers: { accept: "application/vnd.github+json", "user-agent": "lecodes-cli" } });
217071
- if (!res.ok)
217072
- throw new CliError(`Could not read the latest ${repo} release (HTTP ${res.status}) — ${url}`);
217073
- const tag = (await res.json()).tag_name?.replace(/^v/, "") ?? "";
217074
- if (!VERSION_RE.test(tag))
217075
- throw new CliError(`${repo} latest release has no x.y.z tag (got "${tag}")`);
217076
- return tag;
217077
- };
217078
- var resolveSdkVersion = async (pin, fallback, opts) => {
217079
- if (pin !== "latest") {
217080
- if (!VERSION_RE.test(pin)) {
217081
- throw new CliError(`app.json ${opts.configKey} must be "latest" or an exact x.y.z release (got "${pin}").`);
217082
- }
217083
- if (cmpVersion(pin, opts.minVersion) < 0) {
217084
- throw new CliError(`app.json ${opts.configKey} ${pin} is older than the ${opts.minVersion} these generated shells need.`);
217085
- }
217086
- return pin;
217087
- }
217088
- try {
217089
- return await fetchLatestSdkVersion(opts.repo);
217090
- } catch (e) {
217091
- const kept = fallback ?? opts.minVersion;
217092
- warn(`Could not resolve the latest SDK (${e.message}) — staying on ${kept}.`);
217093
- return kept;
217094
- }
217095
- };
217096
- var ensureRepo = (repo, wantTag, lock) => {
217097
- const override = process.env.LECODES_PLUGINS_DIR;
217098
- if (override) {
217099
- note(`Using local plugin checkout ${override} (LECODES_PLUGINS_DIR)`);
217100
- return { dir: resolve2(override), tag: "local", commit: "local" };
217101
- }
217102
- const cache = join12(homedir3(), ".lecodes", "plugins", repo.replace("/", "__"));
217103
- const url = `https://github.com/${repo}.git`;
217104
- if (!existsSync10(join12(cache, ".git"))) {
217105
- mkdirSync6(dirname8(cache), { recursive: true });
217106
- note(`Cloning ${url}…`);
217107
- git(dirname8(cache), "clone", "--quiet", url, cache);
217108
- }
217109
- let tag = lock?.tag ?? wantTag;
217110
- let commit = lock?.commit;
217111
- if (commit) {
217112
- try {
217113
- git(cache, "cat-file", "-e", `${commit}^{commit}`);
217114
- } catch {
217115
- git(cache, "fetch", "--quiet", "--tags", "origin");
217116
- }
217117
- } else {
217118
- git(cache, "fetch", "--quiet", "--tags", "origin");
217119
- if (!tag) {
217120
- tag = git(cache, "tag", "--list", "v*", "--sort=-version:refname").split(`
217121
- `)[0] ?? "";
217122
- if (!tag)
217123
- throw new CliError(`${repo} has no v* version tags to install from`);
217124
- }
217125
- commit = git(cache, "rev-list", "-n", "1", tag);
217126
- }
217127
- git(cache, "checkout", "--quiet", "--detach", commit);
217128
- return { dir: cache, tag: tag ?? "unknown", commit };
217129
- };
217130
- var resolvePlugin = (id, spec, lock, platform) => {
217131
- let repo;
217132
- let subdir;
217133
- let wantTag = null;
217134
- if (spec.startsWith("github:")) {
217135
- const [repoPart, tagPart] = spec.slice("github:".length).split("@");
217136
- repo = repoPart;
217137
- wantTag = tagPart ?? null;
217138
- subdir = "";
217139
- } else {
217140
- repo = FIRST_PARTY_PLUGINS[id];
217141
- if (!repo) {
217142
- throw new CliError(`Unknown plugin "${id}" — known: ${Object.keys(FIRST_PARTY_PLUGINS).join(", ")}, or use a github:owner/repo spec.`);
217143
- }
217144
- subdir = `plugins/${id}`;
217145
- }
217146
- const checkout = ensureRepo(repo, wantTag, lock?.repo === repo ? lock : undefined);
217147
- const dir = subdir ? join12(checkout.dir, subdir) : checkout.dir;
217148
- const manifest = readJson(join12(dir, "lecodes-plugin.json"));
217149
- if (!manifest)
217150
- throw new CliError(`Plugin "${id}": no lecodes-plugin.json in ${dir}`);
217151
- if (manifest.id !== id)
217152
- throw new CliError(`Plugin "${id}": manifest declares id "${manifest.id}"`);
217153
- if (!manifest[platform]) {
217154
- throw new CliError(`Plugin "${id}" does not support ${platform === "ios" ? "iOS" : "Android"} (manifest "${platform}" is null)`);
217155
- }
217156
- if (spec.startsWith("github:")) {
217157
- const names = [...manifest.provides?.views ?? [], ...manifest.provides?.services ?? []];
217158
- for (const name of names) {
217159
- if (!name.includes("."))
217160
- warn(`Plugin "${id}" provides bare channel name "${name}" — third-party names should be vendor-prefixed (e.g. "acme.${name}").`);
217161
- }
217162
- }
217163
- return {
217164
- id,
217165
- dir,
217166
- manifest,
217167
- lock: { repo, tag: checkout.tag, commit: checkout.commit, version: manifest.version ?? "unknown" }
217168
- };
217169
- };
217170
- var latestPluginTag = (repo) => {
217171
- try {
217172
- return ensureRepo(repo, null, undefined).tag;
217173
- } catch {
217174
- return null;
217175
- }
217176
- };
217177
-
217178
217214
  // src/commands/appAndroid.ts
217179
217215
  init_manifest();
217180
217216
  import { execFileSync as execFileSync2, spawn, spawnSync as spawnSync2 } from "node:child_process";
@@ -218692,6 +218728,106 @@ import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
218692
218728
  import { copyFileSync, existsSync as existsSync14, mkdirSync as mkdirSync9, mkdtempSync, readdirSync as readdirSync5, readFileSync as readFileSync14, renameSync as renameSync2, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync11 } from "node:fs";
218693
218729
  import { homedir as homedir5, tmpdir } from "node:os";
218694
218730
  import { join as join15, resolve as resolve4 } from "node:path";
218731
+
218732
+ // ../viewer-lite/src/sourcemapError.ts
218733
+ var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
218734
+ var B64_INDEX = {};
218735
+ for (let i2 = 0;i2 < B64.length; i2++)
218736
+ B64_INDEX[B64[i2]] = i2;
218737
+ var decodeVlq = (segment) => {
218738
+ const out = [];
218739
+ let shift = 0, value = 0;
218740
+ for (const ch of segment) {
218741
+ const digit = B64_INDEX[ch];
218742
+ if (digit === undefined)
218743
+ break;
218744
+ const more = digit & 32;
218745
+ value += (digit & 31) << shift;
218746
+ if (more) {
218747
+ shift += 5;
218748
+ } else {
218749
+ const negative = value & 1;
218750
+ value >>= 1;
218751
+ out.push(negative ? -value : value);
218752
+ value = 0;
218753
+ shift = 0;
218754
+ }
218755
+ }
218756
+ return out;
218757
+ };
218758
+
218759
+ class SourceMap {
218760
+ sources;
218761
+ lines;
218762
+ constructor(raw) {
218763
+ this.sources = raw.sources;
218764
+ this.lines = [];
218765
+ let srcIdx = 0, srcLine = 0, srcCol = 0;
218766
+ for (const lineStr of raw.mappings.split(";")) {
218767
+ const segs = [];
218768
+ let genCol = 0;
218769
+ if (lineStr) {
218770
+ for (const segStr of lineStr.split(",")) {
218771
+ const v = decodeVlq(segStr);
218772
+ if (v.length === 0)
218773
+ continue;
218774
+ genCol += v[0];
218775
+ if (v.length >= 4) {
218776
+ srcIdx += v[1];
218777
+ srcLine += v[2];
218778
+ srcCol += v[3];
218779
+ segs.push([genCol, srcIdx, srcLine, srcCol]);
218780
+ }
218781
+ }
218782
+ }
218783
+ this.lines.push(segs);
218784
+ }
218785
+ }
218786
+ originalPositionFor(line, column) {
218787
+ const segs = this.lines[line - 1];
218788
+ if (!segs || segs.length === 0)
218789
+ return null;
218790
+ let best = null;
218791
+ for (const seg of segs) {
218792
+ if (seg[0] <= column)
218793
+ best = seg;
218794
+ else
218795
+ break;
218796
+ }
218797
+ best ??= segs[0];
218798
+ const source = this.sources[best[1]];
218799
+ if (source === undefined)
218800
+ return null;
218801
+ return { source, line: best[2] + 1, column: best[3] };
218802
+ }
218803
+ }
218804
+ var extractInlineSourceMap = (code) => {
218805
+ const m = code.match(/\/\/# lecodesMap=data:application\/json;(?:charset=[^;]+;)?base64,([A-Za-z0-9+/=]+)\s*$/);
218806
+ if (!m)
218807
+ return null;
218808
+ try {
218809
+ const json = typeof atob === "function" ? atob(m[1]) : Buffer.from(m[1], "base64").toString("utf8");
218810
+ return new SourceMap(JSON.parse(json));
218811
+ } catch {
218812
+ return null;
218813
+ }
218814
+ };
218815
+ var mapNamedFileStack = (text, map, filename) => {
218816
+ const escaped = filename.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
218817
+ return text.replace(new RegExp(`\\b${escaped}:(\\d+):(\\d+)`, "g"), (whole, l2, c4) => {
218818
+ const orig = map.originalPositionFor(parseInt(l2, 10), Math.max(0, parseInt(c4, 10) - 1));
218819
+ return orig ? `${orig.source.replace(/^(\.\/|\/)/, "")}:${orig.line}:${orig.column}` : whole;
218820
+ });
218821
+ };
218822
+
218823
+ // src/compile/nativeStack.ts
218824
+ var BUNDLE_FILENAME = "module.js";
218825
+ var makeStackDemangler = (bundleJs) => {
218826
+ const map = extractInlineSourceMap(bundleJs);
218827
+ return map ? (chunk) => mapNamedFileStack(chunk, map, BUNDLE_FILENAME) : null;
218828
+ };
218829
+
218830
+ // src/desktopRenderer.ts
218695
218831
  var MANIFEST_URL = "https://storage.yandexcloud.net/le-codes/releases/versions.json";
218696
218832
  var DESKTOP_RENDERER_VERSION = "1.1.0";
218697
218833
  var semverNewer = (a2, b) => {
@@ -218864,13 +219000,34 @@ var stageBundle = (js, resources) => {
218864
219000
  }
218865
219001
  return runDir;
218866
219002
  };
219003
+ var pipeThroughDemangler = (src, dst, map) => {
219004
+ let rest = "";
219005
+ src.on("data", (d) => {
219006
+ rest += String(d);
219007
+ const nl = rest.lastIndexOf(`
219008
+ `);
219009
+ if (nl < 0)
219010
+ return;
219011
+ dst.write(map(rest.slice(0, nl + 1)));
219012
+ rest = rest.slice(nl + 1);
219013
+ });
219014
+ src.on("end", () => {
219015
+ if (rest)
219016
+ dst.write(map(rest));
219017
+ });
219018
+ };
218867
219019
  var runDesktopApp = async (js, resources, opts = {}) => {
218868
219020
  const exe = await resolveDesktopExe();
218869
219021
  const runDir = stageBundle(js, resources);
218870
219022
  try {
218871
219023
  const env = { ...process.env, ...windowEnv(opts) };
219024
+ const demangle = makeStackDemangler(js);
218872
219025
  await new Promise((done, fail) => {
218873
- const child = spawn2(exe, [join15(runDir, "app.js")], { cwd: runDir, env, stdio: ["ignore", "inherit", "inherit"] });
219026
+ const child = demangle ? spawn2(exe, [join15(runDir, "app.js")], { cwd: runDir, env, stdio: ["ignore", "pipe", "pipe"] }) : spawn2(exe, [join15(runDir, "app.js")], { cwd: runDir, env, stdio: ["ignore", "inherit", "inherit"] });
219027
+ if (demangle) {
219028
+ pipeThroughDemangler(child.stdout, process.stdout, demangle);
219029
+ pipeThroughDemangler(child.stderr, process.stderr, demangle);
219030
+ }
218874
219031
  child.on("error", (e2) => fail(new CliError(`Can't run the desktop host: ${e2.message}`)));
218875
219032
  child.on("exit", (code) => {
218876
219033
  if (code === 0 || code === null)
@@ -218898,26 +219055,33 @@ var runDesktopRender = async (js, resources, opts) => {
218898
219055
  try {
218899
219056
  const appJs = join15(runDir, "app.js");
218900
219057
  const outAbs = resolve4(opts.outPng);
218901
- await new Promise((done, fail) => {
218902
- const child = spawn2(exe, [appJs], {
218903
- cwd: runDir,
218904
- env: {
218905
- ...process.env,
218906
- CREATOR_DUMP_PNG: outAbs,
218907
- CREATOR_MAX_FRAMES: String(opts.frames),
218908
- CREATOR_WINDOW_SIZE: `${opts.width}x${opts.height}`,
218909
- CREATOR_HIDDEN: "1"
218910
- },
218911
- stdio: ["ignore", "pipe", "pipe"]
218912
- });
218913
- const tail = [];
219058
+ const env = {
219059
+ ...process.env,
219060
+ CREATOR_DUMP_PNG: outAbs,
219061
+ CREATOR_MAX_FRAMES: String(opts.frames),
219062
+ CREATOR_WINDOW_SIZE: `${opts.width}x${opts.height}`,
219063
+ CREATOR_HIDDEN: "1"
219064
+ };
219065
+ if (opts.script !== undefined) {
219066
+ const scriptPath = join15(runDir, "script.txt");
219067
+ writeFileSync11(scriptPath, opts.script);
219068
+ env.CREATOR_SCRIPT = scriptPath;
219069
+ }
219070
+ if (opts.fixedDtMs)
219071
+ env.CREATOR_FIXED_DT = String(opts.fixedDtMs);
219072
+ if (opts.camera)
219073
+ env.CREATOR_CAMERA = opts.camera;
219074
+ if (opts.view)
219075
+ env.CREATOR_VIEW = opts.view;
219076
+ const demangle = makeStackDemangler(js) ?? ((text) => text);
219077
+ return await new Promise((done, fail) => {
219078
+ const child = spawn2(exe, [appJs], { cwd: runDir, env, stdio: ["ignore", "pipe", "pipe"] });
219079
+ const chunks = [];
218914
219080
  const sink = (d) => {
218915
219081
  const text = String(d);
218916
- tail.push(text);
218917
- while (tail.length > 80)
218918
- tail.shift();
219082
+ chunks.push(text);
218919
219083
  if (opts.logs)
218920
- process.stderr.write(text);
219084
+ process.stderr.write(demangle(text));
218921
219085
  };
218922
219086
  child.stdout.on("data", sink);
218923
219087
  child.stderr.on("data", sink);
@@ -218931,10 +219095,12 @@ var runDesktopRender = async (js, resources, opts) => {
218931
219095
  });
218932
219096
  child.on("exit", (code) => {
218933
219097
  clearTimeout(killer);
219098
+ const output = demangle(chunks.join(""));
218934
219099
  if (code === 0 && existsSync14(outAbs))
218935
- return done();
219100
+ return done({ output, exitCode: code });
219101
+ const tail = demangle(chunks.slice(-80).join("")).trimEnd();
218936
219102
  const detail = tail.length > 0 ? `
218937
- ${tail.join("").trimEnd()}` : "";
219103
+ ${tail}` : "";
218938
219104
  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}`));
218939
219105
  });
218940
219106
  });
@@ -219951,6 +220117,7 @@ var clone3 = async (args) => {
219951
220117
  writeTsconfig(root);
219952
220118
  }
219953
220119
  writeDefaultIgnore(root);
220120
+ ensureGitignore(root);
219954
220121
  const commits = await getCommits(apiUrl, token, uuid);
219955
220122
  writeManifest(root, { uuid, apiUrl, name: project.name, baseHeadId: commits.headId, files });
219956
220123
  success(`Cloned ${c.bold(project.name)} into ${c.bold(dir)}/ (${Object.keys(files).length} files)`);
@@ -219980,12 +220147,22 @@ from \`main.ts\`.
219980
220147
  \`\`\`
219981
220148
  lecodes render # semantic JSON of the launched UI (structure, text, names, values)
219982
220149
  lecodes render --png shot.png # pixels (needs @napi-rs/canvas installed)
220150
+ lecodes render --png card.png --clip card # ONE component/region: a node name or x,y,w,h
219983
220151
  lecodes render --desktop --png shot.png # NATIVE render: real Filament 3D + UI + 2D composited —
219984
220152
  # use this to SEE 3D scenes / procedural geometry (headless
219985
220153
  # renders record 3D as JSON only). Windows/Linux; downloads
219986
220154
  # the renderer on first use. --frames <n> for slow loads.
219987
220155
  lecodes render --script <file> # drive the app through a scenario (format below)
220156
+ lecodes render --desktop --view iso --png iso.png # 3D: inspection camera fitted to the WHOLE
220157
+ # scene (iso|top|front|side|back [--fov N]) — or --camera x,y,z
220158
+ # --look-at x,y,z; the app's camera is untouched. In scripts:
220159
+ # { "view": "iso", "screenshot": "iso.png" } then { "view": "off" }
220160
+ lecodes render --desktop --script <file> # the same scenario on the NATIVE host (3D/Jolt, real
220161
+ # GPU): coordinates only ({ x, y }), expect.log checked,
220162
+ # text/node/screen expects skipped; deterministic 16 ms frames
219988
220163
  lecodes test # all tests/*.flow.json + design-derived flows; run before "done"
220164
+ lecodes test --desktop # the file flows on the NATIVE host (3D/Jolt/real GPU; coordinate
220165
+ # flows only — design flows stay headless)
219989
220166
  \`\`\`
219990
220167
 
219991
220168
  Iterate with \`--script\` scenarios and keep the useful ones as \`tests/<name>.flow.json\` — they
@@ -219994,19 +220171,32 @@ are the regression suite. A failing step dumps the failing frame (JSON + PNG) un
219994
220171
 
219995
220172
  ## Scenario format (tests/*.flow.json and --script files)
219996
220173
 
219997
- One action per step (\`tap\`, \`type\`+\`text\`, \`fill\`, \`scroll\` [+\`by\`], \`wait\`), then an
220174
+ One action per step (\`tap\`, \`type\`+\`text\`, \`fill\`, \`scroll\` [+\`by\`], \`wait\`, and for
220175
+ games: \`frames\` (advance N fixed 1/60 steps, no sleeping), \`drag\`+\`to\` [+\`ms\`], \`hold\`+\`ms\`,
220176
+ \`key\`+\`ms\` / \`keyDown\` / \`keyUp\` (KeyboardEvent.code: "ArrowRight", "KeyW", "Space")), then an
219998
220177
  optional \`expect\` (\`screen\`, \`node\`/\`notNode\`, \`text\`/\`notText\`,
219999
- \`value: { target, equals }\`), then outputs (\`screenshot\`, \`json\`). Targets resolve by
220000
- \`name\` → exact visible text → substring → input placeholder. Envelope keys: \`name\`,
220178
+ \`value: { target, equals }\`, \`log\` — a \`console.log\` line contains it), then outputs
220179
+ (\`screenshot\` [+\`clip\`: a node name or \`{ x, y, width, height }\`], \`json\`). Targets resolve by
220180
+ \`name\` → exact visible text → substring → input placeholder; a tap/drag on empty space reaches
220181
+ the 2D scene (sprite \`click\`/\`touchstart\`, \`ev.track()\`). Envelope keys: \`name\`,
220001
220182
  \`width\`/\`height\`, \`settle\`, \`fixtures\`, \`steps\`.
220002
220183
 
220003
220184
  \`\`\`json
220004
220185
  { "steps": [
220005
220186
  { "expect": { "screen": "home", "text": "Taps: 0" } },
220006
- { "tap": "tap", "expect": { "text": "Taps: 1" }, "screenshot": "after.png" }
220187
+ { "tap": "tap", "expect": { "text": "Taps: 1" }, "screenshot": "after.png" },
220188
+ { "key": "ArrowRight", "ms": 500, "expect": { "log": "hero x=" } },
220189
+ { "frames": 60, "screenshot": "hud.png", "clip": "hud" }
220007
220190
  ] }
220008
220191
  \`\`\`
220009
220192
 
220193
+ Game time is deterministic headless: \`setLoop\`, 2D physics/animation and aspect \`update(dt)\`
220194
+ advance in fixed 1/60 steps driven by \`wait\`/\`frames\`/\`drag\`/\`key\` — never by a wall clock —
220195
+ so a flow that passes once passes every time. \`Input.key(code)\` reads the keys a flow holds.
220196
+ Headless draws UI + the 2D scene (sprites, tilemaps, Box2D); 3D is captured as JSON and Jolt does
220197
+ not simulate — use \`--desktop\` for 3D pixels. \`console.log\` + \`expect.log\` is the assertion
220198
+ channel for game state that has no UI.
220199
+
220010
220200
  ## Make the app testable
220011
220201
 
220012
220202
  - Give interactive elements a \`name\`: \`UIButton(...).style({ name: "login" })\`.
@@ -220022,7 +220212,8 @@ optional \`expect\` (\`screen\`, \`node\`/\`notNode\`, \`text\`/\`notText\`,
220022
220212
 
220023
220213
  ## SDK documentation
220024
220214
 
220025
- \`.lecodes/types/\` is the exact API surface (signatures only). For semantics and examples,
220215
+ \`.lecodes/types/\` is the exact API surface (signatures only; generated and gitignored — run
220216
+ \`lecodes types\` if it's missing, never commit \`.lecodes/\`). For semantics and examples,
220026
220217
  fetch the docs — they are served as raw markdown for AI tools:
220027
220218
 
220028
220219
  - https://le.codes/llms.txt — the docs map; fetch it first to find the right page
@@ -220116,7 +220307,7 @@ var run = async (args) => {
220116
220307
  }
220117
220308
  const appCfg = readAppConfigAt(root);
220118
220309
  const win = appCfg?.desktop?.window;
220119
- const { js, resources } = await buildBundle(args);
220310
+ const { js, resources } = await buildBundle(args, undefined, { localAssets: true });
220120
220311
  note("Launching LeCodes Desktop… (close the window to return)");
220121
220312
  await runDesktopApp(js, resources, {
220122
220313
  width: Number(flagStr(args, "width")) || win?.width,
@@ -220207,23 +220398,29 @@ var DEV_PRELUDE_JS = `
220207
220398
  } catch (e) {}
220208
220399
  }
220209
220400
 
220401
+ // An Error carries its stack (JSON.stringify would send a useless "{}"); the dev server maps the
220402
+ // frames back to project positions before printing.
220403
+ function describe(a) {
220404
+ if (typeof a === "string") return a
220405
+ if (a && typeof a.stack === "string" && typeof a.message === "string") {
220406
+ return a.stack ? String(a) + "\\n" + a.stack : String(a)
220407
+ }
220408
+ try { return JSON.stringify(a) } catch (e) { return String(a) }
220409
+ }
220410
+
220210
220411
  // Mirror console output to the dev terminal ('silent' guards recursion through wrapped calls).
220211
220412
  var silent = false
220212
220413
  ;["log", "warn", "error"].forEach(function (level) {
220213
220414
  var orig = console[level]
220214
220415
  console[level] = function () {
220215
220416
  var parts = []
220216
- for (var i = 0; i < arguments.length; i++) {
220217
- var a = arguments[i]
220218
- if (typeof a === "string") { parts.push(a); continue }
220219
- try { parts.push(JSON.stringify(a)) } catch (e) { parts.push(String(a)) }
220220
- }
220417
+ for (var i = 0; i < arguments.length; i++) parts.push(describe(arguments[i]))
220221
220418
  try { orig.apply(console, arguments) } catch (e) {}
220222
220419
  if (!silent) { silent = true; try { post({ type: "log", level: level, text: parts.join(" ") }) } finally { silent = false } }
220223
220420
  }
220224
220421
  })
220225
220422
 
220226
- function report(err) { post({ type: "error", text: String(err) }) }
220423
+ function report(err) { post({ type: "error", text: describe(err) }) }
220227
220424
 
220228
220425
  function load(v) {
220229
220426
  if (busy) { queued = v; return }
@@ -220501,6 +220698,14 @@ var startDevServer = async (options) => {
220501
220698
  for (const client of clients)
220502
220699
  client.send(text);
220503
220700
  };
220701
+ let demangleFor = null;
220702
+ const demangle = (text) => {
220703
+ if (bundleJs === null)
220704
+ return text;
220705
+ if (demangleFor?.js !== bundleJs)
220706
+ demangleFor = { js: bundleJs, map: makeStackDemangler(bundleJs) };
220707
+ return demangleFor.map ? demangleFor.map(text) : text;
220708
+ };
220504
220709
  const printDeviceMessage = (raw) => {
220505
220710
  let m = {};
220506
220711
  try {
@@ -220509,9 +220714,9 @@ var startDevServer = async (options) => {
220509
220714
  return;
220510
220715
  }
220511
220716
  if (m.type === "log")
220512
- log2(`[device:${m.level ?? "log"}] ${m.text ?? ""}`);
220717
+ log2(`[device:${m.level ?? "log"}] ${demangle(m.text ?? "")}`);
220513
220718
  else if (m.type === "error")
220514
- log2(`[device:error] ${m.text ?? ""}`);
220719
+ log2(`[device:error] ${demangle(m.text ?? "")}`);
220515
220720
  };
220516
220721
  const transportOf = (url) => url.searchParams.get("t") === "poll" ? "poll" : "ws";
220517
220722
  const handle = async (req, res) => {
@@ -221086,6 +221291,9 @@ lecodes app open # open it in Xcode, build & run
221086
221291
  - \`assets/\` — images/fonts the app references (created on demand)
221087
221292
  - \`tsconfig.json\` + \`.lecodes/types/\` — generated IDE types (rewritten by the CLI; don't edit).
221088
221293
  They come from the SDK bundled in the CLI; \`lecodes types\` refreshes them from the platform.
221294
+ - \`.lecodes/\` is gitignored — per-checkout state (those types + \`manifest.json\`, this machine's
221295
+ sync cursor once linked). After a fresh git clone: \`lecodes types\`, then
221296
+ \`lecodes link --project <uuid>\` to re-attach.
221089
221297
  - \`.lecodesignore\` — files a future \`lecodes push\` would skip
221090
221298
 
221091
221299
  Full command list: \`lecodes --help\`.
@@ -221116,6 +221324,14 @@ var init2 = async (args) => {
221116
221324
  write(".claude/settings.json", CLAUDE_SETTINGS);
221117
221325
  write("tests/counter.flow.json", STARTER_FLOW);
221118
221326
  write(".lecodesignore", DEFAULT_IGNORE_FILE);
221327
+ const gi = ensureGitignore(root);
221328
+ if (gi === "created") {
221329
+ log(`${c.green("created")} .gitignore`);
221330
+ created++;
221331
+ } else if (gi === "updated")
221332
+ log(`${c.green("updated")} .gitignore ${c.dim("(+ .lecodes/)")}`);
221333
+ else
221334
+ note("kept .gitignore");
221119
221335
  const wroteTypes = materializeTypesLocal(root);
221120
221336
  writeTsconfig(root);
221121
221337
  log(`${c.green("written")} tsconfig.json${wroteTypes ? ` + ${LECODES_DIR}/types/` : ""} (IDE types)`);
@@ -221367,6 +221583,8 @@ var link = async (args) => {
221367
221583
  log(`${c.green("written")} ${LECODES_DIR}/types/ + tsconfig.json ${c.dim("(from the server)")}`);
221368
221584
  }
221369
221585
  writeDefaultIgnore(root);
221586
+ if (ensureGitignore(root))
221587
+ log(`${c.green("written")} .gitignore ${c.dim("(.lecodes/ is per-checkout state)")}`);
221370
221588
  if (flagBool(args, "push")) {
221371
221589
  log("");
221372
221590
  if (root !== process.cwd())
@@ -221403,7 +221621,7 @@ var login = async (args) => {
221403
221621
 
221404
221622
  // src/commands/render.ts
221405
221623
  import { mkdirSync as mkdirSync18, readFileSync as readFileSync22, writeFileSync as writeFileSync19 } from "node:fs";
221406
- import { dirname as dirname15, resolve as resolve11 } from "node:path";
221624
+ import { dirname as dirname15, resolve as resolve12 } from "node:path";
221407
221625
 
221408
221626
  // src/compile/headlessBundle.ts
221409
221627
  init_api();
@@ -221484,6 +221702,240 @@ var compileHeadlessBundle = async (args) => {
221484
221702
 
221485
221703
  // src/commands/render.ts
221486
221704
  init_manifest();
221705
+
221706
+ // src/desktopScript.ts
221707
+ init_util();
221708
+ import { resolve as resolve11 } from "node:path";
221709
+ var isPoint = (v) => typeof v === "object" && v !== null && typeof v.x === "number" && typeof v.y === "number";
221710
+ var FRAME_MS = 16;
221711
+ var ACTIONS = ["tap", "type", "fill", "scroll", "wait", "frames", "drag", "hold", "key", "keyDown", "keyUp", "camera", "view"];
221712
+ var SEMANTIC_EXPECT = ["text", "notText", "node", "notNode", "screen", "value"];
221713
+ var VIEW_NAMES = ["iso", "top", "front", "side", "back"];
221714
+ var isVec3 = (v) => Array.isArray(v) && v.length === 3 && v.every((n2) => typeof n2 === "number");
221715
+ var pt = (p2) => `(${p2.x},${p2.y})`;
221716
+ var translateScenario = (rawSteps, opts) => {
221717
+ if (!Array.isArray(rawSteps))
221718
+ throw new CliError(`"steps" must be an array of step objects.`);
221719
+ const lines = [];
221720
+ const steps = [];
221721
+ let budgetMs = 0;
221722
+ const emit = (line) => {
221723
+ lines.push(line);
221724
+ return lines.length;
221725
+ };
221726
+ emit(`# generated by lecodes render --desktop --script`);
221727
+ emit(`frames ${Math.max(0, opts.headroomFrames)}`);
221728
+ const coord = (at, key, v) => {
221729
+ if (isPoint(v))
221730
+ return v;
221731
+ if (typeof v === "string") {
221732
+ throw new CliError(`${at}: "${key}": "${v}" — the native host can't resolve names; give --desktop scripts { "x", "y" } coordinates (logical px), or run the scenario headless (without --desktop).`);
221733
+ }
221734
+ throw new CliError(`${at}: "${key}" must be { "x", "y" }.`);
221735
+ };
221736
+ const num = (at, key, v, def) => {
221737
+ if (v === undefined && def !== undefined)
221738
+ return def;
221739
+ if (typeof v !== "number")
221740
+ throw new CliError(`${at}: "${key}" must be a number.`);
221741
+ return v;
221742
+ };
221743
+ let inspecting = false;
221744
+ rawSteps.forEach((step, i2) => {
221745
+ const at = `steps[${i2}]`;
221746
+ if (typeof step !== "object" || step === null || Array.isArray(step))
221747
+ throw new CliError(`${at}: must be an object.`);
221748
+ const s2 = step;
221749
+ const actions = ACTIONS.filter((k) => s2[k] !== undefined);
221750
+ if (actions.length > 1)
221751
+ throw new CliError(`${at}: one action per step (found ${actions.join(" + ")}).`);
221752
+ const plan = { index: i2, desc: "", lines: [], skippedExpect: [], skippedOutputs: [] };
221753
+ const parts = [];
221754
+ const action = actions[0];
221755
+ if (action === "type" || action === "fill" || action === "scroll") {
221756
+ throw new CliError(`${at}: "${action}" isn't available with --desktop (the native host has no semantic tree / soft keyboard) — run this scenario headless, or drive the gesture with "drag" coordinates.`);
221757
+ }
221758
+ if ((action === "tap" || action === "drag" || action === "hold") && inspecting) {
221759
+ throw new CliError(`${at}: "${action}" while an inspection camera is active — the app's hit-testing uses its own camera, not the one in the shot. Add { "view": "off" } (or { "camera": "off" }) first.`);
221760
+ }
221761
+ if (action === "view") {
221762
+ const v = s2.view;
221763
+ const fov = s2.fov !== undefined ? num(at, "fov", s2.fov) : undefined;
221764
+ if (v === "off") {
221765
+ plan.lines.push(emit("view off"));
221766
+ inspecting = false;
221767
+ parts.push("view off");
221768
+ } else if (typeof v === "string" && VIEW_NAMES.includes(v)) {
221769
+ plan.lines.push(emit(`view ${v}${fov !== undefined ? ` ${fov}` : ""}`));
221770
+ inspecting = true;
221771
+ parts.push(`view ${v}${fov !== undefined ? ` (fov ${fov})` : ""}`);
221772
+ } else
221773
+ throw new CliError(`${at}: "view" wants one of ${VIEW_NAMES.join(", ")} or "off".`);
221774
+ budgetMs += 2 * FRAME_MS;
221775
+ } else if (action === "camera") {
221776
+ const cam = s2.camera;
221777
+ if (cam === "off") {
221778
+ plan.lines.push(emit("camera off"));
221779
+ inspecting = false;
221780
+ parts.push("camera off");
221781
+ } else if (typeof cam === "object" && cam !== null && isVec3(cam.at) && isVec3(cam.lookAt)) {
221782
+ const { at: eye, lookAt, fov } = cam;
221783
+ const fovN = fov !== undefined ? num(at, "camera.fov", fov) : undefined;
221784
+ plan.lines.push(emit(`camera ${eye.join(" ")} ${lookAt.join(" ")}${fovN !== undefined ? ` ${fovN}` : ""}`));
221785
+ inspecting = true;
221786
+ parts.push(`camera at (${eye.join(",")}) → (${lookAt.join(",")})`);
221787
+ } else
221788
+ throw new CliError(`${at}: "camera" wants { "at": [x,y,z], "lookAt": [x,y,z], "fov"? } or "off".`);
221789
+ budgetMs += 2 * FRAME_MS;
221790
+ }
221791
+ if (s2.fov !== undefined && action !== "view")
221792
+ throw new CliError(`${at}: "fov" only goes with "view" (inside "camera" use camera.fov).`);
221793
+ if (action === "tap") {
221794
+ const p2 = coord(at, "tap", s2.tap);
221795
+ plan.lines.push(emit(`tap ${p2.x} ${p2.y}`));
221796
+ budgetMs += 4 * FRAME_MS;
221797
+ parts.push(`tap ${pt(p2)}`);
221798
+ } else if (action === "drag") {
221799
+ const a2 = coord(at, "drag", s2.drag), b = coord(at, "to", s2.to);
221800
+ const ms = num(at, "ms", s2.ms, 300);
221801
+ plan.lines.push(emit(`drag ${a2.x} ${a2.y} ${b.x} ${b.y} ${Math.round(ms)}`));
221802
+ budgetMs += ms + 4 * FRAME_MS;
221803
+ parts.push(`drag ${pt(a2)} → ${pt(b)} over ${ms}ms`);
221804
+ } else if (action === "hold") {
221805
+ const p2 = coord(at, "hold", s2.hold);
221806
+ const ms = num(at, "ms", s2.ms);
221807
+ plan.lines.push(emit(`hold ${p2.x} ${p2.y} ${Math.round(ms)}`));
221808
+ budgetMs += ms + 4 * FRAME_MS;
221809
+ parts.push(`hold ${pt(p2)} ${ms}ms`);
221810
+ } else if (action === "key") {
221811
+ if (typeof s2.key !== "string")
221812
+ throw new CliError(`${at}: "key" must be a KeyboardEvent.code string.`);
221813
+ const ms = num(at, "ms", s2.ms);
221814
+ plan.lines.push(emit(`key ${s2.key} ${Math.round(ms)}`));
221815
+ budgetMs += ms + 2 * FRAME_MS;
221816
+ parts.push(`key ${s2.key} ${ms}ms`);
221817
+ } else if (action === "keyDown" || action === "keyUp") {
221818
+ const code = s2[action];
221819
+ if (typeof code !== "string")
221820
+ throw new CliError(`${at}: "${action}" must be a code string.`);
221821
+ plan.lines.push(emit(`${action.toLowerCase()} ${code}`));
221822
+ budgetMs += 2 * FRAME_MS;
221823
+ parts.push(`${action} ${code}`);
221824
+ } else if (action === "wait") {
221825
+ const ms = num(at, "wait", s2.wait);
221826
+ plan.lines.push(emit(`wait ${Math.round(ms)}`));
221827
+ budgetMs += ms + FRAME_MS;
221828
+ parts.push(`wait ${ms}ms`);
221829
+ } else if (action === "frames") {
221830
+ const n2 = num(at, "frames", s2.frames);
221831
+ plan.lines.push(emit(`frames ${Math.round(n2)}`));
221832
+ budgetMs += n2 * FRAME_MS + FRAME_MS;
221833
+ parts.push(`advance ${n2} frames`);
221834
+ }
221835
+ if (s2.expect !== undefined) {
221836
+ if (typeof s2.expect !== "object" || s2.expect === null)
221837
+ throw new CliError(`${at}: "expect" must be an object.`);
221838
+ const e2 = s2.expect;
221839
+ const bits = [];
221840
+ for (const k of Object.keys(e2)) {
221841
+ if (k === "log") {
221842
+ if (typeof e2.log !== "string")
221843
+ throw new CliError(`${at}: expect.log must be a string.`);
221844
+ plan.expectLog = e2.log;
221845
+ bits.push(`log~"${e2.log}"`);
221846
+ } else if (SEMANTIC_EXPECT.includes(k)) {
221847
+ plan.skippedExpect.push(k);
221848
+ } else {
221849
+ throw new CliError(`${at}: unknown expect key "${k}".`);
221850
+ }
221851
+ }
221852
+ if (bits.length > 0)
221853
+ parts.push(`expect ${bits.join(" · ")}`);
221854
+ }
221855
+ if (s2.clip !== undefined)
221856
+ throw new CliError(`${at}: "clip" isn't available with --desktop (the native host dumps the whole window).`);
221857
+ if (s2.screenshot !== undefined) {
221858
+ if (typeof s2.screenshot !== "string")
221859
+ throw new CliError(`${at}: "screenshot" must be a path.`);
221860
+ plan.screenshot = resolve11(opts.outDir, s2.screenshot);
221861
+ plan.lines.push(emit(`frames 2`));
221862
+ plan.lines.push(emit(`shot ${plan.screenshot}`));
221863
+ budgetMs += 3 * FRAME_MS;
221864
+ parts.push(`→ ${s2.screenshot}`);
221865
+ }
221866
+ if (s2.json !== undefined)
221867
+ plan.skippedOutputs.push("json");
221868
+ if (plan.lines.length === 0) {
221869
+ plan.lines.push(emit(`frames 1`));
221870
+ budgetMs += FRAME_MS;
221871
+ }
221872
+ plan.desc = typeof s2.name === "string" ? s2.name : parts.join(", ") || "(empty step)";
221873
+ steps.push(plan);
221874
+ });
221875
+ const frameCap = Math.max(0, opts.headroomFrames) + Math.ceil(budgetMs / FRAME_MS) + 600;
221876
+ return { text: lines.join(`
221877
+ `) + `
221878
+ `, steps, frameCap };
221879
+ };
221880
+ var evaluateDesktopRun = (plan, output) => {
221881
+ const lines = output.split(/\r?\n/);
221882
+ const doneAt = new Map;
221883
+ const consoleLines = [];
221884
+ let loaded = false, ended = false;
221885
+ for (const line of lines) {
221886
+ const m = /^\[script\] #(\d+) (done|error) (\S+)(?:: (.*))?$/.exec(line);
221887
+ if (m) {
221888
+ doneAt.set(Number(m[1]), { at: consoleLines.length, error: m[2] === "error" ? `${m[3]}: ${m[4] ?? "failed"}` : undefined });
221889
+ continue;
221890
+ }
221891
+ if (line.startsWith("[script] loaded")) {
221892
+ loaded = true;
221893
+ continue;
221894
+ }
221895
+ if (line === "[script] end") {
221896
+ ended = true;
221897
+ continue;
221898
+ }
221899
+ if (line.startsWith("[script]"))
221900
+ continue;
221901
+ consoleLines.push(line);
221902
+ }
221903
+ const steps = [];
221904
+ let ok = true;
221905
+ for (const st of plan.steps) {
221906
+ const skipped = [
221907
+ ...st.skippedExpect.map((k) => `expect.${k} (no semantic tree on --desktop)`),
221908
+ ...st.skippedOutputs.map((k) => `${k} output (no semantic tree on --desktop)`)
221909
+ ];
221910
+ let error;
221911
+ let cutoff = consoleLines.length;
221912
+ for (const ln of st.lines) {
221913
+ const d = doneAt.get(ln);
221914
+ if (!d) {
221915
+ error = error ?? "the host never completed this step (crashed, closed, or hit the frame cap before it)";
221916
+ break;
221917
+ }
221918
+ if (d.error) {
221919
+ error = error ?? d.error;
221920
+ break;
221921
+ }
221922
+ cutoff = d.at;
221923
+ }
221924
+ if (!error && st.expectLog !== undefined) {
221925
+ const seen = consoleLines.slice(0, cutoff);
221926
+ if (!seen.some((l2) => l2.includes(st.expectLog))) {
221927
+ const tail = seen.filter((l2) => l2.startsWith("[DEBUG]") || l2.startsWith("[INFO]") || l2.startsWith("[WARN]") || l2.startsWith("[ERROR]")).slice(-3);
221928
+ error = `expected a console line containing "${st.expectLog}" — ${tail.length === 0 ? "the app logged nothing" : `last lines: ${tail.map((l2) => `"${l2}"`).join(", ")}`}`;
221929
+ }
221930
+ }
221931
+ if (error)
221932
+ ok = false;
221933
+ steps.push({ desc: st.desc, ok: !error, error, skipped });
221934
+ }
221935
+ return { loaded, ended, ok: ok && loaded, steps };
221936
+ };
221937
+
221938
+ // src/commands/render.ts
221487
221939
  init_util();
221488
221940
  var DEVICE_SAFE_AREA = {
221489
221941
  none: { insets: [0, 0, 0, 0], bars: [] },
@@ -221514,7 +221966,7 @@ var requireRenderer = async () => {
221514
221966
  };
221515
221967
  var fileScenarioIO = (dir) => ({
221516
221968
  write: (relPath, data) => {
221517
- const abs = resolve11(dir, relPath);
221969
+ const abs = resolve12(dir, relPath);
221518
221970
  mkdirSync18(dirname15(abs), { recursive: true });
221519
221971
  writeFileSync19(abs, data);
221520
221972
  }
@@ -221524,7 +221976,7 @@ var resolveFixtures = (value, baseDir) => {
221524
221976
  return;
221525
221977
  let map = value;
221526
221978
  if (typeof value === "string") {
221527
- const abs = resolve11(baseDir, value);
221979
+ const abs = resolve12(baseDir, value);
221528
221980
  try {
221529
221981
  map = JSON.parse(readFileSync22(abs, "utf8"));
221530
221982
  } catch (e2) {
@@ -221539,7 +221991,7 @@ var resolveFixtures = (value, baseDir) => {
221539
221991
  var readScenarioFile = (path2) => {
221540
221992
  let raw;
221541
221993
  try {
221542
- raw = JSON.parse(readFileSync22(resolve11(path2), "utf8"));
221994
+ raw = JSON.parse(readFileSync22(resolve12(path2), "utf8"));
221543
221995
  } catch (e2) {
221544
221996
  throw new CliError(`Can't read scenario "${path2}": ${e2 instanceof Error ? e2.message : String(e2)}`);
221545
221997
  }
@@ -221552,6 +222004,20 @@ var readScenarioFile = (path2) => {
221552
222004
  throw new CliError(`Scenario "${path2}" has no "steps".`);
221553
222005
  return file;
221554
222006
  };
222007
+ var parseClip = (raw) => {
222008
+ if (raw === undefined)
222009
+ return;
222010
+ const parts = raw.split(",").map((p2) => p2.trim());
222011
+ if (parts.length === 4 && parts.every((p2) => /^-?\d+(\.\d+)?$/.test(p2))) {
222012
+ const [x2, y2, width, height] = parts.map(Number);
222013
+ if (width <= 0 || height <= 0)
222014
+ throw new CliError(`--clip: width and height must be positive (got "${raw}").`);
222015
+ return { x: x2, y: y2, width, height };
222016
+ }
222017
+ if (parts.length === 1 && raw.trim() !== "")
222018
+ return raw.trim();
222019
+ throw new CliError(`--clip: expected a node name or "x,y,width,height" (got "${raw}").`);
222020
+ };
221555
222021
  var renderDesktop = async (args) => {
221556
222022
  let root;
221557
222023
  try {
@@ -221560,22 +222026,101 @@ var renderDesktop = async (args) => {
221560
222026
  root = process.cwd();
221561
222027
  }
221562
222028
  const win = readAppConfigAt(root)?.desktop?.window;
221563
- const width = Number(flagStr(args, "width")) || win?.width || 390;
221564
- const height = Number(flagStr(args, "height")) || win?.height || 844;
222029
+ const scriptFlag = flagStr(args, "script");
222030
+ const scenario = scriptFlag !== undefined ? readScenarioFile(scriptFlag) : null;
222031
+ const width = Number(flagStr(args, "width")) || scenario?.width || win?.width || 390;
222032
+ const height = Number(flagStr(args, "height")) || scenario?.height || win?.height || 844;
221565
222033
  const frames = Number(flagStr(args, "frames")) || 60;
221566
- const timeoutMs = Number(flagStr(args, "timeout")) || 60000;
221567
222034
  const pngFlag = flagStr(args, "png") ?? "screenshot.png";
221568
- const { js, resources } = await buildBundle(args);
222035
+ const logs = flagBool(args, "logs");
222036
+ const fixedDtFlag = flagStr(args, "fixed-dt");
222037
+ const fixedDtMs = fixedDtFlag !== undefined ? Number(fixedDtFlag) : scenario ? 16 : 0;
222038
+ if (!Number.isFinite(fixedDtMs) || fixedDtMs < 0 || fixedDtMs > 1000)
222039
+ throw new CliError(`--fixed-dt wants a number of ms (0 = wall clock, max 1000), got "${fixedDtFlag}".`);
222040
+ const { camera, view } = resolveInspectionCamera(args);
222041
+ if ((camera || view) && scenario)
222042
+ throw new CliError('--camera/--view apply to a plain render; in a --script run use a { "view": "iso" } or { "camera": { "at", "lookAt" } } step before the screenshot.');
222043
+ const { js, resources } = await buildBundle(args, undefined, { localAssets: true });
222044
+ mkdirSync18(dirname15(resolve12(pngFlag)), { recursive: true });
222045
+ if (scenario) {
222046
+ const outDir = flagStr(args, "out-dir") ?? ".";
222047
+ const plan = translateScenario(scenario.steps, { outDir, headroomFrames: frames });
222048
+ for (const st of plan.steps)
222049
+ if (st.screenshot)
222050
+ mkdirSync18(dirname15(st.screenshot), { recursive: true });
222051
+ const timeoutMs2 = Number(flagStr(args, "timeout")) || 60000 + plan.steps.length * 5000;
222052
+ note(`Running ${plan.steps.length} step${plan.steps.length === 1 ? "" : "s"} (native)…`);
222053
+ const { output } = await runDesktopRender(js, resources, {
222054
+ width,
222055
+ height,
222056
+ frames: plan.frameCap,
222057
+ outPng: pngFlag,
222058
+ logs,
222059
+ timeoutMs: timeoutMs2,
222060
+ script: plan.text,
222061
+ fixedDtMs
222062
+ });
222063
+ const result = evaluateDesktopRun(plan, output);
222064
+ if (!result.loaded) {
222065
+ throw new CliError("This desktop host doesn't support scripted input (CREATOR_SCRIPT) — it ran the frames and ignored the steps. Update it: lecodes desktop update (or point LECODES_DESKTOP_EXE at a newer build).");
222066
+ }
222067
+ for (const st of result.steps) {
222068
+ logErr(st.ok ? `${c.green("✓")} ${st.desc}` : `${c.red("✗")} ${st.desc}
222069
+ ${c.red(st.error ?? "failed")}`);
222070
+ for (const sk of st.skipped)
222071
+ logErr(c.dim(` – skipped: ${sk}`));
222072
+ }
222073
+ const skippedCount = result.steps.reduce((n2, s2) => n2 + s2.skipped.length, 0);
222074
+ if (skippedCount > 0)
222075
+ logErr(c.dim(` ${skippedCount} semantic check${skippedCount === 1 ? "" : "s"} skipped — the native host has no UI tree; run the same scenario headless to verify them.`));
222076
+ if (!result.ended)
222077
+ warnErr("The host stopped before the script finished (frame cap / crash) — later steps are reported as incomplete.");
222078
+ logErr(result.ok ? c.green(`✓ ${result.steps.length}/${result.steps.length} steps passed (native)`) : c.red(`✗ ${result.steps.filter((s2) => !s2.ok).length} of ${result.steps.length} steps failed (native)`));
222079
+ note(`Wrote ${pngFlag} (${width}x${height}, native render, final frame)`);
222080
+ process.exit(result.ok ? 0 : 1);
222081
+ }
222082
+ const timeoutMs = Number(flagStr(args, "timeout")) || 60000;
221569
222083
  note("Rendering (native)…");
221570
222084
  await runDesktopRender(js, resources, {
221571
222085
  width,
221572
222086
  height,
221573
222087
  frames,
221574
222088
  outPng: pngFlag,
221575
- logs: flagBool(args, "logs"),
221576
- timeoutMs
222089
+ logs,
222090
+ timeoutMs,
222091
+ fixedDtMs,
222092
+ camera,
222093
+ view
221577
222094
  });
221578
- note(`Wrote ${pngFlag} (${width}x${height}, native render)`);
222095
+ note(`Wrote ${pngFlag} (${width}x${height}, native render${view ? `, ${view.split(";")[0]} view` : camera ? ", custom camera" : ""})`);
222096
+ };
222097
+ var VIEW_NAMES2 = ["iso", "top", "front", "side", "back"];
222098
+ var parseVec3Flag = (name, raw) => {
222099
+ const parts = raw.split(",").map((p2) => p2.trim());
222100
+ if (parts.length !== 3 || parts.some((p2) => !/^-?\d+(\.\d+)?$/.test(p2)))
222101
+ throw new CliError(`--${name} wants "x,y,z" (got "${raw}").`);
222102
+ return parts.join(",");
222103
+ };
222104
+ var resolveInspectionCamera = (args) => {
222105
+ const cam = flagStr(args, "camera"), look = flagStr(args, "look-at"), view = flagStr(args, "view"), fov = flagStr(args, "fov");
222106
+ if (fov !== undefined && !(Number(fov) > 0 && Number(fov) < 180))
222107
+ throw new CliError(`--fov wants degrees in (0, 180), got "${fov}".`);
222108
+ const fovSuffix = fov !== undefined ? `;${Number(fov)}` : "";
222109
+ if (cam !== undefined || look !== undefined) {
222110
+ if (view !== undefined)
222111
+ throw new CliError("Use either --camera/--look-at or --view, not both.");
222112
+ if (cam === undefined || look === undefined)
222113
+ throw new CliError("--camera and --look-at go together: --camera x,y,z --look-at x,y,z");
222114
+ return { camera: `${parseVec3Flag("camera", cam)};${parseVec3Flag("look-at", look)}${fovSuffix}` };
222115
+ }
222116
+ if (view !== undefined) {
222117
+ if (!VIEW_NAMES2.includes(view))
222118
+ throw new CliError(`--view wants one of ${VIEW_NAMES2.join(", ")} (got "${view}").`);
222119
+ return { view: `${view}${fovSuffix}` };
222120
+ }
222121
+ if (fov !== undefined)
222122
+ throw new CliError("--fov goes with --camera/--look-at or --view.");
222123
+ return {};
221579
222124
  };
221580
222125
  var render = async (args) => {
221581
222126
  if (flagBool(args, "desktop"))
@@ -221597,7 +222142,7 @@ var render = async (args) => {
221597
222142
  const settleStr = flagStr(args, "settle") ?? (scenario?.settle !== undefined ? String(scenario.settle) : undefined);
221598
222143
  const timeStr = flagStr(args, "time");
221599
222144
  const onConsole = flagBool(args, "logs") ? (level, text2) => logErr(`${c.dim(`[${level}]`)} ${text2}`) : undefined;
221600
- const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname15(resolve11(scriptFlag)) : ".");
222145
+ const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname15(resolve12(scriptFlag)) : ".");
221601
222146
  const flagFixtures = resolveFixtures(flagStr(args, "fixtures"), ".");
221602
222147
  const fixtures = envelopeFixtures || flagFixtures ? { ...envelopeFixtures, ...flagFixtures } : undefined;
221603
222148
  if (fixtures) {
@@ -221636,7 +222181,7 @@ var render = async (args) => {
221636
222181
  ${c.red(st.error ?? "failed")}`);
221637
222182
  }
221638
222183
  if (!result2.ok && result2.failureArtifacts.length > 0) {
221639
- logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p2) => resolve11(outDir, p2)).join(", ")}`));
222184
+ logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p2) => resolve12(outDir, p2)).join(", ")}`));
221640
222185
  }
221641
222186
  logErr(result2.ok ? c.green(`✓ ${result2.steps.length}/${steps.length} steps passed`) : c.red(`✗ failed at step ${result2.steps.length} of ${steps.length}`));
221642
222187
  return done((cb) => cb(), result2.ok ? 0 : 1);
@@ -221647,19 +222192,21 @@ var render = async (args) => {
221647
222192
  const pngFlag = flagStr(args, "png") ?? (flagBool(args, "png") ? "screenshot.png" : undefined);
221648
222193
  if (pngFlag !== undefined) {
221649
222194
  note("Rendering PNG…");
221650
- const { png: png2, warnings: pngWarnings } = await renderer.renderToPng(js, {
222195
+ const clip = parseClip(flagStr(args, "clip"));
222196
+ const { png: png2, width: outW, height: outH, warnings: pngWarnings } = await renderer.renderToPng(js, {
221651
222197
  width,
221652
222198
  height,
221653
222199
  scale: Number(flagStr(args, "scale")) || 2,
221654
- ...timing
222200
+ ...timing,
222201
+ clip
221655
222202
  });
221656
222203
  for (const w of [...warnings, ...pngWarnings])
221657
222204
  warnErr(w);
221658
222205
  if (isNullSink(pngFlag)) {
221659
222206
  note(`Discarded PNG (${pngFlag})`);
221660
222207
  } else {
221661
- writeFileSync19(resolve11(pngFlag), png2);
221662
- note(`Wrote ${pngFlag} (${png2.length} bytes, ${width}x${height})`);
222208
+ writeFileSync19(resolve12(pngFlag), png2);
222209
+ note(`Wrote ${pngFlag} (${png2.length} bytes, ${outW}x${outH}${clip !== undefined ? ", clipped" : ""})`);
221663
222210
  }
221664
222211
  return done((cb) => cb());
221665
222212
  }
@@ -221673,7 +222220,7 @@ var render = async (args) => {
221673
222220
  return done((cb) => cb());
221674
222221
  }
221675
222222
  if (out) {
221676
- writeFileSync19(resolve11(out), result.text);
222223
+ writeFileSync19(resolve12(out), result.text);
221677
222224
  note(`Wrote ${out} (${result.text.length} bytes)`);
221678
222225
  return done((cb) => cb());
221679
222226
  }
@@ -222034,7 +222581,7 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
222034
222581
  // src/commands/scene.ts
222035
222582
  init_manifest();
222036
222583
  import { existsSync as existsSync27, readFileSync as readFileSync25, readdirSync as readdirSync12, statSync as statSync8, writeFileSync as writeFileSync22 } from "node:fs";
222037
- import { basename as basename14, dirname as dirname17, join as join27, resolve as resolve12 } from "node:path";
222584
+ import { basename as basename14, dirname as dirname17, join as join27, resolve as resolve13 } from "node:path";
222038
222585
 
222039
222586
  // src/compile/sceneCompile.ts
222040
222587
  init_localFiles();
@@ -222107,7 +222654,7 @@ var getContext2 = (args) => {
222107
222654
  manifest = null;
222108
222655
  const pathArg = args._[0];
222109
222656
  if (pathArg) {
222110
- const abs = resolve12(process.cwd(), pathArg);
222657
+ const abs = resolve13(process.cwd(), pathArg);
222111
222658
  if (existsSync27(abs) && statSync8(abs).isDirectory())
222112
222659
  root = abs;
222113
222660
  else if (abs.endsWith(".scene.ts") && existsSync27(abs))
@@ -222235,8 +222782,9 @@ var status2 = async (_args) => {
222235
222782
  };
222236
222783
 
222237
222784
  // src/commands/test.ts
222238
- import { existsSync as existsSync28, readFileSync as readFileSync26, readdirSync as readdirSync13 } from "node:fs";
222239
- import { dirname as dirname18, resolve as resolve13 } from "node:path";
222785
+ import { existsSync as existsSync28, mkdirSync as mkdirSync20, readFileSync as readFileSync26, readdirSync as readdirSync13 } from "node:fs";
222786
+ import { dirname as dirname18, resolve as resolve14 } from "node:path";
222787
+ init_manifest();
222240
222788
  init_util();
222241
222789
  var deriveDesignFlows = (meta, entryScreen) => {
222242
222790
  const edges = meta.edges ?? [];
@@ -222321,17 +222869,39 @@ var readFlowFile = (abs, displayName) => {
222321
222869
  };
222322
222870
  };
222323
222871
  var test = async (args) => {
222324
- const { js, warnings, useLocalAssets, root } = await compileHeadlessBundle(args);
222325
- const renderer = await requireRenderer();
222872
+ const desktop2 = flagBool(args, "desktop");
222873
+ if (desktop2 && flagBool(args, "flows"))
222874
+ throw new CliError("--flows needs the headless renderer (design flows tap by name) — drop --desktop.");
222875
+ const native = desktop2 ? await buildBundle(args, undefined, { localAssets: true }) : null;
222876
+ const headless = desktop2 ? null : await compileHeadlessBundle(args);
222877
+ const warnings = headless?.warnings ?? [];
222878
+ const useLocalAssets = headless?.useLocalAssets ?? false;
222879
+ const js = headless?.js ?? "";
222880
+ let root;
222881
+ if (headless)
222882
+ root = headless.root;
222883
+ else {
222884
+ try {
222885
+ root = findProjectRoot(process.cwd());
222886
+ } catch {
222887
+ root = process.cwd();
222888
+ }
222889
+ }
222890
+ const renderer = desktop2 ? null : await requireRenderer();
222891
+ const nativeFrames = Number(flagStr(args, "frames")) || 60;
222892
+ const fixedDtFlag = flagStr(args, "fixed-dt");
222893
+ const nativeFixedDt = fixedDtFlag !== undefined ? Number(fixedDtFlag) : 16;
222894
+ if (!Number.isFinite(nativeFixedDt) || nativeFixedDt < 0 || nativeFixedDt > 1000)
222895
+ throw new CliError(`--fixed-dt wants a number of ms (0 = wall clock, max 1000), got "${fixedDtFlag}".`);
222326
222896
  const projectViewport = desktopDefaultViewport(root);
222327
- const testsDir = resolve13(root, flagStr(args, "dir") ?? "tests");
222328
- const designMetaPath = resolve13(root, flagStr(args, "design-dir") ?? "design", "meta.json");
222897
+ const testsDir = resolve14(root, flagStr(args, "dir") ?? "tests");
222898
+ const designMetaPath = resolve14(root, flagStr(args, "design-dir") ?? "design", "meta.json");
222329
222899
  const asJson = flagBool(args, "json");
222330
222900
  const out = asJson ? logErr : log;
222331
222901
  const cases = [];
222332
222902
  if (args._.length > 0) {
222333
222903
  for (const given of args._) {
222334
- const candidates = [resolve13(given), resolve13(testsDir, given), resolve13(testsDir, `${given}.flow.json`)];
222904
+ const candidates = [resolve14(given), resolve14(testsDir, given), resolve14(testsDir, `${given}.flow.json`)];
222335
222905
  const abs = candidates.find((p2) => existsSync28(p2));
222336
222906
  if (!abs)
222337
222907
  throw new CliError(`No such test: "${given}" (looked in ${testsDir}).`);
@@ -222339,11 +222909,11 @@ var test = async (args) => {
222339
222909
  }
222340
222910
  } else if (existsSync28(testsDir)) {
222341
222911
  for (const f3 of readdirSync13(testsDir).filter((f4) => f4.endsWith(".flow.json")).sort()) {
222342
- cases.push(readFlowFile(resolve13(testsDir, f3), f3));
222912
+ cases.push(readFlowFile(resolve14(testsDir, f3), f3));
222343
222913
  }
222344
222914
  }
222345
222915
  const fixturesFlag = flagStr(args, "fixtures");
222346
- const defaultPack = resolve13(testsDir, "fixtures.json");
222916
+ const defaultPack = resolve14(testsDir, "fixtures.json");
222347
222917
  const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") : existsSync28(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
222348
222918
  const flowsOnly = flagBool(args, "flows");
222349
222919
  const wantFlows = !flagBool(args, "no-flows") && args._.length === 0;
@@ -222351,7 +222921,10 @@ var test = async (args) => {
222351
222921
  cases.length = 0;
222352
222922
  let skipped = [];
222353
222923
  let entryScreen = null;
222354
- if ((wantFlows || flowsOnly) && existsSync28(designMetaPath)) {
222924
+ if (desktop2 && wantFlows && existsSync28(designMetaPath)) {
222925
+ note("Design-derived flows skipped with --desktop (they tap by name; run `lecodes test` headless for them).");
222926
+ }
222927
+ if (!desktop2 && (wantFlows || flowsOnly) && existsSync28(designMetaPath)) {
222355
222928
  let meta;
222356
222929
  try {
222357
222930
  meta = JSON.parse(readFileSync26(designMetaPath, "utf8"));
@@ -222379,18 +222952,60 @@ var test = async (args) => {
222379
222952
  warnErr(w);
222380
222953
  throw new CliError(`No tests found. Write tests/*.flow.json scenarios, or draw activator edges in the design board (design/meta.json).`);
222381
222954
  }
222382
- const timeoutMs = Number(flagStr(args, "timeout")) || 20000 + cases.length * 1e4;
222955
+ const timeoutMs = Number(flagStr(args, "timeout")) || (desktop2 ? 60000 + cases.length * 60000 : 20000 + cases.length * 1e4);
222383
222956
  const killer = setTimeout(() => {
222384
222957
  warnErr(`Test run timed out after ${timeoutMs}ms (raise --timeout or lower --settle).`);
222385
222958
  process.exit(1);
222386
222959
  }, timeoutMs);
222387
222960
  killer.unref?.();
222388
222961
  const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
222389
- const artifactsRoot = resolve13(root, flagStr(args, "artifacts") ?? resolve13(testsDir, ".artifacts"));
222962
+ const artifactsRoot = resolve14(root, flagStr(args, "artifacts") ?? resolve14(testsDir, ".artifacts"));
222390
222963
  const results = [];
222964
+ const runNativeCase = async (kase) => {
222965
+ const caseDir = resolve14(artifactsRoot, slug3(kase.name));
222966
+ const plan = translateScenario(kase.steps, { outDir: caseDir, headroomFrames: nativeFrames });
222967
+ for (const st of plan.steps)
222968
+ if (st.screenshot)
222969
+ mkdirSync20(dirname18(st.screenshot), { recursive: true });
222970
+ mkdirSync20(caseDir, { recursive: true });
222971
+ const finalPng = resolve14(caseDir, "final.png");
222972
+ const { output } = await runDesktopRender(native.js, native.resources, {
222973
+ width: Number(flagStr(args, "width")) || kase.width || projectViewport?.width || 390,
222974
+ height: Number(flagStr(args, "height")) || kase.height || projectViewport?.height || 844,
222975
+ frames: plan.frameCap,
222976
+ outPng: finalPng,
222977
+ logs: flagBool(args, "logs"),
222978
+ timeoutMs: 60000 + plan.steps.length * 5000,
222979
+ script: plan.text,
222980
+ fixedDtMs: nativeFixedDt
222981
+ });
222982
+ const r2 = evaluateDesktopRun(plan, output);
222983
+ if (!r2.loaded)
222984
+ throw new CliError("This desktop host doesn't support scripted input (CREATOR_SCRIPT). Update it: lecodes desktop update (or point LECODES_DESKTOP_EXE at a newer build).");
222985
+ return {
222986
+ name: kase.name,
222987
+ kind: kase.kind,
222988
+ ok: r2.ok,
222989
+ steps: r2.steps.map((s2) => ({ desc: s2.desc, ok: s2.ok, error: s2.error, skipped: s2.skipped.length > 0 ? s2.skipped : undefined })),
222990
+ error: r2.ended ? undefined : "the host stopped before the script finished (frame cap / crash)",
222991
+ artifacts: r2.ok ? [] : [finalPng]
222992
+ };
222993
+ };
222391
222994
  for (const kase of cases) {
222392
222995
  const flagSettle = flagStr(args, "settle");
222393
222996
  let result;
222997
+ if (desktop2) {
222998
+ try {
222999
+ result = await runNativeCase(kase);
223000
+ } catch (e2) {
223001
+ if (e2 instanceof CliError && /doesn't support scripted input/.test(e2.message))
223002
+ throw e2;
223003
+ result = { name: kase.name, kind: kase.kind, ok: false, steps: [], error: e2 instanceof Error ? e2.message : String(e2), artifacts: [] };
223004
+ }
223005
+ results.push(result);
223006
+ reportCase(result);
223007
+ continue;
223008
+ }
222394
223009
  try {
222395
223010
  const steps = renderer.parseScenarioSteps(kase.steps);
222396
223011
  const session = await renderer.createSession(js, {
@@ -222404,14 +223019,14 @@ var test = async (args) => {
222404
223019
  onConsole
222405
223020
  });
222406
223021
  try {
222407
- const io = fileScenarioIO(resolve13(artifactsRoot, slug3(kase.name)));
223022
+ const io = fileScenarioIO(resolve14(artifactsRoot, slug3(kase.name)));
222408
223023
  const outcome = await renderer.runScenario(session, steps, io);
222409
223024
  result = {
222410
223025
  name: kase.name,
222411
223026
  kind: kase.kind,
222412
223027
  ok: outcome.ok,
222413
223028
  steps: outcome.steps.map((s2) => ({ desc: s2.desc, ok: s2.ok, error: s2.error })),
222414
- artifacts: outcome.failureArtifacts.map((p2) => resolve13(artifactsRoot, slug3(kase.name), p2))
223029
+ artifacts: outcome.failureArtifacts.map((p2) => resolve14(artifactsRoot, slug3(kase.name), p2))
222415
223030
  };
222416
223031
  } finally {
222417
223032
  session.dispose();
@@ -222420,14 +223035,20 @@ var test = async (args) => {
222420
223035
  result = { name: kase.name, kind: kase.kind, ok: false, steps: [], error: e2 instanceof Error ? e2.message : String(e2), artifacts: [] };
222421
223036
  }
222422
223037
  results.push(result);
223038
+ reportCase(result);
223039
+ }
223040
+ function reportCase(result) {
223041
+ const skippedCount = result.steps.reduce((n2, s2) => n2 + (s2.skipped?.length ?? 0), 0);
222423
223042
  if (result.ok) {
222424
- out(`${c.green("✓")} ${result.name}`);
223043
+ out(`${c.green("✓")} ${result.name}${skippedCount > 0 ? c.dim(` (${skippedCount} semantic check${skippedCount === 1 ? "" : "s"} skipped on --desktop)`) : ""}`);
222425
223044
  } else {
222426
223045
  out(`${c.red("✗")} ${result.name}`);
222427
223046
  if (result.error)
222428
223047
  out(` ${c.red(result.error)}`);
222429
223048
  for (const s2 of result.steps) {
222430
223049
  out(s2.ok ? c.dim(` ✓ ${s2.desc}`) : ` ${c.red(`✗ ${s2.desc} — ${s2.error}`)}`);
223050
+ for (const sk of s2.skipped ?? [])
223051
+ out(c.dim(` – skipped: ${sk}`));
222431
223052
  }
222432
223053
  if (result.artifacts.length > 0)
222433
223054
  out(c.dim(` failing frame: ${result.artifacts.join(", ")}`));
@@ -222473,11 +223094,11 @@ init_config();
222473
223094
  init_manifest();
222474
223095
  init_types4();
222475
223096
  init_util();
222476
- import { resolve as resolve14 } from "node:path";
223097
+ import { resolve as resolve15 } from "node:path";
222477
223098
  var label = (bundle) => `${Object.keys(bundle).length} file(s)`;
222478
223099
  var types4 = async (args) => {
222479
223100
  const dirArg = args._[0];
222480
- const root = dirArg ? resolve14(dirArg) : (() => {
223101
+ const root = dirArg ? resolve15(dirArg) : (() => {
222481
223102
  try {
222482
223103
  return findProjectRoot(process.cwd());
222483
223104
  } catch {
@@ -222543,16 +223164,16 @@ import { delimiter as delimiter2, dirname as dirname20, join as join29 } from "n
222543
223164
  // src/commands/install.ts
222544
223165
  init_util();
222545
223166
  import { execFileSync as execFileSync5 } from "node:child_process";
222546
- import { appendFileSync as appendFileSync3, chmodSync as chmodSync2, cpSync as cpSync4, existsSync as existsSync29, mkdirSync as mkdirSync20, readFileSync as readFileSync27, realpathSync, rmSync as rmSync7, symlinkSync } from "node:fs";
223167
+ import { appendFileSync as appendFileSync3, chmodSync as chmodSync2, cpSync as cpSync4, existsSync as existsSync29, mkdirSync as mkdirSync21, readFileSync as readFileSync27, realpathSync, rmSync as rmSync7, symlinkSync } from "node:fs";
222547
223168
  import { homedir as homedir7 } from "node:os";
222548
- import { basename as basename15, delimiter, dirname as dirname19, join as join28, resolve as resolve15, sep as sep2 } from "node:path";
223169
+ import { basename as basename15, delimiter, dirname as dirname19, join as join28, resolve as resolve16, sep as sep2 } from "node:path";
222549
223170
  var isWin = process.platform === "win32";
222550
223171
  var HOME = homedir7();
222551
223172
  var safeRealpath = (p2) => {
222552
223173
  try {
222553
223174
  return realpathSync(p2);
222554
223175
  } catch {
222555
- return resolve15(p2);
223176
+ return resolve16(p2);
222556
223177
  }
222557
223178
  };
222558
223179
  var onPath = (dir) => {
@@ -222580,7 +223201,7 @@ var install = async (args) => {
222580
223201
  return installUnix(args, bundle, companions);
222581
223202
  };
222582
223203
  var copyBundle = (bundle, target, companions) => {
222583
- mkdirSync20(target, { recursive: true });
223204
+ mkdirSync21(target, { recursive: true });
222584
223205
  if (safeRealpath(target) === safeRealpath(bundle))
222585
223206
  return;
222586
223207
  for (const f3 of companions) {
@@ -222598,8 +223219,8 @@ var permError = (e2, dir, system) => {
222598
223219
  };
222599
223220
  var installUnix = (args, bundle, companions) => {
222600
223221
  const system = flagBool(args, "system");
222601
- const libDir = resolve15(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join28(HOME, ".local", "lib", "lecodes-cli")));
222602
- const binDir = resolve15(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join28(HOME, ".local", "bin")));
223222
+ const libDir = resolve16(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join28(HOME, ".local", "lib", "lecodes-cli")));
223223
+ const binDir = resolve16(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join28(HOME, ".local", "bin")));
222603
223224
  const link2 = join28(binDir, "lecodes");
222604
223225
  try {
222605
223226
  copyBundle(bundle, libDir, companions);
@@ -222609,7 +223230,7 @@ var installUnix = (args, bundle, companions) => {
222609
223230
  throw permError(e2, libDir, system);
222610
223231
  }
222611
223232
  try {
222612
- mkdirSync20(binDir, { recursive: true });
223233
+ mkdirSync21(binDir, { recursive: true });
222613
223234
  rmSync7(link2, { force: true });
222614
223235
  symlinkSync(join28(libDir, "lecodes"), link2);
222615
223236
  } catch (e2) {
@@ -222670,7 +223291,7 @@ var finishPathUnix = (args, binDir) => {
222670
223291
  log(` • or install system-wide (no PATH edit): ${c.bold("sudo lecodes install --system")}`);
222671
223292
  };
222672
223293
  var installWindows = (args, bundle, companions) => {
222673
- const target = resolve15(flagStr(args, "dir") ?? join28(process.env.LOCALAPPDATA ?? join28(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
223294
+ const target = resolve16(flagStr(args, "dir") ?? join28(process.env.LOCALAPPDATA ?? join28(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
222674
223295
  try {
222675
223296
  copyBundle(bundle, target, companions);
222676
223297
  } catch (e2) {
@@ -222811,8 +223432,12 @@ ${c.bold("Commands:")}
222811
223432
  --png [file] rasterize a PNG (default screenshot.png;
222812
223433
  needs @napi-rs/canvas)
222813
223434
  --script <file> drive the app through a step list (tap/type/
222814
- scroll/wait/expect/screenshot/json steps)
223435
+ scroll/wait/expect/screenshot/json steps;
223436
+ games: frames/drag/hold/key/keyDown/keyUp,
223437
+ expect.log, screenshot clip)
222815
223438
  --out-dir <dir> where --script step outputs land (default .)
223439
+ --clip <name|x,y,w,h> (--png) crop to one node's rect or a
223440
+ logical-px rect — a component/region render
222816
223441
  --fixtures <file> canned fetch responses (URL pattern →
222817
223442
  { status?, json?, text? }) — make
222818
223443
  backend-gated screens reachable headless
@@ -222830,6 +223455,22 @@ ${c.bold("Commands:")}
222830
223455
  LECODES_DESKTOP_EXE overrides)
222831
223456
  --frames <n> (--desktop) frames to run before the shot —
222832
223457
  headroom for async loads (default 60)
223458
+ --desktop --script <file> drive the scenario through the native
223459
+ host: tap/drag/hold take { x, y } coordinates
223460
+ (no names — the host has no UI tree; type/
223461
+ scroll/clip unavailable), expect.log is
223462
+ checked, text/node/screen/value are skipped;
223463
+ per-step screenshots + final --png
223464
+ --view <name> (--desktop) inspection camera auto-fitted to the
223465
+ whole 3D scene: iso | top | front | side | back
223466
+ [--fov N]; the app's camera is untouched
223467
+ --camera x,y,z --look-at x,y,z [--fov N]
223468
+ (--desktop) inspection camera at a fixed eye /
223469
+ look-at point (world units, Y up). In a
223470
+ --script run use { "view" } / { "camera" } steps
223471
+ --fixed-dt <ms> (--desktop) virtual app clock, ms per frame —
223472
+ deterministic game time (scripted runs
223473
+ default to 16; plain renders to wall clock)
222833
223474
  test [name…] Run headless flow tests: tests/*.flow.json scenarios, plus flows
222834
223475
  derived from the design graph (design/meta.json activator edges
222835
223476
  — checks the built app against the designed navigation; needs
@@ -222846,6 +223487,11 @@ ${c.bold("Commands:")}
222846
223487
  --dir <folder> tests folder (default tests/)
222847
223488
  --design-dir <dir> design folder (default design/)
222848
223489
  --artifacts <dir> failure-artifact folder override
223490
+ --desktop run the file scenarios through the NATIVE host
223491
+ (coordinates only; expect.log checked,
223492
+ text/node/screen/value skipped; design flows
223493
+ stay headless). --frames / --fixed-dt as in
223494
+ render --desktop
222849
223495
  (+ the render timing flags: --width/--height, --settle,
222850
223496
  --device, --safe-area, --wait-network, --logs, --entry)
222851
223497
  types [dir] Refresh the IDE types (.lecodes/types/ + tsconfig.json) — by
@@ -222884,7 +223530,9 @@ ${c.bold("Commands:")}
222884
223530
  app.json fullscreen; --scale <0.5..4|fit> overrides
222885
223531
  the display scale — "fit" sizes a 1080p-designed app
222886
223532
  to the monitor with the density raised to match;
222887
- --entry overrides the entrypoint)
223533
+ --entry overrides the entrypoint; asset() resources
223534
+ load from the working tree — --remote-assets or
223535
+ --public-url <url> uses the server's copies instead)
222888
223536
  update move to the newest published release
222889
223537
  (cached in ~/.lecodes/renderer)
222890
223538
  scene [path] LeCodes Scene Editor — visually edit a .scene.ts (tree, inspector,
@@ -222898,7 +223546,22 @@ ${c.bold("Commands:")}
222898
223546
  FBX → LeCodes-ready GLB: meters/Y-up, textures embedded, clips from
222899
223547
  other FBX files merged by bone name (Mixamo: one FBX per animation);
222900
223548
  --fix/--ktx2 run the doctor passes (needs 'lecodes-assets')
222901
- assets probe <file.fbx|.glb> Skeleton, clips, meshes, materials, textures, bind sanity
223549
+ Unity .mat files next to the FBX are read for the real textures
223550
+ (metallic/roughness/ORM/emission packed the glTF way); --no-unity
223551
+ keeps the FBX references; --unity-index <unity-index.txt> resolves GUIDs
223552
+ --list-nodes / --keep <node> … / --split [dir]: carve one variant out of a
223553
+ multi-variant prop file, re-origined on the ground (or one GLB per node)
223554
+ --merge-skins b.fbx …: modular character pieces onto ONE skeleton
223555
+ assets unpack <pkg.unitypackage> [-o dir] [--only re …] [--list]
223556
+ Stream a .unitypackage to a folder: writes unity-index.txt (GUID → path,
223557
+ what convert resolves textures with) and the matching assets + .meta
223558
+ assets scene <map.unity> [-o level.json] [--filter re] [--group g] [--region x0,z0,x1,z1]
223559
+ Unity scene → placed prefabs / mesh objects with world TRS in LeCodes'
223560
+ frame + a prefab table (FBX + --keep node per prefab)
223561
+ assets probe <file.fbx|.glb> [--filmstrip [out.png]]
223562
+ Skeleton, clips (+ root motion / first-frame-vs-rest per clip), meshes,
223563
+ materials, textures, bind sanity, duplicate names; --filmstrip renders
223564
+ a CPU-skinned PNG strip per clip (no GPU)
222902
223565
  assets doctor <in.glb|dir> [--fix] [--ktx2] [--dump]
222903
223566
  Analyze / fix a GLB against engine limits (bone cap 256, texture
222904
223567
  formats/size, KTX2 via toktx)