lecodes-cli 0.19.2 → 0.20.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.
Files changed (59) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +1324 -658
  3. package/package.json +9 -9
  4. package/runtime/materials/decal-relief.mat +178 -0
  5. package/runtime/materials/decal.mat +172 -0
  6. package/runtime/materials/lightmap-baked-lite.mat +175 -0
  7. package/runtime/materials/lightmap-baked.mat +176 -0
  8. package/runtime/materials/lightmap.mat +181 -0
  9. package/runtime/materials/lit.mat +43 -0
  10. package/runtime/materials/particles-quad.mat +171 -0
  11. package/runtime/materials/particles.mat +121 -0
  12. package/runtime/materials/shadow.mat +21 -0
  13. package/runtime/materials/terrain.mat +134 -0
  14. package/runtime/materials/unlit-transparent.mat +42 -0
  15. package/runtime/materials/unlit.mat +44 -0
  16. package/runtime/materials/video.mat +31 -0
  17. package/runtime/sdk-types.json +1 -1
  18. package/runtime/web/assets/Roboto-Variable-DHm9jpd-.woff2 +0 -0
  19. package/runtime/web/assets/__vite-browser-external-BIHI7g3E.js +1 -0
  20. package/runtime/web/assets/basis-C64VHDVD.js +1 -0
  21. package/runtime/web/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
  22. package/runtime/web/assets/createViewerLite-C_bkKXLg.js +852 -0
  23. package/runtime/web/assets/draco-BiISTFcR.js +118 -0
  24. package/runtime/web/assets/draco_decoder-DsQ12WqX.wasm +0 -0
  25. package/runtime/web/assets/index-BTB5KV9x.js +2 -0
  26. package/runtime/web/assets/mapViewImpl-B2JcES8l.js +810 -0
  27. package/runtime/web/assets/maplibre-gl-worker-CJfwIrte.js +8 -0
  28. package/runtime/web/assets/neutral_ibl128-D-lzdkLN.ktx +0 -0
  29. package/runtime/web/assets/worker-Caf-yYEI.js +2 -0
  30. package/runtime/web/embed.html +32 -0
  31. package/runtime/web/embed.js +171 -0
  32. package/src/cmgenTool.ts +17 -9
  33. package/src/commands/app.ts +6 -1
  34. package/src/commands/appDesktop.ts +27 -4
  35. package/src/commands/appShared.ts +145 -3
  36. package/src/commands/appTemplates.ts +28 -4
  37. package/src/commands/assets.ts +3 -7
  38. package/src/commands/design.ts +9 -26
  39. package/src/commands/desktop.ts +9 -1
  40. package/src/commands/dev.ts +64 -0
  41. package/src/commands/projectTemplates.ts +17 -4
  42. package/src/commands/render.ts +9 -11
  43. package/src/commands/scene.ts +2 -6
  44. package/src/commands/shaders.ts +17 -1
  45. package/src/commands/shadersNew.ts +133 -0
  46. package/src/commands/test.ts +2 -1
  47. package/src/commands/thumbs.ts +3 -6
  48. package/src/commands/update.ts +42 -21
  49. package/src/desktopRenderer.ts +32 -2
  50. package/src/desktopScript.ts +1 -1
  51. package/src/dev/androidDev.ts +154 -0
  52. package/src/dev/devServer.ts +53 -6
  53. package/src/dev/webRunner.ts +22 -0
  54. package/src/index.ts +24 -4
  55. package/src/matcTool.ts +31 -19
  56. package/src/peerInstall.ts +160 -0
  57. package/src/peers.ts +48 -14
  58. package/src/projectEnv.ts +4 -0
  59. package/src/releases.ts +16 -0
package/dist/index.js CHANGED
@@ -47,6 +47,78 @@ var __export = (target, all) => {
47
47
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
48
48
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
49
49
 
50
+ // package.json
51
+ var package_default;
52
+ var init_package = __esm(() => {
53
+ package_default = {
54
+ name: "lecodes-cli",
55
+ version: "0.20.1",
56
+ dependencies: {
57
+ "@letary/chisel": "^0.10.0",
58
+ jimp: "^1.6.1",
59
+ "lecodes-sdk": "workspace:*"
60
+ },
61
+ devDependencies: {
62
+ "lecodes-design": "^0.9.0",
63
+ "lecodes-renderer": "workspace:*",
64
+ "qrcode-terminal": "^0.12.0",
65
+ "lecodes-3d-editor": "^0.7.0",
66
+ "lecodes-assets": "workspace:*",
67
+ "lecodes-creator-nav": "workspace:*",
68
+ "lecodes-viewer-lite": "workspace:*"
69
+ },
70
+ peerDependencies: {
71
+ "lecodes-design": "^0.9.0",
72
+ "lecodes-renderer": "^0.20.0",
73
+ "lecodes-3d-editor": "^0.7.0",
74
+ "lecodes-assets": "^0.2.1"
75
+ },
76
+ bin: {
77
+ lecodes: "dist/index.js"
78
+ },
79
+ description: "Clone, edit and push LeCodes projects from your machine.",
80
+ engines: {
81
+ node: ">=18"
82
+ },
83
+ files: [
84
+ "dist",
85
+ "src",
86
+ "runtime",
87
+ "README.md"
88
+ ],
89
+ keywords: [
90
+ "lecodes",
91
+ "cli"
92
+ ],
93
+ license: "MIT",
94
+ peerDependenciesMeta: {
95
+ "lecodes-design": {
96
+ optional: true
97
+ },
98
+ "lecodes-renderer": {
99
+ optional: true
100
+ },
101
+ "lecodes-3d-editor": {
102
+ optional: true
103
+ },
104
+ "lecodes-assets": {
105
+ optional: true
106
+ }
107
+ },
108
+ scripts: {
109
+ icon: "bun scripts/make-icon.ts",
110
+ "vendor-runtime": "bun scripts/vendor-runtime.ts",
111
+ "sync-peers": "bun scripts/sync-peer-versions.ts",
112
+ build: 'bun scripts/vendor-runtime.ts && bun build src/index.ts --target=node --outfile dist/index.js --external @letary/chisel --external lecodes-renderer --external lecodes-design --external lecodes-3d-editor --external lecodes-assets --banner="#!/usr/bin/env node"',
113
+ "build:bin": "bun scripts/build-bin.ts",
114
+ typecheck: "tsc --noEmit",
115
+ dev: "bun run src/index.ts",
116
+ prepublishOnly: "bun run sync-peers && bun run build"
117
+ },
118
+ type: "module"
119
+ };
120
+ });
121
+
50
122
  // ../sdk/src/compile/assetName.ts
51
123
  var inRanges = (cp, ranges) => ranges.some(([from, to]) => cp >= from && cp <= to), INVISIBLE, ODD_SPACE, ILLEGAL = '/\\:*?"<>|', sanitizeAssetName = (name) => {
52
124
  let out = "";
@@ -172165,17 +172237,12 @@ var buildHeader = (name, code, opts = {}) => {
172165
172237
  };
172166
172238
 
172167
172239
  // ../sdk/src/compile/compileProject.ts
172168
- var compileProject = async (opts) => {
172169
- const entries = stubServerEntries(opts.entries, opts.serverUrl ?? "");
172170
- const entry = opts.entryOverride ?? detectEntry(entries, scanModulesChisel).entry;
172171
- if (!entry) {
172172
- throw new Error("Couldn't find an entrypoint (no .ts/.js project file — name yours main.ts). Pass entryOverride.");
172173
- }
172240
+ var entriesToFiles = (entries, opts) => {
172174
172241
  const files = {};
172175
172242
  for (const e of entries) {
172176
172243
  if (e.type === "resource") {
172177
172244
  if (opts.localAssets && !e.remote) {
172178
- const name = e.path.split(/[\\/]/).pop() ?? e.path;
172245
+ const name = e.path.split(/[\/]/).pop() ?? e.path;
172179
172246
  files[e.path] = `export default ("id:" + _creatorUtils.fetchLocal(${JSON.stringify(name)}))`;
172180
172247
  } else {
172181
172248
  files[e.path] = `export default "${opts.publicUrl}${e.fileSrc ?? ""}"`;
@@ -172183,7 +172250,7 @@ var compileProject = async (opts) => {
172183
172250
  } else if (e.type === "shader") {
172184
172251
  const shaders = e.shaders ?? [];
172185
172252
  if (e.shadersLocal && shaders.length > 0) {
172186
- const name = shaders[0].src.split(/[\\/]/).pop() ?? "";
172253
+ const name = shaders[0].src.split(/[\/]/).pop() ?? "";
172187
172254
  const [prefix, suffix = ".filamat"] = name.split("_opengl");
172188
172255
  files[e.path] = `export default ("id:" + _creatorUtils.fetchLocal(${JSON.stringify(prefix + "_")} + _creator.backend + ${JSON.stringify(suffix)}))`;
172189
172256
  } else if (shaders.length > 0) {
@@ -172193,21 +172260,30 @@ var compileProject = async (opts) => {
172193
172260
  }
172194
172261
  } else if (e.path.toLowerCase().endsWith(".svg")) {
172195
172262
  files[e.path] = `export default SvgSource(\`${e.text ?? ""}\`)`;
172263
+ } else if (e.path.toLowerCase().endsWith(".json")) {
172264
+ files[e.path] = `export default ${e.text?.trim() || "null"}`;
172196
172265
  } else {
172197
172266
  files[e.path] = e.text ?? "";
172198
172267
  }
172199
172268
  }
172200
- const { code, map } = await bundleProjectWithMap(entry, files, opts.format ?? "esm", opts.minify, opts.editor, opts.libraries, opts.sourcemap ?? true);
172201
- if (opts.header === false) {
172202
- return map ? `${code}
172203
- ${inlineSourceMapComment(map)}` : code;
172269
+ return files;
172270
+ }, compileProjectWithMap = async (opts) => {
172271
+ const entries = stubServerEntries(opts.entries, opts.serverUrl ?? "");
172272
+ const entry = opts.entryOverride ?? detectEntry(entries, scanModulesChisel).entry;
172273
+ if (!entry) {
172274
+ throw new Error("Couldn't find an entrypoint (no .ts/.js project file — name yours main.ts). Pass entryOverride.");
172204
172275
  }
172276
+ const files = entriesToFiles(entries, opts);
172277
+ const { code, map } = await bundleProjectWithMap(entry, files, opts.format ?? "esm", opts.minify, opts.editor, opts.libraries, opts.sourcemap ?? true);
172278
+ if (opts.header === false)
172279
+ return { code, map, entry };
172205
172280
  const header = buildHeader(opts.name, code, { icon: opts.icon, fonts: opts.fontsHeader, icons: opts.iconsHeader });
172206
- if (!map)
172207
- return header + code;
172208
172281
  const headerLines = (header.match(/\n/g) ?? []).length;
172209
- return `${header}${code}
172210
- ${inlineSourceMapComment(offsetSourceMap(map, headerLines))}`;
172282
+ return { code: header + code, map: map ? offsetSourceMap(map, headerLines) : null, entry };
172283
+ }, compileProject = async (opts) => {
172284
+ const { code, map } = await compileProjectWithMap(opts);
172285
+ return map ? `${code}
172286
+ ${inlineSourceMapComment(map)}` : code;
172211
172287
  };
172212
172288
  var init_compileProject = __esm(() => {
172213
172289
  init_bundler();
@@ -173142,10 +173218,12 @@ __export(exports_compile, {
173142
173218
  faceFileName: () => faceFileName,
173143
173219
  extractMatBlocks: () => extractMatBlocks,
173144
173220
  externalSamplerReg: () => externalSamplerReg,
173221
+ entriesToFiles: () => entriesToFiles,
173145
173222
  detectProjectKind: () => detectProjectKind,
173146
173223
  detectEntry: () => detectEntry,
173147
173224
  deriveEndpointParams: () => deriveEndpointParams,
173148
173225
  compileServerBundle: () => compileServerBundle,
173226
+ compileProjectWithMap: () => compileProjectWithMap,
173149
173227
  compileProject: () => compileProject,
173150
173228
  collectVendoredIcons: () => collectVendoredIcons,
173151
173229
  bundleProjectWithMap: () => bundleProjectWithMap,
@@ -214125,26 +214203,26 @@ var init_esm30 = __esm(() => {
214125
214203
 
214126
214204
  // src/types.ts
214127
214205
  import { dirname as dirname13, join as join22, relative as relative3, sep } from "node:path";
214128
- import { existsSync as existsSync21, mkdirSync as mkdirSync15, readdirSync as readdirSync10, readFileSync as readFileSync18, rmSync as rmSync8, statSync as statSync8, writeFileSync as writeFileSync17 } from "node:fs";
214206
+ import { existsSync as existsSync21, mkdirSync as mkdirSync15, readdirSync as readdirSync11, readFileSync as readFileSync18, rmSync as rmSync8, statSync as statSync9, writeFileSync as writeFileSync17 } from "node:fs";
214129
214207
  import { createRequire as createRequire4 } from "node:module";
214130
214208
  var TSCONFIG, listBundleFiles = (dir, base = dir) => {
214131
214209
  if (!existsSync21(dir))
214132
214210
  return [];
214133
214211
  const out = [];
214134
- for (const name of readdirSync10(dir)) {
214212
+ for (const name of readdirSync11(dir)) {
214135
214213
  const path2 = join22(dir, name);
214136
- if (statSync8(path2).isDirectory())
214214
+ if (statSync9(path2).isDirectory())
214137
214215
  out.push(...listBundleFiles(path2, base));
214138
214216
  else
214139
214217
  out.push(relative3(base, path2).split(sep).join("/"));
214140
214218
  }
214141
214219
  return out;
214142
214220
  }, pruneEmptyDirs = (dir, root) => {
214143
- if (!existsSync21(dir) || !statSync8(dir).isDirectory())
214221
+ if (!existsSync21(dir) || !statSync9(dir).isDirectory())
214144
214222
  return;
214145
- for (const name of readdirSync10(dir))
214223
+ for (const name of readdirSync11(dir))
214146
214224
  pruneEmptyDirs(join22(dir, name), root);
214147
- if (dir !== root && readdirSync10(dir).length === 0)
214225
+ if (dir !== root && readdirSync11(dir).length === 0)
214148
214226
  rmSync8(dir, { recursive: true });
214149
214227
  }, writeTypeBundle = (root, bundle) => {
214150
214228
  const typesDir = join22(root, LECODES_DIR, "types");
@@ -214230,13 +214308,13 @@ var init_types4 = __esm(() => {
214230
214308
 
214231
214309
  // src/compile/designCompile.ts
214232
214310
  import { readFileSync as readFileSync19 } from "node:fs";
214233
- import { basename as basename9 } from "node:path";
214311
+ import { basename as basename10 } from "node:path";
214234
214312
  var LOCAL_ASSET_SCHEME = "lecodesfile://", compileDesignScreen = async (opts) => {
214235
214313
  const { root, manifest, designDir, designKey, screenId, state } = opts;
214236
214314
  const entries = [];
214237
214315
  const scan2 = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest?.files ?? {})) });
214238
214316
  for (const file of scan2.files) {
214239
- const type = manifest?.files[file.path]?.type ?? classifyKind(basename9(file.path));
214317
+ const type = manifest?.files[file.path]?.type ?? classifyKind(basename10(file.path));
214240
214318
  if (type === "resource") {
214241
214319
  entries.push({
214242
214320
  path: file.path,
@@ -214368,7 +214446,7 @@ var init_browserAuth = __esm(() => {
214368
214446
  init_util();
214369
214447
  });
214370
214448
 
214371
- // ../../../lecodes-tools/packages/lecodes-design/src/core/concept.ts
214449
+ // ../../node_modules/.bun/lecodes-design@0.9.0+edf5a69441922373/node_modules/lecodes-design/src/core/concept.ts
214372
214450
  var SPEC_TEMPLATE = `# Concept
214373
214451
 
214374
214452
  The narrative half of this design. \`meta.json\` + the canvas hold the *visual/flow* half (screens,
@@ -214404,7 +214482,7 @@ Behaviour that isn't visible in a single screen — validation, permissions, not
214404
214482
  cases — and the decisions you've made and why, so they aren't re-litigated later.
214405
214483
  `;
214406
214484
 
214407
- // ../../../lecodes-tools/packages/lecodes-design/src/templates.ts
214485
+ // ../../node_modules/.bun/lecodes-design@0.9.0+edf5a69441922373/node_modules/lecodes-design/src/templates.ts
214408
214486
  var SPEC_MD, META_JSON = `{
214409
214487
  "screens": {
214410
214488
  "home": { "pos": [0, 0], "desc": "Example screen — replace me", "role": "list" }
@@ -214843,12 +214921,179 @@ var init_designTemplates = __esm(() => {
214843
214921
  init_templates();
214844
214922
  });
214845
214923
 
214924
+ // src/peers.ts
214925
+ import { existsSync as existsSync24, readFileSync as readFileSync20 } from "node:fs";
214926
+ import { createRequire as createRequire5 } from "node:module";
214927
+ import { homedir as homedir8 } from "node:os";
214928
+ import { dirname as dirname14, join as join24, resolve as resolve11 } from "node:path";
214929
+ var OPTIONAL_PEERS, peersDir = () => process.env.LECODES_PEERS_DIR ? resolve11(process.env.LECODES_PEERS_DIR) : join24(homedir8(), ".lecodes", "peers"), peerDirInPrefix = (name) => join24(peersDir(), "node_modules", ...name.split("/")), readVersion = (dir) => {
214930
+ try {
214931
+ return JSON.parse(readFileSync20(join24(dir, "package.json"), "utf8")).version ?? "unknown";
214932
+ } catch {
214933
+ return null;
214934
+ }
214935
+ }, peerVersionGlobal = (name, probe) => {
214936
+ try {
214937
+ const require2 = createRequire5(import.meta.url);
214938
+ let dir = dirname14(require2.resolve(probe));
214939
+ for (let depth = 0;depth < 16; depth++) {
214940
+ const manifest = join24(dir, "package.json");
214941
+ if (existsSync24(manifest)) {
214942
+ const parsed = JSON.parse(readFileSync20(manifest, "utf8"));
214943
+ if (parsed.name === name)
214944
+ return parsed.version ?? "unknown";
214945
+ }
214946
+ const parent = dirname14(dir);
214947
+ if (parent === dir)
214948
+ break;
214949
+ dir = parent;
214950
+ }
214951
+ } catch {}
214952
+ return null;
214953
+ }, peerVersionPrefix = (name) => readVersion(peerDirInPrefix(name)), peerVersion = (name, probe) => peerVersionGlobal(name, probe) ?? peerVersionPrefix(name), peerSource = (name, probe) => peerVersionGlobal(name, probe) !== null ? "global" : peerVersionPrefix(name) !== null ? "prefix" : null;
214954
+ var init_peers = __esm(() => {
214955
+ OPTIONAL_PEERS = [
214956
+ ["lecodes-renderer", "lecodes-renderer/headless"],
214957
+ ["lecodes-design", "lecodes-design/server"],
214958
+ ["lecodes-3d-editor", "lecodes-3d-editor/server"],
214959
+ ["lecodes-assets", "lecodes-assets/cli"],
214960
+ ["@napi-rs/canvas", "@napi-rs/canvas"]
214961
+ ];
214962
+ });
214963
+
214964
+ // src/peerInstall.ts
214965
+ import { spawnSync as spawnSync6 } from "node:child_process";
214966
+ import { existsSync as existsSync25, mkdirSync as mkdirSync18, readFileSync as readFileSync21, writeFileSync as writeFileSync19 } from "node:fs";
214967
+ import { join as join25 } from "node:path";
214968
+ import { pathToFileURL } from "node:url";
214969
+ async function loadPeer(name, subpath, opts) {
214970
+ const spec = subpath ? `${name}/${subpath}` : name;
214971
+ try {
214972
+ return await import(spec);
214973
+ } catch (e2) {
214974
+ if (!isNotFound(e2, spec))
214975
+ throw e2;
214976
+ }
214977
+ const dir = peerDirInPrefix(name);
214978
+ if (!existsSync25(join25(dir, "package.json"))) {
214979
+ if (opts.install === false)
214980
+ return null;
214981
+ if (peerVersionGlobal(name, spec) !== null)
214982
+ throw new CliError(`${name} is installed but "${spec}" can't be loaded from it — reinstall it: npm install -g ${peerSpec(name)}`);
214983
+ await installPeers(companions(name), opts.for);
214984
+ }
214985
+ return await import(pathToFileURL(resolveEntry(dir, subpath)).href);
214986
+ }
214987
+ var CANVAS_RANGE_FALLBACK = "^0.1.65", readJson2 = (path2) => {
214988
+ try {
214989
+ return JSON.parse(readFileSync21(path2, "utf8"));
214990
+ } catch {
214991
+ return null;
214992
+ }
214993
+ }, peerSpec = (name) => {
214994
+ if (name === "lecodes-renderer")
214995
+ return `lecodes-renderer@${package_default.version}`;
214996
+ if (name === "@napi-rs/canvas") {
214997
+ const renderer = readJson2(join25(peerDirInPrefix("lecodes-renderer"), "package.json"));
214998
+ const range2 = renderer?.peerDependencies?.["@napi-rs/canvas"] ?? renderer?.optionalDependencies?.["@napi-rs/canvas"] ?? CANVAS_RANGE_FALLBACK;
214999
+ return `@napi-rs/canvas@${range2}`;
215000
+ }
215001
+ const range = package_default.peerDependencies?.[name];
215002
+ return range ? `${name}@${range}` : `${name}@latest`;
215003
+ }, companions = (name) => name === "lecodes-renderer" ? ["lecodes-renderer", "@napi-rs/canvas"] : [name], onPath = (cmd) => {
215004
+ try {
215005
+ return spawnSync6(cmd, ["--version"], { stdio: "ignore", shell: process.platform === "win32" }).status === 0;
215006
+ } catch {
215007
+ return false;
215008
+ }
215009
+ }, pickInstaller = () => onPath("npm") ? "npm" : onPath("bun") ? "bun" : null, manualCommand = (dir, specs) => `npm install --prefix "${dir}" ${specs.join(" ")}`, installPeers = async (names3, why, opts = {}) => {
215010
+ const dir = peersDir();
215011
+ mkdirSync18(dir, { recursive: true });
215012
+ const manifest = join25(dir, "package.json");
215013
+ if (!existsSync25(manifest)) {
215014
+ writeFileSync19(manifest, JSON.stringify({
215015
+ name: "lecodes-peers",
215016
+ private: true,
215017
+ description: "Optional lecodes-cli peers, installed on first use. Safe to delete — the CLI re-installs what a command needs."
215018
+ }, null, 2) + `
215019
+ `);
215020
+ }
215021
+ const ordered = [...names3].sort((a2, b) => a2 === "lecodes-renderer" ? -1 : b === "lecodes-renderer" ? 1 : 0);
215022
+ const specsNow = ordered.map(peerSpec);
215023
+ const yes = opts.yes === true || !!process.env.LECODES_YES;
215024
+ if (!yes) {
215025
+ const interactive = process.stdin.isTTY && process.stdout.isTTY;
215026
+ if (!interactive) {
215027
+ throw new CliError(`${why} needs ${names3.join(" + ")}, which isn't installed. Re-run with --yes to install it into ${dir}, or install by hand: ${manualCommand(dir, specsNow)}`);
215028
+ }
215029
+ log(`${why} needs ${c.bold(names3.join(" + "))}.`);
215030
+ log(c.dim(`It installs once into ${dir} (no admin rights); a newer CLI re-installs its own version.`));
215031
+ if (!await confirm("Install now?", true)) {
215032
+ throw new CliError(`Not installed. When you want it: ${manualCommand(dir, specsNow)}`);
215033
+ }
215034
+ }
215035
+ const tool = pickInstaller();
215036
+ if (!tool)
215037
+ throw new CliError(`Neither npm nor bun is on PATH, so ${names3.join(" + ")} can't be installed automatically. Install one, or run on a machine with Node: ${manualCommand(dir, specsNow)}`);
215038
+ for (const name of ordered) {
215039
+ const spec = peerSpec(name);
215040
+ const argv = tool === "npm" ? ["install", "--prefix", dir, "--no-audit", "--no-fund", "--loglevel=error", spec] : ["add", "--cwd", dir, spec];
215041
+ note(`$ ${tool} ${argv.join(" ")}`);
215042
+ const r2 = spawnSync6(tool, argv, { stdio: "inherit", shell: process.platform === "win32", timeout: 10 * 60000 });
215043
+ if (r2.error)
215044
+ throw new CliError(`${tool} could not run: ${r2.error.message}`);
215045
+ if (r2.status !== 0)
215046
+ throw new CliError(`${tool} exited with ${r2.status ?? "a signal"} while installing ${spec}. By hand: ${manualCommand(dir, [spec])}`);
215047
+ if (!existsSync25(join25(peerDirInPrefix(name), "package.json")))
215048
+ throw new CliError(`${name} did not land in ${dir} — the installer finished but the package is missing`);
215049
+ }
215050
+ }, resolveEntry = (pkgDir, subpath) => {
215051
+ const manifest = readJson2(join25(pkgDir, "package.json"));
215052
+ if (!manifest)
215053
+ throw new CliError(`${pkgDir} holds no package.json`);
215054
+ const key = subpath ? `./${subpath}` : ".";
215055
+ const pick = (target) => {
215056
+ if (typeof target === "string")
215057
+ return target;
215058
+ if (target && typeof target === "object") {
215059
+ const t2 = target;
215060
+ for (const cond of ["import", "node", "default", "require"])
215061
+ if (cond in t2) {
215062
+ const p2 = pick(t2[cond]);
215063
+ if (p2)
215064
+ return p2;
215065
+ }
215066
+ }
215067
+ return null;
215068
+ };
215069
+ let rel = null;
215070
+ const exp = manifest.exports;
215071
+ if (typeof exp === "string")
215072
+ rel = key === "." ? exp : null;
215073
+ else if (exp && typeof exp === "object")
215074
+ rel = pick(exp[key]);
215075
+ if (!rel && key === ".")
215076
+ rel = typeof manifest.main === "string" ? manifest.main : "index.js";
215077
+ if (!rel)
215078
+ throw new CliError(`${manifest.name} has no export "${key}"`);
215079
+ return join25(pkgDir, rel);
215080
+ }, isNotFound = (e2, spec) => {
215081
+ const code = e2?.code;
215082
+ const msg = e2 instanceof Error ? e2.message : String(e2);
215083
+ return (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND" || /Cannot find (module|package)/i.test(msg)) && msg.includes(spec.split("/")[0]);
215084
+ };
215085
+ var init_peerInstall = __esm(() => {
215086
+ init_package();
215087
+ init_peers();
215088
+ init_util();
215089
+ });
215090
+
214846
215091
  // src/designMeta.ts
214847
- import { readFileSync as readFileSync20 } from "node:fs";
214848
- import { join as join24 } from "node:path";
215092
+ import { readFileSync as readFileSync22 } from "node:fs";
215093
+ import { join as join26 } from "node:path";
214849
215094
  var PHONE_SIZE, readMetaFrames = (designDir) => {
214850
215095
  try {
214851
- const meta = JSON.parse(readFileSync20(join24(designDir, "meta.json"), "utf8"));
215096
+ const meta = JSON.parse(readFileSync22(join26(designDir, "meta.json"), "utf8"));
214852
215097
  const d = meta?.defaultSize;
214853
215098
  const valid = Array.isArray(d) && d.length === 2 && d.every((n2) => typeof n2 === "number" && Number.isFinite(n2) && n2 >= 64 && n2 <= 8192);
214854
215099
  return {
@@ -214872,8 +215117,8 @@ __export(exports_design, {
214872
215117
  flushDesignComments: () => flushDesignComments,
214873
215118
  design: () => design
214874
215119
  });
214875
- import { existsSync as existsSync24, mkdirSync as mkdirSync18, readFileSync as readFileSync21, readdirSync as readdirSync13, writeFileSync as writeFileSync19, appendFileSync } from "node:fs";
214876
- import { basename as basename10, join as join25 } from "node:path";
215120
+ import { existsSync as existsSync26, mkdirSync as mkdirSync19, readFileSync as readFileSync23, readdirSync as readdirSync14, writeFileSync as writeFileSync20, appendFileSync } from "node:fs";
215121
+ import { basename as basename11, join as join27 } from "node:path";
214877
215122
  var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214878
215123
  let root;
214879
215124
  let manifest;
@@ -214888,21 +215133,16 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214888
215133
  return {
214889
215134
  root,
214890
215135
  manifest,
214891
- name: manifest?.name ?? basename10(root),
215136
+ name: manifest?.name ?? basename11(root),
214892
215137
  dirName,
214893
- designDir: join25(root, dirName),
215138
+ designDir: join27(root, dirName),
214894
215139
  designKey: "/" + dirName.replace(/\\/g, "/")
214895
215140
  };
214896
215141
  }, loadDesignServer = async (ctx, needs = []) => {
214897
- if (!existsSync24(ctx.designDir)) {
215142
+ if (!existsSync26(ctx.designDir)) {
214898
215143
  throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
214899
215144
  }
214900
- let mod;
214901
- try {
214902
- mod = await import("lecodes-design/server");
214903
- } catch {
214904
- throw new CliError("The design canvas isn't installed. Run: npm install -g lecodes-design");
214905
- }
215145
+ const mod = await loadPeer("lecodes-design", "server", { for: "lecodes design" });
214906
215146
  const missing = needs.filter((name) => typeof mod[name] !== "function");
214907
215147
  if (missing.length) {
214908
215148
  throw new CliError(`This needs a newer lecodes-design — the installed one has no \`${missing[0]}\`. Run: npm install -g lecodes-design@latest`);
@@ -214910,7 +215150,7 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214910
215150
  return mod;
214911
215151
  }, requirePlatform = (ctx, verb) => {
214912
215152
  if (!ctx.manifest) {
214913
- throw new CliError(`\`lecodes design comments ${verb}\` needs a cloned project. This folder is standalone, so ${ctx.dirName}/comments/ is already the only store — there is nothing to sync with.`);
215153
+ throw new CliError(`\`lecodes design comments ${verb}\` needs a cloned project. This folder is standalone, so ` + `${ctx.dirName}/comments/ is already the only store — there is nothing to sync with.`);
214914
215154
  }
214915
215155
  const config = loadConfig();
214916
215156
  return { apiUrl: requireApiUrl(config), token: requireToken(config), uuid: ctx.manifest.uuid };
@@ -214926,17 +215166,17 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214926
215166
  `).find((l2) => l2.trim())?.trim() ?? "";
214927
215167
  return line.length > max ? line.slice(0, max - 1) + "…" : line;
214928
215168
  }, listScreens = (designDir) => {
214929
- const dir = join25(designDir, "screens");
214930
- if (!existsSync24(dir))
215169
+ const dir = join27(designDir, "screens");
215170
+ if (!existsSync26(dir))
214931
215171
  return [];
214932
- return readdirSync13(dir).filter((f3) => f3.endsWith(".ts") && !f3.startsWith(".")).map((f3) => f3.slice(0, -3)).sort();
215172
+ return readdirSync14(dir).filter((f3) => f3.endsWith(".ts") && !f3.startsWith(".")).map((f3) => f3.slice(0, -3)).sort();
214933
215173
  }, writeMcpConfig = (ctx, port) => {
214934
- const path2 = join25(ctx.root, ".mcp.json");
215174
+ const path2 = join27(ctx.root, ".mcp.json");
214935
215175
  const entry = { type: "http", url: `http://localhost:${port}/mcp` };
214936
215176
  let config = {};
214937
- if (existsSync24(path2)) {
215177
+ if (existsSync26(path2)) {
214938
215178
  try {
214939
- config = JSON.parse(readFileSync21(path2, "utf8"));
215179
+ config = JSON.parse(readFileSync23(path2, "utf8"));
214940
215180
  } catch {
214941
215181
  warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-design server by hand.");
214942
215182
  return;
@@ -214949,7 +215189,7 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214949
215189
  return;
214950
215190
  }
214951
215191
  config.mcpServers["lecodes-design"] = entry;
214952
- writeFileSync19(path2, JSON.stringify(config, null, 2) + `
215192
+ writeFileSync20(path2, JSON.stringify(config, null, 2) + `
214953
215193
  `);
214954
215194
  log(`${c.green(existing ? "updated" : "created")} .mcp.json (lecodes-design MCP → ${entry.url})`);
214955
215195
  }, init = async (ctx, args) => {
@@ -214958,26 +215198,26 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214958
215198
  ["spec.md", SPEC_MD],
214959
215199
  ["README.md", README_HINT],
214960
215200
  ["CLAUDE.md", CLAUDE_MD],
214961
- [join25("shared", "tokens.ts"), TOKENS_TS],
214962
- [join25("shared", "tabs.ts"), TABS_TS],
214963
- [join25("screens", "home.ts"), HOME_SCREEN_TS]
215201
+ [join27("shared", "tokens.ts"), TOKENS_TS],
215202
+ [join27("shared", "tabs.ts"), TABS_TS],
215203
+ [join27("screens", "home.ts"), HOME_SCREEN_TS]
214964
215204
  ];
214965
- mkdirSync18(join25(ctx.designDir, "screens"), { recursive: true });
214966
- mkdirSync18(join25(ctx.designDir, "shared"), { recursive: true });
215205
+ mkdirSync19(join27(ctx.designDir, "screens"), { recursive: true });
215206
+ mkdirSync19(join27(ctx.designDir, "shared"), { recursive: true });
214967
215207
  let created = 0;
214968
215208
  for (const [rel, content] of files) {
214969
- const abs = join25(ctx.designDir, rel);
214970
- if (existsSync24(abs)) {
215209
+ const abs = join27(ctx.designDir, rel);
215210
+ if (existsSync26(abs)) {
214971
215211
  note(`kept ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
214972
215212
  continue;
214973
215213
  }
214974
- writeFileSync19(abs, content);
215214
+ writeFileSync20(abs, content);
214975
215215
  log(`${c.green("created")} ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
214976
215216
  created++;
214977
215217
  }
214978
- const ignorePath = join25(ctx.root, ".lecodesignore");
215218
+ const ignorePath = join27(ctx.root, ".lecodesignore");
214979
215219
  const ignoreLines = [`${ctx.dirName}/.snapshots/`, `${ctx.dirName}/comments/`];
214980
- const current = existsSync24(ignorePath) ? readFileSync21(ignorePath, "utf8") : "";
215220
+ const current = existsSync26(ignorePath) ? readFileSync23(ignorePath, "utf8") : "";
214981
215221
  const present = new Set(current.split(/\r?\n/));
214982
215222
  const missing = ignoreLines.filter((line) => !present.has(line));
214983
215223
  if (missing.length) {
@@ -214992,8 +215232,8 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214992
215232
  writeMcpConfig(ctx, Number(flagStr(args, "port")) || DEFAULT_PORT);
214993
215233
  const wroteTypes = materializeTypesLocal(ctx.designDir);
214994
215234
  writeTsconfig(ctx.designDir);
214995
- mkdirSync18(join25(ctx.designDir, ".lecodes", "types"), { recursive: true });
214996
- writeFileSync19(join25(ctx.designDir, ".lecodes", "types", "design-macros.d.ts"), DESIGN_MACROS_DTS);
215235
+ mkdirSync19(join27(ctx.designDir, ".lecodes", "types"), { recursive: true });
215236
+ writeFileSync20(join27(ctx.designDir, ".lecodes", "types", "design-macros.d.ts"), DESIGN_MACROS_DTS);
214997
215237
  log(`${c.green("created")} ${ctx.dirName}/tsconfig.json${wroteTypes ? ` + ${ctx.dirName}/.lecodes/types/` : ""} (IDE types)`);
214998
215238
  if (!wroteTypes)
214999
215239
  warnErr("Bundled SDK types not found — tsconfig written, but globals may show unresolved until the CLI is rebuilt with vendored types.");
@@ -215016,23 +215256,14 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
215016
215256
  const allStates = flagBool(args, "all-states");
215017
215257
  if (stateFlag !== undefined && allStates)
215018
215258
  throw new CliError("Use either --state <name> or --all-states, not both.");
215019
- let renderer;
215020
- try {
215021
- renderer = await import("lecodes-renderer/headless");
215022
- } catch {
215023
- throw new CliError("The renderer isn't installed. Run: npm install -g lecodes-renderer");
215024
- }
215259
+ const renderer = await loadPeer("lecodes-renderer", "headless", { for: "lecodes design render" });
215025
215260
  let discover = null;
215026
215261
  if (stateFlag !== undefined || allStates) {
215027
- try {
215028
- const ds = await import("lecodes-design/server");
215029
- discover = (src) => {
215030
- const d = ds.discoverScreenStates(src);
215031
- return { states: d.states, canonical: d.canonical };
215032
- };
215033
- } catch {
215034
- throw new CliError("Rendering specific states needs the design canvas. Run: npm install -g lecodes-design");
215035
- }
215262
+ const ds = await loadPeer("lecodes-design", "server", { for: "lecodes design render --state" });
215263
+ discover = (src) => {
215264
+ const d = ds.discoverScreenStates(src);
215265
+ return { states: d.states, canonical: d.canonical };
215266
+ };
215036
215267
  }
215037
215268
  const killer = setTimeout(() => {
215038
215269
  warnErr("Snapshot timed out after 120s.");
@@ -215041,16 +215272,16 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
215041
215272
  killer.unref?.();
215042
215273
  const png2 = flagBool(args, "png") || flagStr(args, "png") !== undefined;
215043
215274
  const frames = readMetaFrames(ctx.designDir);
215044
- const outDir = flagStr(args, "out-dir") ?? join25(ctx.designDir, ".snapshots");
215275
+ const outDir = flagStr(args, "out-dir") ?? join27(ctx.designDir, ".snapshots");
215045
215276
  const single = targets.length === 1 && requested.length === 1 && !png2 && !allStates && !flagStr(args, "out-dir");
215046
215277
  const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
215047
215278
  if (!single)
215048
- mkdirSync18(outDir, { recursive: true });
215279
+ mkdirSync19(outDir, { recursive: true });
215049
215280
  for (const id of targets) {
215050
215281
  const [width, height] = frames.screens[id]?.size ?? frames.defaultSize;
215051
215282
  let plan = [{ state: undefined, bases: [id] }];
215052
215283
  if (discover) {
215053
- const { states, canonical } = discover(readFileSync21(join25(ctx.designDir, "screens", `${id}.ts`), "utf8"));
215284
+ const { states, canonical } = discover(readFileSync23(join27(ctx.designDir, "screens", `${id}.ts`), "utf8"));
215054
215285
  if (stateFlag !== undefined) {
215055
215286
  if (!states.includes(stateFlag)) {
215056
215287
  if (requested.length === 1)
@@ -215091,11 +215322,11 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
215091
215322
  for (const w of pngResult.warnings)
215092
215323
  warnErr(`${tag}: ${w}`);
215093
215324
  for (const base of bases) {
215094
- writeFileSync19(join25(outDir, `${base}.json`), result.text);
215095
- log(`${c.green("✓")} ${tag} → ${join25(outDir, `${base}.json`)}`);
215325
+ writeFileSync20(join27(outDir, `${base}.json`), result.text);
215326
+ log(`${c.green("✓")} ${tag} → ${join27(outDir, `${base}.json`)}`);
215096
215327
  if (pngResult) {
215097
- writeFileSync19(join25(outDir, `${base}.png`), pngResult.png);
215098
- log(`${c.green("✓")} ${tag} → ${join25(outDir, `${base}.png`)}`);
215328
+ writeFileSync20(join27(outDir, `${base}.png`), pngResult.png);
215329
+ log(`${c.green("✓")} ${tag} → ${join27(outDir, `${base}.png`)}`);
215099
215330
  }
215100
215331
  }
215101
215332
  }
@@ -215103,12 +215334,7 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
215103
215334
  }, serve = async (ctx, args) => {
215104
215335
  const designServer = await loadDesignServer(ctx);
215105
215336
  const port = Number(flagStr(args, "port")) || DEFAULT_PORT;
215106
- let renderer = null;
215107
- try {
215108
- renderer = await import("lecodes-renderer/headless");
215109
- } catch {
215110
- renderer = null;
215111
- }
215337
+ const renderer = await loadPeer("lecodes-renderer", "headless", { for: "lecodes design (render_screen)", install: false });
215112
215338
  const renderScreen = renderer ? async (id, format, state) => {
215113
215339
  const r2 = renderer;
215114
215340
  const js = await compileDesignScreen({
@@ -215397,17 +215623,14 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
215397
215623
  note("Read them with: lecodes design comments");
215398
215624
  }, flushDesignComments = async (args) => {
215399
215625
  const ctx = getContext(args);
215400
- if (!existsSync24(ctx.designDir))
215626
+ if (!existsSync26(ctx.designDir))
215401
215627
  return null;
215402
215628
  const platform = optionalPlatform(ctx);
215403
215629
  if (!platform)
215404
215630
  return null;
215405
- let designServer;
215406
- try {
215407
- designServer = await import("lecodes-design/server");
215408
- } catch {
215631
+ const designServer = await loadPeer("lecodes-design", "server", { for: "lecodes design", install: false });
215632
+ if (!designServer)
215409
215633
  return null;
215410
- }
215411
215634
  if (typeof designServer.queuedThreads !== "function")
215412
215635
  return null;
215413
215636
  if (typeof designServer.settleQueued !== "function" || designServer.settleQueued.length < 4)
@@ -215514,6 +215737,7 @@ var init_design = __esm(() => {
215514
215737
  init_browserAuth();
215515
215738
  init_designTemplates();
215516
215739
  init_util();
215740
+ init_peerInstall();
215517
215741
  init_config();
215518
215742
  init_api();
215519
215743
  init_types4();
@@ -216536,80 +216760,15 @@ var require_main = __commonJS((exports, module) => {
216536
216760
  }
216537
216761
  };
216538
216762
  });
216539
- // package.json
216540
- var package_default = {
216541
- name: "lecodes-cli",
216542
- version: "0.19.2",
216543
- dependencies: {
216544
- "@letary/chisel": "^0.10.0",
216545
- jimp: "^1.6.1",
216546
- "lecodes-sdk": "workspace:*"
216547
- },
216548
- devDependencies: {
216549
- "lecodes-design": "link:lecodes-design",
216550
- "lecodes-renderer": "workspace:*",
216551
- "qrcode-terminal": "^0.12.0",
216552
- "lecodes-3d-editor": "link:lecodes-3d-editor",
216553
- "lecodes-assets": "workspace:*",
216554
- "lecodes-creator-nav": "workspace:*",
216555
- "lecodes-viewer-lite": "workspace:*"
216556
- },
216557
- peerDependencies: {
216558
- "lecodes-design": "^0.9.0",
216559
- "lecodes-renderer": "^0.19.2",
216560
- "lecodes-3d-editor": "^0.7.0",
216561
- "lecodes-assets": "^0.2.1"
216562
- },
216563
- bin: {
216564
- lecodes: "dist/index.js"
216565
- },
216566
- description: "Clone, edit and push LeCodes projects from your machine.",
216567
- engines: {
216568
- node: ">=18"
216569
- },
216570
- files: [
216571
- "dist",
216572
- "src",
216573
- "runtime",
216574
- "README.md"
216575
- ],
216576
- keywords: [
216577
- "lecodes",
216578
- "cli"
216579
- ],
216580
- license: "MIT",
216581
- peerDependenciesMeta: {
216582
- "lecodes-design": {
216583
- optional: true
216584
- },
216585
- "lecodes-renderer": {
216586
- optional: true
216587
- },
216588
- "lecodes-3d-editor": {
216589
- optional: true
216590
- },
216591
- "lecodes-assets": {
216592
- optional: true
216593
- }
216594
- },
216595
- scripts: {
216596
- icon: "bun scripts/make-icon.ts",
216597
- "vendor-runtime": "bun scripts/vendor-runtime.ts",
216598
- "sync-peers": "bun scripts/sync-peer-versions.ts",
216599
- build: 'bun scripts/vendor-runtime.ts && bun build src/index.ts --target=node --outfile dist/index.js --external @letary/chisel --external lecodes-renderer --external lecodes-design --external lecodes-3d-editor --external lecodes-assets --banner="#!/usr/bin/env node"',
216600
- "build:bin": "bun scripts/build-bin.ts",
216601
- typecheck: "tsc --noEmit",
216602
- dev: "bun run src/index.ts",
216603
- prepublishOnly: "bun run sync-peers && bun run build"
216604
- },
216605
- type: "module"
216606
- };
216763
+
216764
+ // src/index.ts
216765
+ init_package();
216607
216766
 
216608
216767
  // src/commands/app.ts
216609
216768
  init_manifest();
216610
216769
  init_config();
216611
216770
  import { execFileSync as execFileSync6 } from "node:child_process";
216612
- import { cpSync as cpSync4, existsSync as existsSync20, mkdirSync as mkdirSync14, readFileSync as readFileSync17, readdirSync as readdirSync9, rmSync as rmSync7, statSync as statSync7, writeFileSync as writeFileSync16 } from "node:fs";
216771
+ import { cpSync as cpSync4, existsSync as existsSync20, mkdirSync as mkdirSync14, readFileSync as readFileSync17, readdirSync as readdirSync10, rmSync as rmSync7, statSync as statSync8, writeFileSync as writeFileSync16 } from "node:fs";
216613
216772
  import { homedir as homedir7 } from "node:os";
216614
216773
  import { basename as basename8, dirname as dirname12, join as join21, resolve as resolve8 } from "node:path";
216615
216774
 
@@ -216767,6 +216926,11 @@ var semverNewer = (a, b) => {
216767
216926
  return (pb[i] ?? 0) - (pa[i] ?? 0);
216768
216927
  return 0;
216769
216928
  };
216929
+ var newestForPlatform = (versions, key) => {
216930
+ const carrying = versions.filter((v) => v.files[key]).sort((a, b) => semverNewer(a.version, b.version));
216931
+ const newest = carrying[0];
216932
+ return newest ? { version: newest.version, file: newest.files[key] } : null;
216933
+ };
216770
216934
  var fetchReleaseProduct = async (product, label) => {
216771
216935
  let manifest;
216772
216936
  try {
@@ -216871,12 +217035,15 @@ var installedMatcVersions = () => {
216871
217035
  }
216872
217036
  };
216873
217037
  var fetchMatcManifest = () => fetchReleaseProduct("matc", "matc (shader compiler)");
216874
- var downloadMatc = async (version, versions, dest) => {
217038
+ var pickMatc = (versions) => {
216875
217039
  const key = platformKey();
216876
- const file = versions.find((v) => v.version === version)?.files[key];
216877
- if (!file) {
216878
- throw new CliError(`matc ${version} [${key}] isn't published — push the project once so shaders compile on the ` + `server, or set LECODES_MATC to a locally built matc.`);
217040
+ const hit = newestForPlatform(versions, key);
217041
+ if (!hit) {
217042
+ throw new CliError(`matc isn't published for ${key} — push the project once so shaders compile on the ` + `server, or set LECODES_MATC to a locally built matc.`);
216879
217043
  }
217044
+ return hit;
217045
+ };
217046
+ var downloadMatc = async (version, file, dest) => {
216880
217047
  note(`Downloading the shader compiler (matc ${version}, ${(file.size / 1024 / 1024).toFixed(1)} MB)…`);
216881
217048
  await downloadReleaseArchive(file, {
216882
217049
  cacheRoot: cacheRoot(),
@@ -216898,24 +217065,26 @@ var resolveMatcExe = async () => {
216898
217065
  const cached = resolveMatcExeCached();
216899
217066
  if (cached)
216900
217067
  return cached;
216901
- const { latest, versions } = await fetchMatcManifest();
216902
- const dir = join12(cacheRoot(), `matc-${latest}`);
216903
- await downloadMatc(latest, versions, dir);
217068
+ const { versions } = await fetchMatcManifest();
217069
+ const { version, file } = pickMatc(versions);
217070
+ const dir = join12(cacheRoot(), `matc-${version}`);
217071
+ await downloadMatc(version, file, dir);
216904
217072
  const exe = findMatc(dir);
216905
217073
  if (!exe)
216906
217074
  throw new CliError(`matc cache at ${dir} is broken — delete the folder and retry.`);
216907
217075
  return exe;
216908
217076
  };
216909
217077
  var updateMatc = async () => {
216910
- const { latest, versions } = await fetchMatcManifest();
217078
+ const { versions } = await fetchMatcManifest();
217079
+ const { version, file } = pickMatc(versions);
216911
217080
  const before = installedMatcVersions()[0];
216912
- const dir = join12(cacheRoot(), `matc-${latest}`);
217081
+ const dir = join12(cacheRoot(), `matc-${version}`);
216913
217082
  if (existsSync10(dir) && findMatc(dir) !== null) {
216914
- note(`Already up to date: matc ${latest}.`);
217083
+ note(`Already up to date: matc ${version}.`);
216915
217084
  return;
216916
217085
  }
216917
- await downloadMatc(latest, versions, dir);
216918
- note(before ? `matc updated to ${latest} (was ${before}).` : `matc ${latest} installed.`);
217086
+ await downloadMatc(version, file, dir);
217087
+ note(before ? `matc updated to ${version} (was ${before}).` : `matc ${version} installed.`);
216919
217088
  };
216920
217089
 
216921
217090
  // src/compile/shaders.ts
@@ -217036,12 +217205,13 @@ init_manifest();
217036
217205
  init_ignore();
217037
217206
  init_util();
217038
217207
  import { execFileSync } from "node:child_process";
217039
- import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "node:fs";
217208
+ import { existsSync as existsSync13, mkdirSync as mkdirSync8, readdirSync as readdirSync5, readFileSync as readFileSync13, statSync as statSync3, writeFileSync as writeFileSync9 } from "node:fs";
217040
217209
  import { homedir as homedir4 } from "node:os";
217041
217210
  import { dirname as dirname8, join as join15, resolve as resolve2 } from "node:path";
217042
217211
  var FIRST_PARTY_PLUGINS = {
217043
217212
  camera: "letary/lecodes-plugins",
217044
217213
  "qr-scanner": "letary/lecodes-plugins",
217214
+ map: "letary/lecodes-plugins",
217045
217215
  geolocation: "letary/lecodes-plugins",
217046
217216
  push: "letary/lecodes-plugins"
217047
217217
  };
@@ -217072,6 +217242,7 @@ var desktopDefaultViewport = (root) => {
217072
217242
  height: cfg.desktop.window?.height ?? DESKTOP_DEFAULT_WINDOW.height
217073
217243
  };
217074
217244
  };
217245
+ var swiftPackageName = (url) => (url.replace(/\/+$/, "").split("/").pop() ?? url).replace(/\.git$/, "");
217075
217246
  var readJson = (path) => {
217076
217247
  if (!existsSync13(path))
217077
217248
  return null;
@@ -217225,7 +217396,8 @@ var ensureRepo = (repo, wantTag, lock) => {
217225
217396
  git(cache, "checkout", "--quiet", "--detach", commit);
217226
217397
  return { dir: cache, tag: tag ?? "unknown", commit };
217227
217398
  };
217228
- var resolvePlugin = (id, spec, lock, platform) => {
217399
+ var PLUGIN_PLATFORM_LABEL = { ios: "iOS", android: "Android", desktop: "desktop" };
217400
+ function resolvePlugin(id, spec, lock, platform, opts) {
217229
217401
  let repo;
217230
217402
  let subdir;
217231
217403
  let wantTag = null;
@@ -217249,7 +217421,9 @@ var resolvePlugin = (id, spec, lock, platform) => {
217249
217421
  if (manifest.id !== id)
217250
217422
  throw new CliError(`Plugin "${id}": manifest declares id "${manifest.id}"`);
217251
217423
  if (!manifest[platform]) {
217252
- throw new CliError(`Plugin "${id}" does not support ${platform === "ios" ? "iOS" : "Android"} (manifest "${platform}" is null)`);
217424
+ if (opts?.optional)
217425
+ return null;
217426
+ throw new CliError(`Plugin "${id}" does not support ${PLUGIN_PLATFORM_LABEL[platform]} (manifest "${platform}" is null)`);
217253
217427
  }
217254
217428
  if (spec.startsWith("github:")) {
217255
217429
  const names = [...manifest.provides?.views ?? [], ...manifest.provides?.services ?? []];
@@ -217264,7 +217438,7 @@ var resolvePlugin = (id, spec, lock, platform) => {
217264
217438
  manifest,
217265
217439
  lock: { repo, tag: checkout.tag, commit: checkout.commit, version: manifest.version ?? "unknown" }
217266
217440
  };
217267
- };
217441
+ }
217268
217442
  var latestPluginTag = (repo) => {
217269
217443
  try {
217270
217444
  return ensureRepo(repo, null, undefined).tag;
@@ -217272,6 +217446,55 @@ var latestPluginTag = (repo) => {
217272
217446
  return null;
217273
217447
  }
217274
217448
  };
217449
+ var desktopPluginTarget = () => `${process.platform}-${process.arch}`;
217450
+ var isDesktopPluginLibrary = (name) => {
217451
+ const n = name.toLowerCase();
217452
+ return process.platform === "win32" ? n.endsWith(".dll") : n.endsWith(".so");
217453
+ };
217454
+ var desktopPluginLibraries = (root, opts = {}) => {
217455
+ const dirs = [];
217456
+ const files = [];
217457
+ const local = join15(root, "plugins");
217458
+ if (existsSync13(local) && statSync3(local).isDirectory()) {
217459
+ dirs.push(local);
217460
+ for (const f2 of readdirSync5(local))
217461
+ if (isDesktopPluginLibrary(f2))
217462
+ files.push(join15(local, f2));
217463
+ }
217464
+ const config = readJson(join15(root, "app.json"));
217465
+ const specs = Object.entries(config?.plugins ?? {});
217466
+ if (specs.length === 0)
217467
+ return { dirs, files };
217468
+ const lock = readJson(join15(root, "plugins.lock.json"))?.plugins ?? {};
217469
+ const target = desktopPluginTarget();
217470
+ for (const [id, spec] of specs) {
217471
+ let plugin;
217472
+ try {
217473
+ plugin = resolvePlugin(id, spec, lock[id], "desktop", { optional: true });
217474
+ } catch (e) {
217475
+ if (opts.strict)
217476
+ throw e;
217477
+ warn(`Plugin "${id}": ${e.message} — skipped for this desktop run`);
217478
+ continue;
217479
+ }
217480
+ const rel = plugin?.manifest.desktop?.library?.[target];
217481
+ if (!plugin || !rel)
217482
+ continue;
217483
+ const abs = resolve2(plugin.dir, rel);
217484
+ if (!existsSync13(abs)) {
217485
+ const msg = `Plugin "${id}": desktop library ${abs} is missing (manifest desktop.library["${target}"])`;
217486
+ if (opts.strict)
217487
+ throw new CliError(msg);
217488
+ warn(`${msg} — skipped for this desktop run`);
217489
+ continue;
217490
+ }
217491
+ files.push(abs);
217492
+ const dir = dirname8(abs);
217493
+ if (!dirs.includes(dir))
217494
+ dirs.push(dir);
217495
+ }
217496
+ return { dirs, files };
217497
+ };
217275
217498
 
217276
217499
  // src/commands/compile.ts
217277
217500
  init_util();
@@ -218076,13 +218299,31 @@ var APP_ICON_JSON = `{
218076
218299
  `;
218077
218300
  var runtimePackageTemplate = (sdkVersion, variant, plugins) => {
218078
218301
  const product = `LeCodesSDK-${variant}`;
218079
- const pluginTargets = plugins.map((p) => `
218302
+ const packages = new Map;
218303
+ for (const p of plugins) {
218304
+ for (const pkg of p.packages ?? []) {
218305
+ const prior = packages.get(pkg.url);
218306
+ if (prior && (prior.exact !== pkg.exact || prior.from !== pkg.from)) {
218307
+ throw new Error(`Plugin "${p.id}" pins ${pkg.url} at ${pkg.exact ?? `from ${pkg.from}`} but another plugin pins ${prior.exact ?? `from ${prior.from}`} — the shell can link one version.`);
218308
+ }
218309
+ packages.set(pkg.url, pkg);
218310
+ }
218311
+ }
218312
+ const packageDecls = [...packages.values()].map((pkg) => `
218313
+ .package(url: "${pkg.url}", ${pkg.exact ? `exact: "${pkg.exact}"` : `from: "${pkg.from}"`}),`).join("");
218314
+ const pluginTargets = plugins.map((p) => {
218315
+ const deps = [
218316
+ `.product(name: "${product}", package: "lecodes-ios-sdk")`,
218317
+ ...(p.packages ?? []).flatMap((pkg) => pkg.products.map((name) => `.product(name: "${name}", package: "${swiftPackageName(pkg.url)}")`))
218318
+ ];
218319
+ return `
218080
218320
  // plugin: ${p.id} (vendored — see plugins.lock.json)
218081
218321
  .target(
218082
218322
  name: "${p.module}",
218083
- dependencies: [.product(name: "${product}", package: "lecodes-ios-sdk")],
218323
+ dependencies: [${deps.join(", ")}],
218084
218324
  path: "Sources/${p.module}"
218085
- ),`).join("");
218325
+ ),`;
218326
+ }).join("");
218086
218327
  const runtimeDeps = [`.product(name: "${product}", package: "lecodes-ios-sdk")`, ...plugins.map((p) => `"${p.module}"`)];
218087
218328
  return `// swift-tools-version:5.9
218088
218329
  //
@@ -218101,7 +218342,7 @@ let package = Package(
218101
218342
  .library(name: "LeCodesRuntime", targets: ["LeCodesRuntime"]),
218102
218343
  ],
218103
218344
  dependencies: [
218104
- .package(url: "https://github.com/letary/lecodes-ios-sdk.git", exact: "${sdkVersion}"),
218345
+ .package(url: "https://github.com/letary/lecodes-ios-sdk.git", exact: "${sdkVersion}"),${packageDecls}
218105
218346
  ],
218106
218347
  targets: [
218107
218348
  .target(
@@ -218247,7 +218488,7 @@ buildNumber) — edit it there, then re-run \`lecodes app sync\`. Plugins are ma
218247
218488
  init_manifest();
218248
218489
  import { execFileSync as execFileSync2, spawn, spawnSync as spawnSync3 } from "node:child_process";
218249
218490
  import { createHash as createHash6 } from "node:crypto";
218250
- import { chmodSync, cpSync, existsSync as existsSync16, mkdirSync as mkdirSync10, readFileSync as readFileSync15, readdirSync as readdirSync5, renameSync as renameSync2, rmSync as rmSync3, statSync as statSync3, writeFileSync as writeFileSync12 } from "node:fs";
218491
+ import { chmodSync, cpSync, existsSync as existsSync16, mkdirSync as mkdirSync10, readFileSync as readFileSync15, readdirSync as readdirSync6, renameSync as renameSync2, rmSync as rmSync3, statSync as statSync4, writeFileSync as writeFileSync12 } from "node:fs";
218251
218492
  import { homedir as homedir5 } from "node:os";
218252
218493
  import { basename as basename5, dirname as dirname10, join as join17 } from "node:path";
218253
218494
  init_util();
@@ -219494,7 +219735,7 @@ var syncAndroid = async (root, config, prior) => {
219494
219735
  }
219495
219736
  const namespace = register.split(".").slice(0, -1).join(".");
219496
219737
  const libsDir = join17(dest, "libs");
219497
- const aarNames = existsSync16(libsDir) ? readdirSync5(libsDir).filter((f3) => f3.endsWith(".aar")) : [];
219738
+ const aarNames = existsSync16(libsDir) ? readdirSync6(libsDir).filter((f3) => f3.endsWith(".aar")) : [];
219498
219739
  for (const aar of aarNames) {
219499
219740
  const base = aar.replace(/\.aar$/, "");
219500
219741
  const wrapDir = join17(libsDir, base);
@@ -219510,8 +219751,8 @@ var syncAndroid = async (root, config, prior) => {
219510
219751
  log(`${c.green("plugin")} ${id}@${plugin.lock.version} (${plugin.lock.tag} → plugins/${module})`);
219511
219752
  }
219512
219753
  const keep = new Set(runtimePlugins.map((p2) => p2.module));
219513
- for (const entry of existsSync16(pluginsDir) ? readdirSync5(pluginsDir) : []) {
219514
- if (!keep.has(entry) && statSync3(join17(pluginsDir, entry)).isDirectory()) {
219754
+ for (const entry of existsSync16(pluginsDir) ? readdirSync6(pluginsDir) : []) {
219755
+ if (!keep.has(entry) && statSync4(join17(pluginsDir, entry)).isDirectory()) {
219515
219756
  rmrf(join17(pluginsDir, entry));
219516
219757
  log(`${c.yellow("removed")} android/plugins/${entry} (plugin uninstalled)`);
219517
219758
  }
@@ -219676,7 +219917,7 @@ var buildAndroid = async (args) => {
219676
219917
  for (const dir of [apkDir, aabDir]) {
219677
219918
  if (!existsSync16(dir))
219678
219919
  continue;
219679
- for (const f3 of readdirSync5(dir)) {
219920
+ for (const f3 of readdirSync6(dir)) {
219680
219921
  if (f3.endsWith(".apk") || f3.endsWith(".aab"))
219681
219922
  outputs.push(join17(dir, f3));
219682
219923
  }
@@ -219733,7 +219974,7 @@ var doctorAndroidChecks = async (root, config, check) => {
219733
219974
  const validVariant = requestedVariant === "auto" || SDK_VARIANTS.includes(requestedVariant);
219734
219975
  check(validVariant, `android.variant "${requestedVariant}"`, `use "auto" or one of ${SDK_VARIANTS.join(", ")}`);
219735
219976
  const wrapperJar = join17(androidDir, "gradle", "wrapper", "gradle-wrapper.jar");
219736
- check(existsSync16(wrapperJar) && statSync3(wrapperJar).size > 0, "Gradle wrapper jar", `run \`gradle wrapper --gradle-version ${GRADLE_VERSION}\` in android/ (or let Android Studio repair it)`);
219977
+ check(existsSync16(wrapperJar) && statSync4(wrapperJar).size > 0, "Gradle wrapper jar", `run \`gradle wrapper --gradle-version ${GRADLE_VERSION}\` in android/ (or let Android Studio repair it)`);
219737
219978
  check(existsSync16(join17(androidDir, "app", "src", "main", "assets", "app.js")), "Bundled app.js", "run `lecodes app sync`");
219738
219979
  const linked = shellSdkVersionAndroid(androidDir);
219739
219980
  const pin = config.android?.sdk ?? "latest";
@@ -219756,14 +219997,14 @@ var doctorAndroidChecks = async (root, config, check) => {
219756
219997
 
219757
219998
  // src/commands/appDesktop.ts
219758
219999
  import { execFileSync as execFileSync5, spawn as spawn4 } from "node:child_process";
219759
- import { cpSync as cpSync3, existsSync as existsSync19, mkdirSync as mkdirSync13, readdirSync as readdirSync8, rmSync as rmSync6, statSync as statSync6, writeFileSync as writeFileSync15 } from "node:fs";
220000
+ import { cpSync as cpSync3, existsSync as existsSync19, mkdirSync as mkdirSync13, readdirSync as readdirSync9, rmSync as rmSync6, statSync as statSync7, writeFileSync as writeFileSync15 } from "node:fs";
219760
220001
  import { basename as basename7, dirname as dirname11, join as join20, resolve as resolve7 } from "node:path";
219761
220002
 
219762
220003
  // src/desktopRenderer.ts
219763
220004
  import { execFileSync as execFileSync3, spawn as spawn2, spawnSync as spawnSync4 } from "node:child_process";
219764
- import { copyFileSync, existsSync as existsSync17, mkdirSync as mkdirSync11, mkdtempSync as mkdtempSync2, readdirSync as readdirSync6, readFileSync as readFileSync16, rmSync as rmSync4, statSync as statSync4, writeFileSync as writeFileSync13 } from "node:fs";
220005
+ import { copyFileSync, existsSync as existsSync17, mkdirSync as mkdirSync11, mkdtempSync as mkdtempSync2, readdirSync as readdirSync7, readFileSync as readFileSync16, rmSync as rmSync4, statSync as statSync5, writeFileSync as writeFileSync13 } from "node:fs";
219765
220006
  import { homedir as homedir6, tmpdir as tmpdir2 } from "node:os";
219766
- import { join as join18, resolve as resolve5 } from "node:path";
220007
+ import { delimiter, join as join18, resolve as resolve5 } from "node:path";
219767
220008
  init_util();
219768
220009
 
219769
220010
  // ../viewer-lite/src/sourcemapError.ts
@@ -219883,7 +220124,7 @@ var isMac = () => process.platform === "darwin";
219883
220124
  var exeName = () => process.platform === "win32" ? "lecodes-desktop.exe" : isMac() ? "lecodes-app" : "lecodes-desktop";
219884
220125
  var findExe = (dir) => {
219885
220126
  const name = exeName();
219886
- for (const entry of readdirSync6(dir, { withFileTypes: true })) {
220127
+ for (const entry of readdirSync7(dir, { withFileTypes: true })) {
219887
220128
  const p2 = join18(dir, entry.name);
219888
220129
  if (entry.isFile() && entry.name === name)
219889
220130
  return p2;
@@ -219899,7 +220140,7 @@ var cacheRoot2 = () => join18(homedir6(), ".lecodes", "renderer");
219899
220140
  var currentFile = () => join18(cacheRoot2(), "current.json");
219900
220141
  var installedDesktopVersions = () => {
219901
220142
  try {
219902
- return readdirSync6(cacheRoot2(), { withFileTypes: true }).filter((e2) => e2.isDirectory() && e2.name.startsWith("desktop-")).map((e2) => e2.name.slice("desktop-".length)).filter((v) => findExe(join18(cacheRoot2(), `desktop-${v}`)) !== null).sort(semverNewer);
220143
+ return readdirSync7(cacheRoot2(), { withFileTypes: true }).filter((e2) => e2.isDirectory() && e2.name.startsWith("desktop-")).map((e2) => e2.name.slice("desktop-".length)).filter((v) => findExe(join18(cacheRoot2(), `desktop-${v}`)) !== null).sort(semverNewer);
219903
220144
  } catch {
219904
220145
  return [];
219905
220146
  }
@@ -219924,7 +220165,7 @@ var extractDmg = (archive, stage) => {
219924
220165
  mkdirSync11(mount, { recursive: true });
219925
220166
  execFileSync3("hdiutil", ["attach", "-nobrowse", "-readonly", "-quiet", "-mountpoint", mount, archive]);
219926
220167
  try {
219927
- const app = readdirSync6(mount).find((n2) => n2.endsWith(".app"));
220168
+ const app = readdirSync7(mount).find((n2) => n2.endsWith(".app"));
219928
220169
  if (!app)
219929
220170
  throw new CliError("The downloaded DMG holds no .app bundle.");
219930
220171
  execFileSync3("cp", ["-R", join18(mount, app), join18(stage, app)]);
@@ -220045,8 +220286,12 @@ var updateDesktop = async () => {
220045
220286
  else
220046
220287
  note(`Desktop renderer set to ${latest} (was ${before}).`);
220047
220288
  };
220289
+ var pluginEnv = (dirs) => {
220290
+ const all = [...dirs ?? [], ...process.env.CREATOR_PLUGIN_DIRS ? [process.env.CREATOR_PLUGIN_DIRS] : []];
220291
+ return all.length > 0 ? { CREATOR_PLUGIN_DIRS: all.join(delimiter) } : {};
220292
+ };
220048
220293
  var windowEnv = (opts) => {
220049
- const env = {};
220294
+ const env = { ...pluginEnv(opts.pluginDirs) };
220050
220295
  const width = opts.width ?? (opts.height !== undefined ? 1280 : undefined);
220051
220296
  const height = opts.height ?? (opts.width !== undefined ? 720 : undefined);
220052
220297
  if (width && height)
@@ -220055,6 +220300,8 @@ var windowEnv = (opts) => {
220055
220300
  env.CREATOR_MIN_WINDOW_SIZE = `${opts.minWidth}x${opts.minHeight}`;
220056
220301
  if (opts.title)
220057
220302
  env.CREATOR_WINDOW_TITLE = opts.title;
220303
+ if (opts.storageScope)
220304
+ env.CREATOR_STORAGE_SCOPE = opts.storageScope;
220058
220305
  if (opts.resizable === false)
220059
220306
  env.CREATOR_WINDOW_FIXED = "1";
220060
220307
  if (opts.fullscreen !== undefined)
@@ -220070,6 +220317,12 @@ var windowEnv = (opts) => {
220070
220317
  }
220071
220318
  if (opts.fps)
220072
220319
  env.CREATOR_FPS = "1";
220320
+ if (opts.mouseEmulateTouch !== undefined && process.env.CREATOR_MOUSE_TOUCH === undefined) {
220321
+ env.CREATOR_MOUSE_TOUCH = opts.mouseEmulateTouch ? "1" : "0";
220322
+ }
220323
+ if (opts.virtualKeyboard !== undefined && process.env.CREATOR_VIRTUAL_KEYBOARD === undefined) {
220324
+ env.CREATOR_VIRTUAL_KEYBOARD = typeof opts.virtualKeyboard === "boolean" ? opts.virtualKeyboard ? "1" : "0" : JSON.stringify(opts.virtualKeyboard);
220325
+ }
220073
220326
  return env;
220074
220327
  };
220075
220328
  var netArgs = (net) => {
@@ -220090,7 +220343,7 @@ var stageBundle = (js, resources) => {
220090
220343
  const runDir = mkdtempSync2(join18(tmpdir2(), "lecodes-render-"));
220091
220344
  writeFileSync13(join18(runDir, "app.js"), js);
220092
220345
  for (const r2 of resources) {
220093
- if (r2.absPath && existsSync17(r2.absPath) && statSync4(r2.absPath).isFile()) {
220346
+ if (r2.absPath && existsSync17(r2.absPath) && statSync5(r2.absPath).isFile()) {
220094
220347
  copyFileSync(r2.absPath, join18(runDir, r2.name));
220095
220348
  }
220096
220349
  }
@@ -220182,6 +220435,7 @@ var runDesktopRender = async (js, resources, opts) => {
220182
220435
  env.CREATOR_CAMERA = opts.camera;
220183
220436
  if (opts.view)
220184
220437
  env.CREATOR_VIEW = opts.view;
220438
+ Object.assign(env, pluginEnv(opts.pluginDirs));
220185
220439
  const demangle = makeStackDemangler(js) ?? ((text) => text);
220186
220440
  return await new Promise((done, fail) => {
220187
220441
  const child = spawn2(exe, [appJs], { cwd: runDir, env, stdio: ["ignore", "pipe", "pipe"] });
@@ -220240,13 +220494,13 @@ init_util();
220240
220494
  // src/commands/appDesktopMac.ts
220241
220495
  init_util();
220242
220496
  import { execFileSync as execFileSync4, spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
220243
- import { cpSync as cpSync2, existsSync as existsSync18, mkdirSync as mkdirSync12, mkdtempSync as mkdtempSync3, readdirSync as readdirSync7, rmSync as rmSync5, statSync as statSync5, symlinkSync, writeFileSync as writeFileSync14 } from "node:fs";
220497
+ import { cpSync as cpSync2, existsSync as existsSync18, mkdirSync as mkdirSync12, mkdtempSync as mkdtempSync3, readdirSync as readdirSync8, rmSync as rmSync5, statSync as statSync6, symlinkSync, writeFileSync as writeFileSync14 } from "node:fs";
220244
220498
  import { tmpdir as tmpdir3 } from "node:os";
220245
220499
  import { basename as basename6, join as join19, resolve as resolve6 } from "node:path";
220246
220500
  var mb = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)} MB`;
220247
- var dirSize = (dir) => readdirSync7(dir, { withFileTypes: true }).reduce((sum, e2) => {
220501
+ var dirSize = (dir) => readdirSync8(dir, { withFileTypes: true }).reduce((sum, e2) => {
220248
220502
  const p2 = join19(dir, e2.name);
220249
- return sum + (e2.isDirectory() ? dirSize(p2) : e2.isSymbolicLink() ? 0 : statSync5(p2).size);
220503
+ return sum + (e2.isDirectory() ? dirSize(p2) : e2.isSymbolicLink() ? 0 : statSync6(p2).size);
220250
220504
  }, 0);
220251
220505
  var cleanBundleName = (name) => name.replace(/[/:]/g, "").trim() || "App";
220252
220506
  var macHostAppOf = (exe) => {
@@ -220259,7 +220513,7 @@ var macHostAppOf = (exe) => {
220259
220513
  var macHostProvidedNames = (exe) => {
220260
220514
  const names3 = new Set;
220261
220515
  const walk = (dir) => {
220262
- for (const e2 of readdirSync7(dir, { withFileTypes: true })) {
220516
+ for (const e2 of readdirSync8(dir, { withFileTypes: true })) {
220263
220517
  if (e2.isDirectory())
220264
220518
  walk(join19(dir, e2.name));
220265
220519
  else
@@ -220413,7 +220667,7 @@ ${(r2.stdout + r2.stderr).trim()}`);
220413
220667
  if (signing.notaryProfile)
220414
220668
  notarize(dmg, signing.notaryProfile, tmp, "disk image");
220415
220669
  }
220416
- success(`DMG: ${dmg} (${mb(statSync5(dmg).size)})`);
220670
+ success(`DMG: ${dmg} (${mb(statSync6(dmg).size)})`);
220417
220671
  };
220418
220672
  var gatekeeperVerdict = (app) => {
220419
220673
  const r2 = spawnSync5("spctl", ["-a", "-t", "exec", "-vv", app], { encoding: "utf8" });
@@ -220514,12 +220768,12 @@ var buildMacApp = async (input) => {
220514
220768
  writeFileSync14(join19(proj, "app.js"), js);
220515
220769
  let assetBytes = 0, assetCount = 0;
220516
220770
  for (const r2 of shipped) {
220517
- if (!r2.absPath || !existsSync18(r2.absPath) || !statSync5(r2.absPath).isFile()) {
220771
+ if (!r2.absPath || !existsSync18(r2.absPath) || !statSync6(r2.absPath).isFile()) {
220518
220772
  warn(`asset "${r2.name}" (${r2.path}) has no local file — skipped`);
220519
220773
  continue;
220520
220774
  }
220521
220775
  cpSync2(r2.absPath, join19(proj, "assets", r2.name));
220522
- assetBytes += statSync5(r2.absPath).size;
220776
+ assetBytes += statSync6(r2.absPath).size;
220523
220777
  assetCount++;
220524
220778
  }
220525
220779
  note(` assets: ${assetCount} files, ${mb(assetBytes)} → Contents/Resources/project/assets/`);
@@ -220567,7 +220821,7 @@ var buildMacApp = async (input) => {
220567
220821
  const zip = join19(out, `${appName}.zip`);
220568
220822
  rmSync5(zip, { force: true });
220569
220823
  execFileSync4("ditto", ["-c", "-k", "--keepParent", app, zip]);
220570
- success(`Zip: ${zip} (${mb(statSync5(zip).size)})`);
220824
+ success(`Zip: ${zip} (${mb(statSync6(zip).size)})`);
220571
220825
  } else if (!flagBool(args, "dmg")) {
220572
220826
  note("Tip: --zip packs the .app for sending; --dmg wraps it in a drag-to-Applications image.");
220573
220827
  }
@@ -220591,9 +220845,9 @@ or remove the quarantine flag in Terminal: xattr -dr com.apple.quarantine "` +
220591
220845
 
220592
220846
  // src/commands/appDesktop.ts
220593
220847
  var mb2 = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)} MB`;
220594
- var dirSize2 = (dir) => readdirSync8(dir, { withFileTypes: true }).reduce((sum, e2) => {
220848
+ var dirSize2 = (dir) => readdirSync9(dir, { withFileTypes: true }).reduce((sum, e2) => {
220595
220849
  const p2 = join20(dir, e2.name);
220596
- return sum + (e2.isDirectory() ? dirSize2(p2) : statSync6(p2).size);
220850
+ return sum + (e2.isDirectory() ? dirSize2(p2) : statSync7(p2).size);
220597
220851
  }, 0);
220598
220852
  var initDesktop = async (args) => {
220599
220853
  const root = projectRoot();
@@ -220622,7 +220876,7 @@ var buildDesktop = async (args) => {
220622
220876
  const slug = flagStr(args, "slug") ?? cleanFileName(name);
220623
220877
  const out = resolve7(root, flagStr(args, "o", "out") ?? join20(root, "dist", slug));
220624
220878
  if (existsSync19(out)) {
220625
- const entries = readdirSync8(out);
220879
+ const entries = readdirSync9(out);
220626
220880
  const looksLikeBuild = entries.length === 0 || entries.includes("app.js") || entries.some((e2) => e2.endsWith(".app"));
220627
220881
  if (!looksLikeBuild) {
220628
220882
  throw new CliError(`${out} exists and doesn't look like a previous desktop build — pick another -o or clear it yourself.`);
@@ -220659,7 +220913,7 @@ var buildDesktop = async (args) => {
220659
220913
  const hostProvided = mac ? macHostProvidedNames(exe) : new Set;
220660
220914
  if (!mac) {
220661
220915
  try {
220662
- for (const f3 of readdirSync8(join20(hostDir, "assets")))
220916
+ for (const f3 of readdirSync9(join20(hostDir, "assets")))
220663
220917
  hostProvided.add(f3);
220664
220918
  } catch {}
220665
220919
  }
@@ -220675,7 +220929,7 @@ var buildDesktop = async (args) => {
220675
220929
  const version = process.env.LECODES_DESKTOP_EXE ? "local build" : selectedDesktopVersion();
220676
220930
  rmSync6(out, { recursive: true, force: true });
220677
220931
  mkdirSync13(out, { recursive: true });
220678
- for (const entry of readdirSync8(hostDir, { withFileTypes: true })) {
220932
+ for (const entry of readdirSync9(hostDir, { withFileTypes: true })) {
220679
220933
  if (entry.isFile() && entry.name.toLowerCase().endsWith(".dll")) {
220680
220934
  cpSync3(join20(hostDir, entry.name), join20(out, entry.name));
220681
220935
  } else if (entry.isDirectory() && entry.name === "assets") {
@@ -220695,20 +220949,37 @@ var buildDesktop = async (args) => {
220695
220949
  if (shipped.length > 0)
220696
220950
  mkdirSync13(join20(out, ASSETS_DIR), { recursive: true });
220697
220951
  for (const r2 of shipped) {
220698
- if (!r2.absPath || !existsSync19(r2.absPath) || !statSync6(r2.absPath).isFile()) {
220952
+ if (!r2.absPath || !existsSync19(r2.absPath) || !statSync7(r2.absPath).isFile()) {
220699
220953
  warn(`asset "${r2.name}" (${r2.path}) has no local file — skipped`);
220700
220954
  continue;
220701
220955
  }
220702
220956
  cpSync3(r2.absPath, join20(out, ASSETS_DIR, r2.name));
220703
- assetBytes += statSync6(r2.absPath).size;
220957
+ assetBytes += statSync7(r2.absPath).size;
220704
220958
  assetCount++;
220705
220959
  }
220706
220960
  note(` assets: ${assetCount} files, ${mb2(assetBytes)} → ${ASSETS_DIR}/`);
220707
- writeJson(join20(out, "app.json"), { name, ...config.desktop ? { desktop: { window: config.desktop.window } } : {} });
220961
+ const desktop = config.desktop;
220962
+ writeJson(join20(out, "app.json"), {
220963
+ name,
220964
+ ...desktop ? { desktop: {
220965
+ window: desktop.window,
220966
+ ...desktop.mouseEmulateTouch !== undefined ? { mouseEmulateTouch: desktop.mouseEmulateTouch } : {},
220967
+ ...desktop.virtualKeyboard !== undefined ? { virtualKeyboard: desktop.virtualKeyboard } : {}
220968
+ } } : {}
220969
+ });
220708
220970
  if (config.icon) {
220709
220971
  await writeIco(root, config, join20(out, "app.ico"));
220710
220972
  note(" app.ico rendered from " + config.icon);
220711
220973
  }
220974
+ {
220975
+ const { files } = desktopPluginLibraries(root, { strict: true });
220976
+ if (files.length > 0) {
220977
+ mkdirSync13(join20(out, "plugins"), { recursive: true });
220978
+ for (const f3 of files)
220979
+ cpSync3(f3, join20(out, "plugins", basename7(f3)));
220980
+ note(` plugins: ${files.map((f3) => basename7(f3)).join(", ")} → plugins/`);
220981
+ }
220982
+ }
220712
220983
  writeFileSync15(join20(out, "README.txt"), "\uFEFF" + readmeText2(name, exeName2, version));
220713
220984
  success(`Portable build: ${out} (${mb2(dirSize2(out))})`);
220714
220985
  if (flagBool(args, "probe"))
@@ -220808,7 +221079,7 @@ var zipBuild = (out) => {
220808
221079
  } else {
220809
221080
  execFileSync5("zip", ["-rq", zip, basename7(out)], { cwd: dirname11(out) });
220810
221081
  }
220811
- success(`Zip: ${zip} (${mb2(statSync6(zip).size)})`);
221082
+ success(`Zip: ${zip} (${mb2(statSync7(zip).size)})`);
220812
221083
  };
220813
221084
  var readmeText2 = (name, exeName2, version) => `${name}
220814
221085
  ${"=".repeat(Math.max(name.length, 4))}
@@ -221015,13 +221286,18 @@ var syncIos = async (root, config) => {
221015
221286
  for (const [key, value] of Object.entries(ios.entitlements ?? {})) {
221016
221287
  (entitlementValues[key] ??= []).push(value);
221017
221288
  }
221018
- runtimePlugins.push({ id, module, register });
221289
+ for (const pkg of ios.packages ?? []) {
221290
+ if (!pkg.url || !Array.isArray(pkg.products) || pkg.products.length === 0 || !pkg.exact && !pkg.from || pkg.exact && pkg.from) {
221291
+ throw new CliError(`Plugin "${id}": manifest ios.packages entries need { url, exact | from, products: [...] } — got ${JSON.stringify(pkg)}`);
221292
+ }
221293
+ }
221294
+ runtimePlugins.push({ id, module, register, packages: ios.packages });
221019
221295
  newLock[id] = plugin.lock;
221020
221296
  log(`${c.green("plugin")} ${id}@${plugin.lock.version} (${plugin.lock.tag} → Sources/${module})`);
221021
221297
  }
221022
221298
  const keep = new Set(["LeCodesRuntime", ...runtimePlugins.map((p2) => p2.module)]);
221023
- for (const entry of existsSync20(sourcesDir) ? readdirSync9(sourcesDir) : []) {
221024
- if (!keep.has(entry) && statSync7(join21(sourcesDir, entry)).isDirectory()) {
221299
+ for (const entry of existsSync20(sourcesDir) ? readdirSync10(sourcesDir) : []) {
221300
+ if (!keep.has(entry) && statSync8(join21(sourcesDir, entry)).isDirectory()) {
221025
221301
  rmSync7(join21(sourcesDir, entry), { recursive: true });
221026
221302
  log(`${c.yellow("removed")} ios/LeCodesRuntime/Sources/${entry} (plugin uninstalled)`);
221027
221303
  }
@@ -221049,7 +221325,7 @@ var syncIos = async (root, config) => {
221049
221325
  if (variant === "3d" || variant === "full") {
221050
221326
  const sdkResources = await ensureSdkResourcesBundle(sdkVersion);
221051
221327
  cpSync4(sdkResources, join21(resourcesDir, "LeCodesSDKResources.bundle"), { recursive: true });
221052
- for (const f3 of readdirSync9(sdkResources))
221328
+ for (const f3 of readdirSync10(sdkResources))
221053
221329
  if (f3 !== "Info.plist")
221054
221330
  sdkProvided.add(f3);
221055
221331
  log(`${c.green("resources")} LeCodesSDKResources.bundle (filament) → ios/App/Resources/`);
@@ -221387,7 +221663,7 @@ var doctorIosChecks = async (root, config, check) => {
221387
221663
  check(xcodeVersion !== "", `Xcode CLI (${xcodeVersion || "not found"})`, "install Xcode + command line tools");
221388
221664
  check(!!config.ios.teamId, "Development team set", "lecodes app init wrote app.json — fill ios.teamId", false);
221389
221665
  const iconDir = join21(root, "ios", "App", "Assets.xcassets", "AppIcon.appiconset");
221390
- const hasIcon = existsSync20(iconDir) && readdirSync9(iconDir).some((f3) => f3.endsWith(".png"));
221666
+ const hasIcon = existsSync20(iconDir) && readdirSync10(iconDir).some((f3) => f3.endsWith(".png"));
221391
221667
  check(hasIcon, "App icon present", "drop a 1024×1024 PNG into ios/App/Assets.xcassets/AppIcon.appiconset/ (App Store requires it)", false);
221392
221668
  const bundle = join21(root, "ios", "App", "Resources", "app.js");
221393
221669
  check(existsSync20(bundle), "Bundled app.js", "run `lecodes app sync`");
@@ -221430,7 +221706,7 @@ var doctorIosChecks = async (root, config, check) => {
221430
221706
  const plist = readFileSync17(join21(root, "ios", "App", "Info.plist"), "utf8");
221431
221707
  check(plist.includes("<key>LeCodesUpdateURL</key>"), "OTA update URL in Info.plist", "run `lecodes app sync`", false);
221432
221708
  const appDir = join21(root, "ios", "App");
221433
- const callsUpdater = readdirSync9(appDir).some((f3) => f3.endsWith(".swift") && readFileSync17(join21(appDir, f3), "utf8").includes("LeCodesUpdater.activeBundle"));
221709
+ const callsUpdater = readdirSync10(appDir).some((f3) => f3.endsWith(".swift") && readFileSync17(join21(appDir, f3), "utf8").includes("LeCodesUpdater.activeBundle"));
221434
221710
  check(callsUpdater, "App code launches via LeCodesUpdater", `no App/*.swift calls LeCodesUpdater.activeBundle() — downloaded updates would never run; see "Over-the-air updates" in ios/README.md for the ViewController snippet`, false);
221435
221711
  }
221436
221712
  const asc = readJson(join21(homedir7(), ".lecodes", "asc.json"));
@@ -221583,7 +221859,7 @@ var app = async (args) => {
221583
221859
  init_api();
221584
221860
  init_config();
221585
221861
  init_manifest();
221586
- import { existsSync as existsSync22, mkdirSync as mkdirSync16, readdirSync as readdirSync11 } from "node:fs";
221862
+ import { existsSync as existsSync22, mkdirSync as mkdirSync16, readdirSync as readdirSync12 } from "node:fs";
221587
221863
  import { resolve as resolve9 } from "node:path";
221588
221864
  init_types4();
221589
221865
  init_ignore();
@@ -221606,7 +221882,7 @@ var clone3 = async (args) => {
221606
221882
  const project = await getProject(apiUrl, token, uuid);
221607
221883
  const dir = args._[1] ?? slug(project.name);
221608
221884
  const root = resolve9(dir);
221609
- if (existsSync22(root) && readdirSync11(root).length > 0) {
221885
+ if (existsSync22(root) && readdirSync12(root).length > 0) {
221610
221886
  throw new CliError(`Directory "${dir}" already exists and is not empty.`);
221611
221887
  }
221612
221888
  mkdirSync16(root, { recursive: true });
@@ -221632,7 +221908,7 @@ init_config();
221632
221908
  init_manifest();
221633
221909
  init_types4();
221634
221910
  init_ignore();
221635
- import { existsSync as existsSync23, mkdirSync as mkdirSync17, readdirSync as readdirSync12, writeFileSync as writeFileSync18 } from "node:fs";
221911
+ import { existsSync as existsSync23, mkdirSync as mkdirSync17, readdirSync as readdirSync13, writeFileSync as writeFileSync18 } from "node:fs";
221636
221912
  import { join as join23, resolve as resolve10 } from "node:path";
221637
221913
 
221638
221914
  // src/commands/projectTemplates.ts
@@ -221648,14 +221924,14 @@ from \`main.ts\`.
221648
221924
 
221649
221925
  \`\`\`
221650
221926
  lecodes render # semantic JSON of the launched UI (structure, text, names, values)
221651
- lecodes render --png shot.png # pixels (needs @napi-rs/canvas installed)
221927
+ lecodes render --png shot.png # pixels (the renderer + @napi-rs/canvas install themselves on first use; --yes skips the prompt)
221652
221928
  lecodes render --png card.png --clip card # ONE component/region: a node name or x,y,w,h
221653
221929
  lecodes render src/screens/cart.ts # ONE screen module, without booting the app: any file that
221654
221930
  # \`export default\`s a UIScreen (or a (state) => UIScreen —
221655
221931
  # --state <name> picks the branch). Same flags as above.
221656
221932
  lecodes render --desktop --png shot.png # NATIVE render: real Filament 3D + UI + 2D composited —
221657
221933
  # use this to SEE 3D scenes / procedural geometry (headless
221658
- # renders record 3D as JSON only). Windows/Linux; downloads
221934
+ # renders record 3D as JSON — simulated, not drawn). Windows/Linux; downloads
221659
221935
  # the renderer on first use. --frames <n> for slow loads.
221660
221936
  lecodes render --script <file> # drive the app through a scenario (format below)
221661
221937
  lecodes render --desktop --view iso --png iso.png # 3D: inspection camera fitted to the WHOLE
@@ -221668,6 +221944,11 @@ lecodes render --desktop --script <file> # the same scenario on the NATIVE host
221668
221944
  lecodes test # all tests/*.flow.json + design-derived flows; run before "done"
221669
221945
  lecodes test --desktop # the file flows on the NATIVE host (3D/Jolt/real GPU; coordinate
221670
221946
  # flows only — design flows stay headless)
221947
+ lecodes dev --android --time 12 --png phone.png # the REAL phone over adb (USB): launches the app on
221948
+ # the dev url via adb reverse, streams device logs as [device:log],
221949
+ # screenshots after 12 s, force-stops, exit 1 on a crash. --serial
221950
+ # <id> with several devices, --apk <file> to install a build first.
221951
+ # Without --time it runs until Ctrl+C (which stops the app).
221671
221952
  \`\`\`
221672
221953
 
221673
221954
  Iterate with \`--script\` scenarios and keep the useful ones as \`tests/<name>.flow.json\` — they
@@ -221704,8 +221985,9 @@ advance in fixed 1/60 steps driven by \`wait\`/\`frames\`/\`drag\`/\`key\` — n
221704
221985
  so a flow that passes once passes every time. \`Input.key(code)\` reads the keys a flow holds and
221705
221986
  \`Input.on('keydown' | 'keyup', e => e.code)\` fires for each \`key\`/\`keyDown\`/\`keyUp\` step
221706
221987
  (events land before the next loop tick); \`stick\` values are what \`Input.gamepad(0).axis()\` reads.
221707
- Headless draws UI + the 2D scene (sprites, tilemaps, Box2D); 3D is captured as JSON and Jolt does
221708
- not simulate — use \`--desktop\` for 3D pixels. \`console.log\` + \`expect.log\` is the assertion
221988
+ Headless draws UI + the 2D scene (sprites, tilemaps, Box2D); 3D is captured as JSON with REAL Jolt
221989
+ physics (bodies fall, characters collide, triggers fire, ragdolls drop — a character with no ground
221990
+ under it falls, like on device) — use \`--desktop\` for 3D pixels. \`console.log\` + \`expect.log\` is the assertion
221709
221991
  channel for game state that has no UI.
221710
221992
 
221711
221993
  ## Make the app testable
@@ -221721,6 +222003,13 @@ channel for game state that has no UI.
221721
222003
  with \`"fixtures": "fixtures.json"\` in its envelope.
221722
222004
  - \`fill\` fills every empty input — use it before tapping a disabled-until-filled submit.
221723
222005
 
222006
+ ## 3D scenes
222007
+
222008
+ - A HUD over a \`Scene\` is a widget ATTACHED to it: \`const hud = UIWidget(...).style({ left: 16,
222009
+ bottom: 16, pointerEvents: "none" }); hud.attachTo(scene); hud.show()\`. Never a \`UIScreen\`:
222010
+ a screen is a destination, so \`UIScreen(...).open()\` after \`scene.open()\` CLOSES the scene
222011
+ (the host logs "3D scene closed" and you see only the screen) — no background makes it transparent.
222012
+
221724
222013
  ## SDK documentation
221725
222014
 
221726
222015
  \`.lecodes/types/\` is the exact API surface (signatures only; generated and gitignored — run
@@ -221774,7 +222063,7 @@ var create = async (args) => {
221774
222063
  throw new CliError("Usage: lecodes create <name> [directory]");
221775
222064
  const dir = args._[1] ?? slug2(name);
221776
222065
  const root = resolve10(dir);
221777
- if (existsSync23(root) && readdirSync12(root).length > 0) {
222066
+ if (existsSync23(root) && readdirSync13(root).length > 0) {
221778
222067
  throw new CliError(`Directory "${dir}" already exists and is not empty.`);
221779
222068
  }
221780
222069
  mkdirSync17(root, { recursive: true });
@@ -221797,6 +222086,7 @@ var create = async (args) => {
221797
222086
  // src/commands/desktop.ts
221798
222087
  init_util();
221799
222088
  init_manifest();
222089
+ import { basename as basename9 } from "node:path";
221800
222090
  var resolveScale = (flag, cfg) => {
221801
222091
  const raw = flag ?? cfg;
221802
222092
  if (raw === undefined || raw === "system")
@@ -221830,19 +222120,26 @@ var run = async (args) => {
221830
222120
  const renderer = resolveDesktopRenderer(root, args);
221831
222121
  const { js, resources } = await buildBundle(args, undefined, { localAssets: true, desktopRenderer: renderer });
221832
222122
  assertRendererSupportsBundle(renderer, js);
222123
+ const { dirs: pluginDirs, files: pluginFiles } = desktopPluginLibraries(root);
222124
+ if (pluginFiles.length > 0)
222125
+ note(`Native plugins: ${pluginFiles.map((f3) => basename9(f3)).join(", ")}`);
221833
222126
  note(`Launching LeCodes Desktop${renderer === "vulkan" ? " (Vulkan)" : ""}… (close the window to return)`);
221834
222127
  await runDesktopApp(js, resources, {
221835
222128
  renderer,
222129
+ pluginDirs,
221836
222130
  width: Number(flagStr(args, "width")) || win?.width,
221837
222131
  height: Number(flagStr(args, "height")) || win?.height,
221838
222132
  minWidth: win?.minWidth,
221839
222133
  minHeight: win?.minHeight,
221840
222134
  resizable: win?.resizable,
221841
222135
  title: flagStr(args, "title") ?? win?.title ?? appCfg?.name,
222136
+ storageScope: appCfg?.name,
221842
222137
  fullscreen: flagBool(args, "fullscreen") ? true : flagBool(args, "windowed") ? false : win?.fullscreen,
221843
222138
  scale: resolveScale(flagStr(args, "scale"), win?.scale),
221844
222139
  renderScale: resolveRenderScale(flagStr(args, "render-scale"), appCfg?.desktop?.renderScale),
221845
222140
  maxRenderSize: appCfg?.desktop?.maxRenderSize,
222141
+ mouseEmulateTouch: appCfg?.desktop?.mouseEmulateTouch,
222142
+ virtualKeyboard: appCfg?.desktop?.virtualKeyboard,
221846
222143
  fps: flagBool(args, "fps"),
221847
222144
  net: flagBool(args, "server") ? { role: "server", port: Number(flagStr(args, "port")) || undefined, maxClients: Number(flagStr(args, "max-clients")) || undefined } : flagStr(args, "connect") ? { role: "client", address: flagStr(args, "connect") } : undefined
221848
222145
  });
@@ -221886,16 +222183,16 @@ init_design();
221886
222183
  // src/commands/dev.ts
221887
222184
  init_manifest();
221888
222185
  init_ignore();
221889
- import { existsSync as existsSync25, mkdirSync as mkdirSync19, readFileSync as readFileSync22, watch, writeFileSync as writeFileSync20 } from "node:fs";
222186
+ import { existsSync as existsSync29, mkdirSync as mkdirSync20, readFileSync as readFileSync24, watch, writeFileSync as writeFileSync22 } from "node:fs";
221890
222187
  import { randomBytes as randomBytes3 } from "node:crypto";
221891
222188
  import { networkInterfaces } from "node:os";
221892
- import { basename as basename11, join as join27 } from "node:path";
222189
+ import { basename as basename12, join as join31 } from "node:path";
221893
222190
 
221894
222191
  // src/dev/devServer.ts
221895
222192
  init_localFiles();
221896
222193
  import { createServer as createServer2 } from "node:http";
221897
- import { createReadStream, statSync as statSync9 } from "node:fs";
221898
- import { join as join26 } from "node:path";
222194
+ import { createReadStream, statSync as statSync10 } from "node:fs";
222195
+ import { join as join28 } from "node:path";
221899
222196
  init_projectCompile();
221900
222197
  init_fonts();
221901
222198
  init_assetIcons();
@@ -222158,7 +222455,12 @@ var MIME = {
222158
222455
  woff2: "font/woff2",
222159
222456
  json: "application/json",
222160
222457
  txt: "text/plain",
222161
- js: "text/javascript"
222458
+ js: "text/javascript",
222459
+ mjs: "text/javascript",
222460
+ css: "text/css",
222461
+ html: "text/html",
222462
+ wasm: "application/wasm",
222463
+ map: "application/json"
222162
222464
  };
222163
222465
  var contentType = (path2) => {
222164
222466
  const i2 = path2.lastIndexOf(".");
@@ -222169,14 +222471,14 @@ var sendText = (res, status2, type, body) => {
222169
222471
  res.end(body);
222170
222472
  };
222171
222473
  var sendJson = (res, status2, body) => sendText(res, status2, "application/json", JSON.stringify(body));
222172
- var readBody = (req) => new Promise((resolve11) => {
222474
+ var readBody = (req) => new Promise((resolve12) => {
222173
222475
  let body = "";
222174
222476
  req.on("data", (c4) => {
222175
222477
  if (body.length < 256 * 1024)
222176
222478
  body += c4.toString("utf8");
222177
222479
  });
222178
- req.on("end", () => resolve11(body));
222179
- req.on("error", () => resolve11(body));
222480
+ req.on("end", () => resolve12(body));
222481
+ req.on("error", () => resolve12(body));
222180
222482
  });
222181
222483
  var startDevServer = async (options) => {
222182
222484
  const { root, manifest, name, origin, token, entryOverride } = options;
@@ -222253,22 +222555,61 @@ var startDevServer = async (options) => {
222253
222555
  else if (m.type === "error")
222254
222556
  log2(`[device:error] ${demangle(m.text ?? "")}`);
222255
222557
  };
222256
- const transportOf = (url) => url.searchParams.get("t") === "poll" ? "poll" : "ws";
222558
+ const transportOf = (url) => {
222559
+ const t2 = url.searchParams.get("t");
222560
+ return t2 === "poll" ? "poll" : t2 === "web" ? "web" : "ws";
222561
+ };
222562
+ const withPrelude = (js, url, v2, forced) => {
222563
+ const transport = forced ?? transportOf(url);
222564
+ return transport === "web" ? js : js + `
222565
+ ` + renderPrelude({ origin, base, transport, v: v2 });
222566
+ };
222567
+ const webDir = options.webDir ?? null;
222257
222568
  const handle = async (req, res) => {
222258
222569
  const url = new URL(req.url ?? "/", origin);
222259
222570
  const path2 = url.pathname;
222571
+ if (path2 === "/favicon.ico") {
222572
+ res.writeHead(204);
222573
+ res.end();
222574
+ return;
222575
+ }
222260
222576
  if (!path2.startsWith(base + "/"))
222261
222577
  return sendText(res, 404, "text/plain", "Not found");
222262
222578
  const route = path2.slice(base.length);
222263
222579
  if (route === "/dev.js") {
222264
- return sendText(res, 200, "text/javascript", devJs + `
222265
- ` + renderPrelude({ origin, base, transport: transportOf(url), v: -1 }));
222580
+ return sendText(res, 200, "text/javascript", withPrelude(devJs, url, -1));
222581
+ }
222582
+ if (route === "/dev-poll.js") {
222583
+ return sendText(res, 200, "text/javascript", withPrelude(devJs, url, -1, "poll"));
222266
222584
  }
222267
222585
  if (route === "/bundle.js") {
222268
222586
  if (bundleJs === null)
222269
222587
  return sendText(res, 503, "text/plain", "No successful compile yet — check the terminal.");
222270
- return sendText(res, 200, "text/javascript", bundleJs + `
222271
- ` + renderPrelude({ origin, base, transport: transportOf(url), v }));
222588
+ res.setHeader("x-lecodes-v", String(v));
222589
+ return sendText(res, 200, "text/javascript", withPrelude(bundleJs, url, v));
222590
+ }
222591
+ if (route === "/web") {
222592
+ res.writeHead(302, { location: `${base}/web/${url.search}` });
222593
+ res.end();
222594
+ return;
222595
+ }
222596
+ if (route.startsWith("/web/")) {
222597
+ if (!webDir)
222598
+ return sendText(res, 404, "text/plain", "No Web Lite runner vendored with this CLI (lecodes-viewer-lite dist-embed).");
222599
+ const rel = route === "/web/" ? "embed.html" : route.slice("/web/".length);
222600
+ if (rel.split("/").some((seg) => seg === ".." || seg === "" || seg === "."))
222601
+ return sendText(res, 400, "text/plain", "Bad path");
222602
+ const abs = join28(webDir, ...rel.split("/"));
222603
+ try {
222604
+ if (!statSync10(abs).isFile())
222605
+ throw new Error("not a file");
222606
+ } catch {
222607
+ return sendText(res, 404, "text/plain", "Not found");
222608
+ }
222609
+ const hashed = rel.startsWith("assets/");
222610
+ res.writeHead(200, { "content-type": contentType(rel), "cache-control": hashed ? "max-age=31536000, immutable" : "no-store" });
222611
+ createReadStream(abs).pipe(res);
222612
+ return;
222272
222613
  }
222273
222614
  if (route === "/version") {
222274
222615
  return sendJson(res, 200, { v });
@@ -222281,9 +222622,9 @@ var startDevServer = async (options) => {
222281
222622
  const name2 = route.slice("/shaders/".length);
222282
222623
  if (!isShaderArtifactName(name2))
222283
222624
  return sendText(res, 400, "text/plain", "Bad path");
222284
- const abs = join26(shadersCacheDir(root), name2);
222625
+ const abs = join28(shadersCacheDir(root), name2);
222285
222626
  try {
222286
- if (!statSync9(abs).isFile())
222627
+ if (!statSync10(abs).isFile())
222287
222628
  throw new Error("not a file");
222288
222629
  } catch {
222289
222630
  return sendText(res, 404, "text/plain", "Not found");
@@ -222300,7 +222641,7 @@ var startDevServer = async (options) => {
222300
222641
  return sendText(res, 400, "text/plain", "Bad path");
222301
222642
  }
222302
222643
  const abs = resolveLocalPath(root, key);
222303
- if (!abs || !statSync9(abs).isFile())
222644
+ if (!abs || !statSync10(abs).isFile())
222304
222645
  return sendText(res, 404, "text/plain", "Not found");
222305
222646
  res.writeHead(200, { "content-type": contentType(key), "cache-control": "no-store" });
222306
222647
  createReadStream(abs).pipe(res);
@@ -222343,17 +222684,18 @@ var startDevServer = async (options) => {
222343
222684
  if (conn)
222344
222685
  clients.add(conn);
222345
222686
  });
222346
- await new Promise((resolve11, reject) => {
222687
+ await new Promise((resolve12, reject) => {
222347
222688
  server2.once("error", reject);
222348
222689
  server2.listen(options.port, options.bind ?? "0.0.0.0", () => {
222349
222690
  server2.removeListener("error", reject);
222350
- resolve11();
222691
+ resolve12();
222351
222692
  });
222352
222693
  });
222353
222694
  const address = server2.address();
222354
222695
  const boundPort = typeof address === "object" && address !== null ? address.port : options.port;
222355
222696
  return {
222356
222697
  url: `${origin}${base}/dev.js`,
222698
+ webUrl: webDir ? `${origin}${base}/web/` : null,
222357
222699
  base,
222358
222700
  port: boundPort,
222359
222701
  v: () => v,
@@ -222367,6 +222709,147 @@ var startDevServer = async (options) => {
222367
222709
  };
222368
222710
  };
222369
222711
 
222712
+ // src/dev/webRunner.ts
222713
+ init_distRoot();
222714
+ import { existsSync as existsSync27 } from "node:fs";
222715
+ import { createRequire as createRequire6 } from "node:module";
222716
+ import { dirname as dirname15, join as join29 } from "node:path";
222717
+ var resolveWebRunnerDir = () => {
222718
+ const candidates = [join29(distRoot, "runtime", "web")];
222719
+ try {
222720
+ const pkg = createRequire6(import.meta.url).resolve("lecodes-viewer-lite/package.json");
222721
+ candidates.push(join29(dirname15(pkg), "dist-embed"));
222722
+ } catch {}
222723
+ for (const dir of candidates)
222724
+ if (existsSync27(join29(dir, "embed.html")) && existsSync27(join29(dir, "embed.js")))
222725
+ return dir;
222726
+ return null;
222727
+ };
222728
+
222729
+ // src/commands/dev.ts
222730
+ init_browserAuth();
222731
+
222732
+ // src/dev/androidDev.ts
222733
+ init_util();
222734
+ import { existsSync as existsSync28, writeFileSync as writeFileSync21 } from "node:fs";
222735
+ import { join as join30 } from "node:path";
222736
+ import { spawnSync as spawnSync7 } from "node:child_process";
222737
+ var DEFAULT_ANDROID_PACKAGE = "com.letary.lecodes";
222738
+ var adbCandidates = () => {
222739
+ const out = [];
222740
+ if (process.env.LECODES_ADB)
222741
+ out.push(process.env.LECODES_ADB);
222742
+ out.push("adb");
222743
+ const exe = process.platform === "win32" ? "adb.exe" : "adb";
222744
+ for (const home of [process.env.ANDROID_HOME, process.env.ANDROID_SDK_ROOT]) {
222745
+ if (home)
222746
+ out.push(join30(home, "platform-tools", exe));
222747
+ }
222748
+ if (process.platform === "win32" && process.env.LOCALAPPDATA)
222749
+ out.push(join30(process.env.LOCALAPPDATA, "Android", "Sdk", "platform-tools", exe));
222750
+ if (process.env.HOME)
222751
+ out.push(join30(process.env.HOME, "Android", "Sdk", "platform-tools", exe), join30(process.env.HOME, "Library", "Android", "sdk", "platform-tools", exe));
222752
+ return out;
222753
+ };
222754
+ var run2 = (adb, args, input) => {
222755
+ const r2 = spawnSync7(adb, args, { encoding: "utf8", input, windowsHide: true, maxBuffer: 64 * 1024 * 1024 });
222756
+ const out = `${r2.stdout ?? ""}${r2.stderr ?? ""}`.trim();
222757
+ return { ok: r2.status === 0, out };
222758
+ };
222759
+ var resolveAdb = () => {
222760
+ for (const cand of adbCandidates()) {
222761
+ if (cand !== "adb" && !existsSync28(cand))
222762
+ continue;
222763
+ const r2 = spawnSync7(cand, ["version"], { encoding: "utf8", windowsHide: true });
222764
+ if (r2.status === 0)
222765
+ return cand;
222766
+ }
222767
+ throw new CliError("adb not found — install Android platform-tools (or point LECODES_ADB at adb).");
222768
+ };
222769
+ var listDevices = (adb) => {
222770
+ const { out } = run2(adb, ["devices", "-l"]);
222771
+ const devices = [];
222772
+ for (const line of out.split(/\r?\n/).slice(1)) {
222773
+ const m = /^(\S+)\s+(\S+)(.*)$/.exec(line.trim());
222774
+ if (!m)
222775
+ continue;
222776
+ const model = /model:(\S+)/.exec(m[3] ?? "")?.[1];
222777
+ devices.push({ serial: m[1], state: m[2], model });
222778
+ }
222779
+ return devices;
222780
+ };
222781
+ var pickDevice = (adb, serial) => {
222782
+ const all = listDevices(adb);
222783
+ if (serial) {
222784
+ const d = all.find((x2) => x2.serial === serial);
222785
+ if (!d)
222786
+ throw new CliError(`No device "${serial}" — adb sees: ${all.map((x2) => `${x2.serial} (${x2.state})`).join(", ") || "nothing"}.`);
222787
+ if (d.state !== "device")
222788
+ throw new CliError(`Device ${serial} is "${d.state}" — accept the USB-debugging prompt on the phone.`);
222789
+ return serial;
222790
+ }
222791
+ const ready = all.filter((d) => d.state === "device");
222792
+ if (ready.length === 1)
222793
+ return ready[0].serial;
222794
+ if (ready.length === 0) {
222795
+ const other = all.map((d) => `${d.serial} (${d.state})`).join(", ");
222796
+ throw new CliError(other ? `No usable device: adb sees ${other}. "unauthorized" = accept the USB-debugging prompt on the phone; "offline" = replug.` : "No Android device: enable USB debugging (Developer options), plug the phone in, accept the prompt — or `adb connect <ip>:5555` for Wi-Fi debugging.");
222797
+ }
222798
+ throw new CliError(`Several devices: ${ready.map((d) => `${d.serial}${d.model ? ` (${d.model})` : ""}`).join(", ")} — pick one with --serial <id>.`);
222799
+ };
222800
+ var launchOnAndroid = (opts) => {
222801
+ const adb = resolveAdb();
222802
+ const serial = pickDevice(adb, opts.serial);
222803
+ const packageName = opts.packageName ?? process.env.LECODES_ANDROID_PACKAGE ?? DEFAULT_ANDROID_PACKAGE;
222804
+ const dev = (args) => run2(adb, ["-s", serial, ...args]);
222805
+ if (opts.apk) {
222806
+ if (!existsSync28(opts.apk))
222807
+ throw new CliError(`--apk: ${opts.apk} does not exist.`);
222808
+ note(`Installing ${opts.apk}…`);
222809
+ const r2 = dev(["install", "-r", opts.apk]);
222810
+ if (!r2.ok || !/Success/.test(r2.out)) {
222811
+ throw new CliError(`adb install failed: ${r2.out}${/USER_RESTRICTED/.test(r2.out) ? `
222812
+ (MIUI: enable "Install via USB" in Developer options.)` : ""}`);
222813
+ }
222814
+ }
222815
+ const installed = dev(["shell", "pm", "path", packageName]);
222816
+ if (!installed.ok || !installed.out.includes("package:")) {
222817
+ throw new CliError(`${packageName} is not installed on ${serial} — pass --apk <file> (lecodes-android: .\\gradlew :app:assembleDebug → app/build/outputs/apk/debug/app-debug.apk).`);
222818
+ }
222819
+ const rev = dev(["reverse", `tcp:${opts.port}`, `tcp:${opts.port}`]);
222820
+ if (!rev.ok)
222821
+ warnErr(`adb reverse failed (${rev.out}) — the phone must reach the LAN url instead.`);
222822
+ dev(["shell", "am", "force-stop", packageName]);
222823
+ dev(["logcat", "-c"]);
222824
+ const start = dev(["shell", "am", "start", "-n", `${packageName}/.MainActivity`, "-a", "android.intent.action.VIEW", "-d", opts.url]);
222825
+ if (!start.ok || /Error/.test(start.out))
222826
+ throw new CliError(`Could not launch ${packageName}: ${start.out}`);
222827
+ let stopped = false;
222828
+ return {
222829
+ serial,
222830
+ packageName,
222831
+ adb,
222832
+ screenshot: (file) => {
222833
+ const r2 = spawnSync7(adb, ["-s", serial, "exec-out", "screencap", "-p"], { windowsHide: true, maxBuffer: 64 * 1024 * 1024 });
222834
+ if (r2.status !== 0 || !r2.stdout || r2.stdout.length === 0)
222835
+ return false;
222836
+ writeFileSync21(file, r2.stdout);
222837
+ return true;
222838
+ },
222839
+ stop: () => {
222840
+ if (stopped)
222841
+ return;
222842
+ stopped = true;
222843
+ dev(["shell", "am", "force-stop", packageName]);
222844
+ dev(["reverse", "--remove", `tcp:${opts.port}`]);
222845
+ }
222846
+ };
222847
+ };
222848
+ var androidCrashLines = (session) => {
222849
+ const { out } = run2(session.adb, ["-s", session.serial, "logcat", "-d", "-v", "brief"]);
222850
+ return out.split(/\r?\n/).filter((l2) => /FATAL EXCEPTION|Fatal signal|\[crash\]/.test(l2) || /AndroidRuntime/.test(l2) && l2.includes(session.packageName)).slice(-20);
222851
+ };
222852
+
222370
222853
  // src/commands/dev.ts
222371
222854
  init_util();
222372
222855
  var DEFAULT_PORT2 = 4499;
@@ -222395,24 +222878,24 @@ var lanCandidates = () => {
222395
222878
  return out.sort((x2, y2) => x2.rank - y2.rank).map(({ address, iface }) => ({ address, iface }));
222396
222879
  };
222397
222880
  var loadDevToken = (root) => {
222398
- const path2 = join27(root, LECODES_DIR, "dev.json");
222881
+ const path2 = join31(root, LECODES_DIR, "dev.json");
222399
222882
  try {
222400
- const token2 = JSON.parse(readFileSync22(path2, "utf8")).token;
222883
+ const token2 = JSON.parse(readFileSync24(path2, "utf8")).token;
222401
222884
  if (token2 && /^[a-z0-9]{8,}$/.test(token2))
222402
222885
  return token2;
222403
222886
  } catch {}
222404
222887
  const token = randomBytes3(6).toString("hex");
222405
- mkdirSync19(join27(root, LECODES_DIR), { recursive: true });
222406
- writeFileSync20(path2, JSON.stringify({ token }, null, 2) + `
222888
+ mkdirSync20(join31(root, LECODES_DIR), { recursive: true });
222889
+ writeFileSync22(path2, JSON.stringify({ token }, null, 2) + `
222407
222890
  `);
222408
222891
  return token;
222409
222892
  };
222410
222893
  var printQr = async (url) => {
222411
222894
  try {
222412
222895
  const qrcode = (await Promise.resolve().then(() => __toESM(require_main(), 1))).default;
222413
- await new Promise((resolve11) => qrcode.generate(url, { small: true }, (out) => {
222896
+ await new Promise((resolve12) => qrcode.generate(url, { small: true }, (out) => {
222414
222897
  log(out);
222415
- resolve11();
222898
+ resolve12();
222416
222899
  }));
222417
222900
  } catch {
222418
222901
  note("(couldn't render a QR code — open the URL below on the device)");
@@ -222428,7 +222911,7 @@ var dev = async (args) => {
222428
222911
  root = process.cwd();
222429
222912
  manifest = null;
222430
222913
  }
222431
- const name = manifest?.name ?? basename11(root);
222914
+ const name = manifest?.name ?? basename12(root);
222432
222915
  if (typeof globalThis.Bun !== "undefined") {
222433
222916
  warnErr("Running under Bun: the websocket reload channel doesn't work here — run `lecodes dev` with Node (or use ?t=poll on the device URL).");
222434
222917
  }
@@ -222440,6 +222923,10 @@ var dev = async (args) => {
222440
222923
  const alternatives = candidates.filter((c4) => c4.address !== host);
222441
222924
  const origin = `http://${host}:${port}`;
222442
222925
  const token = loadDevToken(root);
222926
+ const webDir = resolveWebRunnerDir();
222927
+ if (flagBool(args, "web") && !webDir) {
222928
+ throw new CliError("No Web Lite runner found for --web: this CLI has no runtime/web (a monorepo checkout needs `bun run build:embed` in packages/viewer-lite).");
222929
+ }
222443
222930
  const server2 = await startDevServer({
222444
222931
  root,
222445
222932
  manifest,
@@ -222448,6 +222935,7 @@ var dev = async (args) => {
222448
222935
  port,
222449
222936
  token,
222450
222937
  entryOverride: flagStr(args, "entry"),
222938
+ webDir,
222451
222939
  log: note
222452
222940
  }).catch((e2) => {
222453
222941
  const msg = e2 instanceof Error ? e2.message : String(e2);
@@ -222481,6 +222969,7 @@ ${first.error}`);
222481
222969
  return true;
222482
222970
  };
222483
222971
  const debug2 = process.env.LECODES_DEV_DEBUG === "1";
222972
+ const underBun = typeof globalThis.Bun !== "undefined";
222484
222973
  let timer = null;
222485
222974
  const onChange = (file) => {
222486
222975
  if (debug2)
@@ -222516,14 +223005,63 @@ ${result.error}`);
222516
223005
  if (!flagBool(args, "no-qr"))
222517
223006
  await printQr(server2.url);
222518
223007
  log(`Scan the QR with the LeCodes app, or open: ${c.bold(server2.url)}`);
223008
+ const webLocal = server2.webUrl ? server2.webUrl.replace(`http://${host}:`, "http://localhost:") : null;
223009
+ if (server2.webUrl && webLocal)
223010
+ log(`In the browser (Web Lite): ${c.bold(webLocal)} (LAN: ${server2.webUrl})`);
222519
223011
  note(`Older desktop builds (no websocket bridge): ${server2.url}?t=poll`);
222520
223012
  if (alternatives.length > 0) {
222521
223013
  note(`Device can't reach it? This machine also has ${alternatives.map((a2) => `${a2.address} (${a2.iface})`).join(", ")} — retry with --host <ip>.`);
222522
223014
  }
222523
- if (existsSync25(join27(root, LECODES_DIR, "manifest.json")))
223015
+ if (existsSync29(join31(root, LECODES_DIR, "manifest.json")))
222524
223016
  note(`Token stored in ${LECODES_DIR}/dev.json (delete it to rotate the URL).`);
222525
223017
  log("");
222526
223018
  log(`Watching ${root} — save a file to hot-reload. Ctrl+C to stop.`);
223019
+ if (flagBool(args, "web") && webLocal) {
223020
+ note("Opening the browser…");
223021
+ openBrowser(webLocal);
223022
+ }
223023
+ if (flagBool(args, "android")) {
223024
+ const seconds = Number(flagStr(args, "time")) || 0;
223025
+ const png2 = flagStr(args, "png");
223026
+ const deviceUrl = `http://127.0.0.1:${port}${server2.url.slice(origin.length)}`.replace(/\/dev\.js$/, underBun ? "/dev-poll.js" : "/dev.js");
223027
+ let session;
223028
+ try {
223029
+ session = launchOnAndroid({ port, url: deviceUrl, serial: flagStr(args, "serial"), apk: flagStr(args, "apk"), packageName: flagStr(args, "package") });
223030
+ } catch (e2) {
223031
+ server2.close();
223032
+ watcher?.close();
223033
+ throw e2;
223034
+ }
223035
+ note(`Android: ${session.serial} → ${deviceUrl} (adb reverse; device logs stream here as [device:log])`);
223036
+ const finish = (code) => {
223037
+ session.stop();
223038
+ server2.close();
223039
+ watcher?.close();
223040
+ process.exit(code);
223041
+ };
223042
+ process.on("SIGINT", () => {
223043
+ note("Stopping the app on the phone…");
223044
+ finish(0);
223045
+ });
223046
+ if (seconds > 0) {
223047
+ setTimeout(() => {
223048
+ const crashes = androidCrashLines(session);
223049
+ if (png2) {
223050
+ if (session.screenshot(png2))
223051
+ note(`Wrote ${png2} (device screenshot after ${seconds} s)`);
223052
+ else
223053
+ warnErr(`Screenshot failed (adb screencap).`);
223054
+ }
223055
+ if (crashes.length > 0) {
223056
+ warnErr(`The app crashed on the phone:
223057
+ ${crashes.join(`
223058
+ `)}`);
223059
+ finish(1);
223060
+ }
223061
+ finish(0);
223062
+ }, seconds * 1000);
223063
+ }
223064
+ }
222527
223065
  if (flagBool(args, "desktop")) {
222528
223066
  const appCfg = readAppConfigAt(root);
222529
223067
  const win = appCfg?.desktop?.window;
@@ -222549,8 +223087,8 @@ init_localFiles();
222549
223087
 
222550
223088
  // src/serverDiff.ts
222551
223089
  init_localFiles();
222552
- import { readFileSync as readFileSync23 } from "node:fs";
222553
- import { basename as basename12 } from "node:path";
223090
+ import { readFileSync as readFileSync25 } from "node:fs";
223091
+ import { basename as basename13 } from "node:path";
222554
223092
  var isTextAsset = (t2) => t2 === "text" || t2 === "shader" || t2 === "scene";
222555
223093
  var computeServerDiff = (project, scan2) => {
222556
223094
  const pathById = buildPaths(project.assets);
@@ -222567,8 +223105,8 @@ var computeServerDiff = (project, scan2) => {
222567
223105
  const server2 = serverByPath.get(path2);
222568
223106
  const local = localByPath.get(path2);
222569
223107
  if (local && !server2) {
222570
- const text = isTextKind(classifyKind(basename12(path2)));
222571
- entries.push({ path: path2, status: "added", binary: !text, serverText: "", localText: text ? readFileSync23(local.absPath, "utf8") : undefined });
223108
+ const text = isTextKind(classifyKind(basename13(path2)));
223109
+ entries.push({ path: path2, status: "added", binary: !text, serverText: "", localText: text ? readFileSync25(local.absPath, "utf8") : undefined });
222572
223110
  continue;
222573
223111
  }
222574
223112
  if (server2 && !local) {
@@ -222579,12 +223117,12 @@ var computeServerDiff = (project, scan2) => {
222579
223117
  if (server2 && local) {
222580
223118
  if (isTextAsset(server2.type)) {
222581
223119
  const serverText = server2.text ?? "";
222582
- const localText = readFileSync23(local.absPath, "utf8");
223120
+ const localText = readFileSync25(local.absPath, "utf8");
222583
223121
  if (serverText !== localText)
222584
223122
  entries.push({ path: path2, status: "modified", binary: false, serverText, localText });
222585
223123
  } else {
222586
223124
  const serverSize = server2.file?.size;
222587
- const localSize = Buffer.byteLength(readFileSync23(local.absPath));
223125
+ const localSize = Buffer.byteLength(readFileSync25(local.absPath));
222588
223126
  if (serverSize !== undefined && serverSize !== localSize) {
222589
223127
  entries.push({ path: path2, status: "modified", binary: true });
222590
223128
  }
@@ -222742,8 +223280,8 @@ var statusLabel = (e2) => e2.status === "added" ? c.green(" added ") : e2.st
222742
223280
  init_manifest();
222743
223281
  init_types4();
222744
223282
  init_ignore();
222745
- import { existsSync as existsSync26, mkdirSync as mkdirSync20, writeFileSync as writeFileSync21 } from "node:fs";
222746
- import { basename as basename13, dirname as dirname14, join as join28, resolve as resolve11 } from "node:path";
223283
+ import { existsSync as existsSync30, mkdirSync as mkdirSync21, writeFileSync as writeFileSync23 } from "node:fs";
223284
+ import { basename as basename14, dirname as dirname16, join as join32, resolve as resolve12 } from "node:path";
222747
223285
  init_util();
222748
223286
  var STARTER2 = `// Welcome to LeCodes — the whole SDK is available as globals, no imports needed.
222749
223287
  // Run it live on a device with \`lecodes dev\`, or bundle it with \`lecodes compile -o app.js\`.
@@ -222851,18 +223389,18 @@ Full command list: \`lecodes --help\`.
222851
223389
  `;
222852
223390
  var init2 = async (args) => {
222853
223391
  const dirArg = (args._[0] ?? ".").replace(/[/\\]+$/, "") || ".";
222854
- const root = resolve11(dirArg);
222855
- const name = basename13(root);
222856
- mkdirSync20(root, { recursive: true });
223392
+ const root = resolve12(dirArg);
223393
+ const name = basename14(root);
223394
+ mkdirSync21(root, { recursive: true });
222857
223395
  let created = 0;
222858
223396
  const write = (rel, content) => {
222859
- const abs = join28(root, rel);
222860
- if (existsSync26(abs)) {
223397
+ const abs = join32(root, rel);
223398
+ if (existsSync30(abs)) {
222861
223399
  note(`kept ${rel}`);
222862
223400
  return;
222863
223401
  }
222864
- mkdirSync20(dirname14(abs), { recursive: true });
222865
- writeFileSync21(abs, content);
223402
+ mkdirSync21(dirname16(abs), { recursive: true });
223403
+ writeFileSync23(abs, content);
222866
223404
  log(`${c.green("created")} ${rel}`);
222867
223405
  created++;
222868
223406
  };
@@ -222909,8 +223447,8 @@ init_config();
222909
223447
  init_ignore();
222910
223448
  init_localFiles();
222911
223449
  init_manifest();
222912
- import { existsSync as existsSync27 } from "node:fs";
222913
- import { basename as basename15, join as join29, resolve as resolve12 } from "node:path";
223450
+ import { existsSync as existsSync31 } from "node:fs";
223451
+ import { basename as basename16, join as join33, resolve as resolve13 } from "node:path";
222914
223452
  init_types4();
222915
223453
  init_util();
222916
223454
 
@@ -222919,8 +223457,8 @@ init_api();
222919
223457
  init_config();
222920
223458
  init_manifest();
222921
223459
  init_localFiles();
222922
- import { basename as basename14 } from "node:path";
222923
- import { readFileSync as readFileSync24 } from "node:fs";
223460
+ import { basename as basename15 } from "node:path";
223461
+ import { readFileSync as readFileSync26 } from "node:fs";
222924
223462
  init_util();
222925
223463
  var push = async (args) => {
222926
223464
  const root = findProjectRoot(process.cwd());
@@ -222976,14 +223514,14 @@ var push = async (args) => {
222976
223514
  let uploaded = 0;
222977
223515
  for (const file of scan2.files) {
222978
223516
  const source = sourceFor(file.path);
222979
- const kind = source?.type ?? classifyKind(basename14(file.path));
223517
+ const kind = source?.type ?? classifyKind(basename15(file.path));
222980
223518
  const entry = { path: file.path, type: kind };
222981
223519
  if (source)
222982
223520
  entry.assetId = source.id;
222983
223521
  if (isTextKind(kind)) {
222984
- entry.text = readFileSync24(file.absPath, "utf8");
223522
+ entry.text = readFileSync26(file.absPath, "utf8");
222985
223523
  } else if (!source || source.sha !== file.sha) {
222986
- const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename14(file.path), readFileSync24(file.absPath));
223524
+ const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename15(file.path), readFileSync26(file.absPath));
222987
223525
  entry.blobFileId = blob.id;
222988
223526
  uploaded++;
222989
223527
  }
@@ -223090,10 +223628,10 @@ var link = async (args) => {
223090
223628
  const apiUrl = requireApiUrl(config);
223091
223629
  const token = requireToken(config);
223092
223630
  const dirArg = args._[0];
223093
- const root = dirArg ? resolve12(dirArg) : process.cwd();
223094
- if (!existsSync27(root))
223631
+ const root = dirArg ? resolve13(dirArg) : process.cwd();
223632
+ if (!existsSync31(root))
223095
223633
  throw new CliError(`No such directory: ${root}`);
223096
- if (existsSync27(join29(root, LECODES_DIR, "manifest.json")) && !flagBool(args, "force")) {
223634
+ if (existsSync31(join33(root, LECODES_DIR, "manifest.json")) && !flagBool(args, "force")) {
223097
223635
  throw new CliError("This folder is already linked (.lecodes/manifest.json exists) — use `lecodes push`, or `lecodes link --force` to re-link it to another project.");
223098
223636
  }
223099
223637
  let enclosing = null;
@@ -223120,7 +223658,7 @@ var link = async (args) => {
223120
223658
  }
223121
223659
  name = project.name;
223122
223660
  } else {
223123
- name = flagStr(args, "name") ?? basename15(root);
223661
+ name = flagStr(args, "name") ?? basename16(root);
223124
223662
  info(c.dim(`Creating project ${c.bold(name)}…`));
223125
223663
  uuid = (await createProject(apiUrl, token, name, flagStr(args, "team"))).uuid;
223126
223664
  files = {};
@@ -223157,10 +223695,10 @@ var link = async (args) => {
223157
223695
 
223158
223696
  // src/commands/lightmap.ts
223159
223697
  init_manifest();
223160
- import { existsSync as existsSync28, mkdirSync as mkdirSync21, mkdtempSync as mkdtempSync4, readFileSync as readFileSync25, renameSync as renameSync3, rmSync as rmSync9, statSync as statSync10, unlinkSync, writeFileSync as writeFileSync22 } from "node:fs";
223161
- import { spawn as spawn6, spawnSync as spawnSync6 } from "node:child_process";
223698
+ import { existsSync as existsSync32, mkdirSync as mkdirSync22, mkdtempSync as mkdtempSync4, readFileSync as readFileSync27, renameSync as renameSync3, rmSync as rmSync9, statSync as statSync11, unlinkSync, writeFileSync as writeFileSync24 } from "node:fs";
223699
+ import { spawn as spawn6, spawnSync as spawnSync8 } from "node:child_process";
223162
223700
  import { tmpdir as tmpdir4 } from "node:os";
223163
- import { join as join30, relative as relative4, resolve as resolve13 } from "node:path";
223701
+ import { join as join34, relative as relative4, resolve as resolve14 } from "node:path";
223164
223702
  init_util();
223165
223703
  var num = (args, key, fallback, min, max) => {
223166
223704
  const raw = flagStr(args, key);
@@ -223173,20 +223711,20 @@ var num = (args, key, fallback, min, max) => {
223173
223711
  };
223174
223712
  var findToktx = () => {
223175
223713
  const env = process.env.KTX_TOKTX;
223176
- if (env && existsSync28(env))
223714
+ if (env && existsSync32(env))
223177
223715
  return env;
223178
- for (const base of [process.env.LOCALAPPDATA && join30(process.env.LOCALAPPDATA, "Programs"), process.env.ProgramFiles]) {
223716
+ for (const base of [process.env.LOCALAPPDATA && join34(process.env.LOCALAPPDATA, "Programs"), process.env.ProgramFiles]) {
223179
223717
  if (!base)
223180
223718
  continue;
223181
- const guess = join30(base, "KTX-Software", "bin", "toktx.exe");
223182
- if (existsSync28(guess))
223719
+ const guess = join34(base, "KTX-Software", "bin", "toktx.exe");
223720
+ if (existsSync32(guess))
223183
223721
  return guess;
223184
223722
  }
223185
223723
  return "toktx";
223186
223724
  };
223187
223725
  var checkToktx = (toktx) => {
223188
223726
  try {
223189
- const p2 = spawnSync6(toktx, ["--version"], { encoding: "utf8" });
223727
+ const p2 = spawnSync8(toktx, ["--version"], { encoding: "utf8" });
223190
223728
  if (p2.status === 0)
223191
223729
  return (p2.stdout ?? "").toString().trim() || "toktx";
223192
223730
  } catch {}
@@ -223200,7 +223738,7 @@ var requireToktx = () => {
223200
223738
  }
223201
223739
  return toktx;
223202
223740
  };
223203
- var encodeKtx2 = (toktx, png2, ktx2) => new Promise((resolve14, reject) => {
223741
+ var encodeKtx2 = (toktx, png2, ktx2) => new Promise((resolve15, reject) => {
223204
223742
  const args = ["--t2", "--encode", "uastc", "--uastc_quality", "2", "--zcmp", "18", "--genmipmap", "--assign_oetf", "srgb", ktx2, png2];
223205
223743
  const p2 = spawn6(toktx, args, { stdio: ["ignore", "pipe", "pipe"] });
223206
223744
  let out = "";
@@ -223213,7 +223751,7 @@ var encodeKtx2 = (toktx, png2, ktx2) => new Promise((resolve14, reject) => {
223213
223751
  p2.on("error", (e2) => reject(new CliError(`toktx failed (${e2.message}) while encoding ${ktx2}`)));
223214
223752
  p2.on("close", (status2) => {
223215
223753
  if (status2 === 0)
223216
- resolve14();
223754
+ resolve15();
223217
223755
  else
223218
223756
  reject(new CliError(`toktx failed (${status2 ?? "?"}) while encoding ${ktx2}:
223219
223757
  ${out.trim()}`));
@@ -223228,13 +223766,13 @@ var bake = async (args) => {
223228
223766
  root = process.cwd();
223229
223767
  }
223230
223768
  const toktx = requireToktx();
223231
- const outDir = resolve13(root, flagStr(args, "out") ?? "assets/lightmap");
223769
+ const outDir = resolve14(root, flagStr(args, "out") ?? "assets/lightmap");
223232
223770
  const stem = flagStr(args, "name") ?? "lightmap";
223233
223771
  if (!/^[A-Za-z0-9_-]+$/.test(stem))
223234
223772
  throw new CliError(`--name wants a plain file stem (letters, digits, - and _), got "${stem}".`);
223235
223773
  const entryFlag = flagStr(args, "entry");
223236
223774
  const entryFile = entryFlag ?? "main.ts";
223237
- if (!existsSync28(resolve13(root, entryFile))) {
223775
+ if (!existsSync32(resolve14(root, entryFile))) {
223238
223776
  throw new CliError(`No ${entryFile} in ${root} — pass --entry <file> (the file that builds the scene and calls Lightmap.load).`);
223239
223777
  }
223240
223778
  const cfg = {
@@ -223266,7 +223804,7 @@ var bake = async (args) => {
223266
223804
  const entryText = `globalThis.__lecodesLightmap = ${JSON.stringify(cfg)}
223267
223805
  import ${JSON.stringify(importPath)}
223268
223806
  `;
223269
- mkdirSync21(outDir, { recursive: true });
223807
+ mkdirSync22(outDir, { recursive: true });
223270
223808
  ensurePlaceholders(outDir, stem);
223271
223809
  const desktopRenderer = resolveDesktopRenderer(root, args);
223272
223810
  const { js, resources } = await buildBundle(args, undefined, {
@@ -223275,8 +223813,8 @@ import ${JSON.stringify(importPath)}
223275
223813
  extraEntries: [{ path: entryPath, type: "text", text: entryText }],
223276
223814
  entryOverride: entryPath
223277
223815
  });
223278
- const scratch = mkdtempSync4(join30(tmpdir4(), "lecodes-lightmap-"));
223279
- const outPng = join30(scratch, "frame.png");
223816
+ const scratch = mkdtempSync4(join34(tmpdir4(), "lecodes-lightmap-"));
223817
+ const outPng = join34(scratch, "frame.png");
223280
223818
  note(`Baking up to ${cfg.pages} × ${cfg.size}² at ${(cfg.texel * 100).toFixed(1)} cm texels (${cfg.sunRays} sun + ${cfg.aoRays} AO + ${cfg.lightRays} light rays)${cfg.volumeCell ? ` + a ${cfg.volumeCell} m light volume` : ""} → ${relative4(root, outDir) || "."}/${stem}.*…`);
223281
223819
  let output = "";
223282
223820
  try {
@@ -223310,22 +223848,22 @@ import ${JSON.stringify(importPath)}
223310
223848
  logErr(text.startsWith("error") ? c.red(`✗ ${text}`) : text.startsWith("warning") ? c.yellow(` ${text}`) : c.dim(` ${text}`));
223311
223849
  }
223312
223850
  if (/\[lightmap\] done/.test(output)) {
223313
- const bakePath = join30(outDir, "lightmap.bake");
223314
- if (!existsSync28(bakePath))
223851
+ const bakePath = join34(outDir, "lightmap.bake");
223852
+ if (!existsSync32(bakePath))
223315
223853
  throw new CliError(`The host reported a bake but ${bakePath} is missing.`);
223316
- const data = JSON.parse(readFileSync25(bakePath, "utf8"));
223854
+ const data = JSON.parse(readFileSync27(bakePath, "utf8"));
223317
223855
  const pages = Math.max(1, data.pages ?? 1);
223318
223856
  const hasLight = !!data.lightTextures?.length;
223319
223857
  const t0 = Date.now();
223320
223858
  let pngKb = 0, ktxKb = 0;
223321
223859
  const encodePage = async (page, light) => {
223322
- const png2 = join30(outDir, pageFile("lightmap", page, light, ".png"));
223323
- const ktx2 = join30(outDir, pageFile(stem, page, light));
223324
- if (!existsSync28(png2))
223860
+ const png2 = join34(outDir, pageFile("lightmap", page, light, ".png"));
223861
+ const ktx2 = join34(outDir, pageFile(stem, page, light));
223862
+ if (!existsSync32(png2))
223325
223863
  throw new CliError(`The host reported a bake but ${png2} is missing.`);
223326
223864
  await encodeKtx2(toktx, png2, ktx2);
223327
- pngKb += statSync10(png2).size / 1024;
223328
- ktxKb += statSync10(ktx2).size / 1024;
223865
+ pngKb += statSync11(png2).size / 1024;
223866
+ ktxKb += statSync11(ktx2).size / 1024;
223329
223867
  unlinkSync(png2);
223330
223868
  };
223331
223869
  const encodes = [];
@@ -223337,15 +223875,15 @@ import ${JSON.stringify(importPath)}
223337
223875
  await Promise.all(encodes);
223338
223876
  for (let p2 = pages;p2 < 16; p2++) {
223339
223877
  for (const light of [false, true]) {
223340
- const stale = join30(outDir, pageFile(stem, p2, light));
223341
- if (existsSync28(stale) && statSync10(stale).size !== PLACEHOLDER_KTX2.length)
223342
- writeFileSync22(stale, PLACEHOLDER_KTX2);
223878
+ const stale = join34(outDir, pageFile(stem, p2, light));
223879
+ if (existsSync32(stale) && statSync11(stale).size !== PLACEHOLDER_KTX2.length)
223880
+ writeFileSync24(stale, PLACEHOLDER_KTX2);
223343
223881
  }
223344
223882
  }
223345
223883
  if (!hasLight) {
223346
- const light = join30(outDir, pageFile(stem, 0, true));
223347
- if (existsSync28(light) && statSync10(light).size !== PLACEHOLDER_KTX2.length)
223348
- writeFileSync22(light, PLACEHOLDER_KTX2);
223884
+ const light = join34(outDir, pageFile(stem, 0, true));
223885
+ if (existsSync32(light) && statSync11(light).size !== PLACEHOLDER_KTX2.length)
223886
+ writeFileSync24(light, PLACEHOLDER_KTX2);
223349
223887
  }
223350
223888
  data.texture = pageFile(stem, 0, false);
223351
223889
  data.textures = Array.from({ length: pages }, (_, p2) => pageFile(stem, p2, false));
@@ -223353,19 +223891,19 @@ import ${JSON.stringify(importPath)}
223353
223891
  data.lightTextures = Array.from({ length: pages }, (_, p2) => pageFile(stem, p2, true));
223354
223892
  if (data.volume)
223355
223893
  data.volume.file = `${stem}.volume`;
223356
- writeFileSync22(join30(outDir, `${stem}.bake`), JSON.stringify(data, null, 1) + `
223894
+ writeFileSync24(join34(outDir, `${stem}.bake`), JSON.stringify(data, null, 1) + `
223357
223895
  `);
223358
223896
  if (stem !== "lightmap")
223359
223897
  unlinkSync(bakePath);
223360
- const volumeSrc = join30(outDir, "lightmap.volume");
223361
- const hasVolume = cfg.volumeCell > 0 && existsSync28(volumeSrc);
223898
+ const volumeSrc = join34(outDir, "lightmap.volume");
223899
+ const hasVolume = cfg.volumeCell > 0 && existsSync32(volumeSrc);
223362
223900
  if (hasVolume && stem !== "lightmap") {
223363
- rmSync9(join30(outDir, `${stem}.volume`), { force: true });
223364
- renameSync3(volumeSrc, join30(outDir, `${stem}.volume`));
223901
+ rmSync9(join34(outDir, `${stem}.volume`), { force: true });
223902
+ renameSync3(volumeSrc, join34(outDir, `${stem}.volume`));
223365
223903
  }
223366
223904
  logErr(c.dim(` encoded ${pages} page(s)${hasLight ? " + the light atlas" : ""} (UASTC + mips, ${Math.round(ktxKb)} KB from ${Math.round(pngKb)} KB of rasters) in ${((Date.now() - t0) / 1000).toFixed(1)} s`));
223367
223905
  logErr(c.green(`✓ baked → ${relative4(root, outDir) || "."}/${stem}.bake + ${data.textures.join(", ")}${hasLight ? ` + ${data.lightTextures.join(", ")}` : ""}${hasVolume ? ` + ${stem}.volume` : ""}`));
223368
- const rel = (f3) => relative4(root, join30(outDir, f3)).replace(/\\/g, "/");
223906
+ const rel = (f3) => relative4(root, join34(outDir, f3)).replace(/\\/g, "/");
223369
223907
  const assets = (files) => files.length === 1 ? `asset('<rel>/${rel(files[0])}')` : `[${files.map((f3) => `asset('<rel>/${rel(f3)}')`).join(", ")}]`;
223370
223908
  logErr(c.dim(` scene file: env.lightmap: { data: asset('<rel>/${rel(`${stem}.bake`)}'), texture: ${assets(data.textures)}${hasLight ? `, light: ${assets(data.lightTextures)}` : ""}${hasVolume ? `, volume: asset('<rel>/${rel(`${stem}.volume`)}')` : ""} } — asset() paths are relative to the scene file`));
223371
223909
  return;
@@ -223395,20 +223933,20 @@ var placeholderVolume = () => {
223395
223933
  return b;
223396
223934
  };
223397
223935
  var ensurePlaceholders = (outDir, stem) => {
223398
- const missing = (f3) => !existsSync28(f3) || statSync10(f3).size === 0;
223399
- const bake2 = join30(outDir, `${stem}.bake`);
223400
- const ktx2 = join30(outDir, pageFile(stem, 0, false));
223401
- const light = join30(outDir, pageFile(stem, 0, true));
223402
- const volume = join30(outDir, `${stem}.volume`);
223936
+ const missing = (f3) => !existsSync32(f3) || statSync11(f3).size === 0;
223937
+ const bake2 = join34(outDir, `${stem}.bake`);
223938
+ const ktx2 = join34(outDir, pageFile(stem, 0, false));
223939
+ const light = join34(outDir, pageFile(stem, 0, true));
223940
+ const volume = join34(outDir, `${stem}.volume`);
223403
223941
  if (missing(bake2))
223404
- writeFileSync22(bake2, JSON.stringify({ version: 2, placeholder: true, size: 1, texel: 0, pages: 1, instances: [] }) + `
223942
+ writeFileSync24(bake2, JSON.stringify({ version: 2, placeholder: true, size: 1, texel: 0, pages: 1, instances: [] }) + `
223405
223943
  `);
223406
223944
  if (missing(ktx2))
223407
- writeFileSync22(ktx2, PLACEHOLDER_KTX2);
223945
+ writeFileSync24(ktx2, PLACEHOLDER_KTX2);
223408
223946
  if (missing(light))
223409
- writeFileSync22(light, PLACEHOLDER_KTX2);
223947
+ writeFileSync24(light, PLACEHOLDER_KTX2);
223410
223948
  if (missing(volume))
223411
- writeFileSync22(volume, placeholderVolume());
223949
+ writeFileSync24(volume, placeholderVolume());
223412
223950
  };
223413
223951
  var lightmap = async (args) => {
223414
223952
  const sub = args._[0];
@@ -223419,9 +223957,9 @@ var lightmap = async (args) => {
223419
223957
 
223420
223958
  // src/commands/navmesh.ts
223421
223959
  init_manifest();
223422
- import { existsSync as existsSync29, mkdirSync as mkdirSync22, mkdtempSync as mkdtempSync5, readdirSync as readdirSync14, readFileSync as readFileSync26, rmSync as rmSync10, statSync as statSync11, unlinkSync as unlinkSync2, writeFileSync as writeFileSync23 } from "node:fs";
223960
+ import { existsSync as existsSync33, mkdirSync as mkdirSync23, mkdtempSync as mkdtempSync5, readdirSync as readdirSync15, readFileSync as readFileSync28, rmSync as rmSync10, statSync as statSync12, unlinkSync as unlinkSync2, writeFileSync as writeFileSync25 } from "node:fs";
223423
223961
  import { tmpdir as tmpdir5 } from "node:os";
223424
- import { basename as basename16, join as join31, relative as relative5, resolve as resolve14 } from "node:path";
223962
+ import { basename as basename17, join as join35, relative as relative5, resolve as resolve15 } from "node:path";
223425
223963
  init_util();
223426
223964
 
223427
223965
  // ../creator-nav/dist/creatorNav.js
@@ -223430,16 +223968,16 @@ var createCreatorNav = (() => {
223430
223968
  var moduleRtn;
223431
223969
  var Module = moduleArg;
223432
223970
  var readyPromiseResolve, readyPromiseReject;
223433
- var readyPromise = new Promise((resolve14, reject) => {
223434
- readyPromiseResolve = resolve14;
223971
+ var readyPromise = new Promise((resolve15, reject) => {
223972
+ readyPromiseResolve = resolve15;
223435
223973
  readyPromiseReject = reject;
223436
223974
  });
223437
223975
  var ENVIRONMENT_IS_WEB = typeof window == "object";
223438
223976
  var ENVIRONMENT_IS_WORKER = typeof WorkerGlobalScope != "undefined";
223439
223977
  var ENVIRONMENT_IS_NODE = typeof process == "object" && process.versions?.node && process.type != "renderer";
223440
223978
  if (ENVIRONMENT_IS_NODE) {
223441
- const { createRequire: createRequire5 } = await import("module");
223442
- var require2 = createRequire5(import.meta.url);
223979
+ const { createRequire: createRequire7 } = await import("module");
223980
+ var require2 = createRequire7(import.meta.url);
223443
223981
  }
223444
223982
  var arguments_ = [];
223445
223983
  var thisProgram = "./this.program";
@@ -223609,9 +224147,9 @@ var createCreatorNav = (() => {
223609
224147
  }
223610
224148
  var info2 = getWasmImports();
223611
224149
  if (Module["instantiateWasm"]) {
223612
- return new Promise((resolve14, reject) => {
224150
+ return new Promise((resolve15, reject) => {
223613
224151
  Module["instantiateWasm"](info2, (mod, inst) => {
223614
- resolve14(receiveInstance(mod, inst));
224152
+ resolve15(receiveInstance(mod, inst));
223615
224153
  });
223616
224154
  });
223617
224155
  }
@@ -223967,14 +224505,14 @@ var createCreatorNav = (() => {
223967
224505
  var __emscripten_stack_restore = wasmExports["_emscripten_stack_restore"];
223968
224506
  var __emscripten_stack_alloc = wasmExports["_emscripten_stack_alloc"];
223969
224507
  var _emscripten_stack_get_current = wasmExports["emscripten_stack_get_current"];
223970
- function run2() {
224508
+ function run3() {
223971
224509
  if (runDependencies > 0) {
223972
- dependenciesFulfilled = run2;
224510
+ dependenciesFulfilled = run3;
223973
224511
  return;
223974
224512
  }
223975
224513
  preRun();
223976
224514
  if (runDependencies > 0) {
223977
- dependenciesFulfilled = run2;
224515
+ dependenciesFulfilled = run3;
223978
224516
  return;
223979
224517
  }
223980
224518
  function doRun() {
@@ -224006,7 +224544,7 @@ var createCreatorNav = (() => {
224006
224544
  }
224007
224545
  }
224008
224546
  preInit();
224009
- run2();
224547
+ run3();
224010
224548
  moduleRtn = readyPromise;
224011
224549
  return moduleRtn;
224012
224550
  };
@@ -224218,13 +224756,13 @@ var num2 = (args, key, fallback, min, max) => {
224218
224756
  return n2;
224219
224757
  };
224220
224758
  var guessName = (root) => {
224221
- const dirs = [join31(root, "src"), root];
224759
+ const dirs = [join35(root, "src"), root];
224222
224760
  for (const dir of dirs) {
224223
- if (!existsSync29(dir))
224761
+ if (!existsSync33(dir))
224224
224762
  continue;
224225
224763
  let entries;
224226
224764
  try {
224227
- entries = readdirSync14(dir);
224765
+ entries = readdirSync15(dir);
224228
224766
  } catch {
224229
224767
  continue;
224230
224768
  }
@@ -224232,7 +224770,7 @@ var guessName = (root) => {
224232
224770
  if (!f3.endsWith(".scene.ts"))
224233
224771
  continue;
224234
224772
  try {
224235
- if (/\bnavmesh\s*:/.test(readFileSync26(join31(dir, f3), "utf8")))
224773
+ if (/\bnavmesh\s*:/.test(readFileSync28(join35(dir, f3), "utf8")))
224236
224774
  return f3.slice(0, -".scene.ts".length);
224237
224775
  } catch {}
224238
224776
  }
@@ -224261,7 +224799,7 @@ var writeObj = (path2, tris) => {
224261
224799
  lines.push(`v ${tris[i2].toFixed(4)} ${tris[i2 + 1].toFixed(4)} ${tris[i2 + 2].toFixed(4)}`);
224262
224800
  for (let i2 = 0;i2 < tris.length / 3; i2 += 3)
224263
224801
  lines.push(`f ${i2 + 1} ${i2 + 2} ${i2 + 3}`);
224264
- writeFileSync23(path2, lines.join(`
224802
+ writeFileSync25(path2, lines.join(`
224265
224803
  `) + `
224266
224804
  `);
224267
224805
  };
@@ -224272,10 +224810,10 @@ var bake2 = async (args) => {
224272
224810
  } catch {
224273
224811
  root = process.cwd();
224274
224812
  }
224275
- const outDir = resolve14(root, flagStr(args, "out") ?? "assets/nav");
224813
+ const outDir = resolve15(root, flagStr(args, "out") ?? "assets/nav");
224276
224814
  const entryFlag = flagStr(args, "entry");
224277
224815
  const entryFile = entryFlag ?? "main.ts";
224278
- if (!existsSync29(resolve14(root, entryFile))) {
224816
+ if (!existsSync33(resolve15(root, entryFile))) {
224279
224817
  throw new CliError(`No ${entryFile} in ${root} — pass --entry <file> (the file that builds the level: a scene file with env.navmesh, or code calling NavMesh.load).`);
224280
224818
  }
224281
224819
  const name = flagStr(args, "name") ?? guessName(root) ?? "scene";
@@ -224286,10 +224824,10 @@ var bake2 = async (args) => {
224286
224824
  const logs = flagBool(args, "logs");
224287
224825
  const keepGeometry = flagBool(args, "keep-geometry");
224288
224826
  const obj = flagBool(args, "obj");
224289
- mkdirSync22(outDir, { recursive: true });
224290
- const navmeshPath = join31(outDir, `${name}.navmesh`);
224291
- if (!existsSync29(navmeshPath))
224292
- writeFileSync23(navmeshPath, placeholderLnav());
224827
+ mkdirSync23(outDir, { recursive: true });
224828
+ const navmeshPath = join35(outDir, `${name}.navmesh`);
224829
+ if (!existsSync33(navmeshPath))
224830
+ writeFileSync25(navmeshPath, placeholderLnav());
224293
224831
  const importPath = "./" + entryFile.replace(/\\/g, "/").replace(/\.tsx?$/, "");
224294
224832
  const entryPath = "/.navmesh-entry.ts";
224295
224833
  const cfg = { outDir: outDir.replace(/\\/g, "/"), name };
@@ -224303,8 +224841,8 @@ import ${JSON.stringify(importPath)}
224303
224841
  extraEntries: [{ path: entryPath, type: "text", text: entryText }],
224304
224842
  entryOverride: entryPath
224305
224843
  });
224306
- const scratch = mkdtempSync5(join31(tmpdir5(), "lecodes-navmesh-"));
224307
- const outPng = join31(scratch, "frame.png");
224844
+ const scratch = mkdtempSync5(join35(tmpdir5(), "lecodes-navmesh-"));
224845
+ const outPng = join35(scratch, "frame.png");
224308
224846
  note(`Dumping the level's static collision geometry through the desktop host → ${relative5(root, outDir) || "."}/${name}.navgeo…`);
224309
224847
  let output = "";
224310
224848
  try {
@@ -224357,10 +224895,10 @@ import ${JSON.stringify(importPath)}
224357
224895
  }
224358
224896
  if (!config)
224359
224897
  throw new CliError("The host reported a dump but no `[navmesh] config` line — SDK/host mismatch?");
224360
- const geoPath = join31(outDir, `${name}.navgeo`);
224361
- if (!existsSync29(geoPath))
224898
+ const geoPath = join35(outDir, `${name}.navgeo`);
224899
+ if (!existsSync33(geoPath))
224362
224900
  throw new CliError(`The host reported a dump but ${geoPath} is missing.`);
224363
- const geo = readNgeo(new Uint8Array(readFileSync26(geoPath)));
224901
+ const geo = readNgeo(new Uint8Array(readFileSync28(geoPath)));
224364
224902
  const params = {
224365
224903
  agentRadius: num2(args, "radius", config.agent.radius, 0.05, 10),
224366
224904
  agentHeight: num2(args, "height", config.agent.height, 0.1, 20),
@@ -224406,20 +224944,20 @@ import ${JSON.stringify(importPath)}
224406
224944
  if (result.polys === 0) {
224407
224945
  throw new CliError("The build produced no polygons — nothing walkable for this agent. Check the cell size against the level's scale, the slope, and that the floor is a STATIC body (Shape + Physics { motion: 'static' }).");
224408
224946
  }
224409
- writeFileSync23(navmeshPath, result.data);
224947
+ writeFileSync25(navmeshPath, result.data);
224410
224948
  if (obj) {
224411
- const objPath = join31(outDir, `${name}.navmesh.obj`);
224949
+ const objPath = join35(outDir, `${name}.navmesh.obj`);
224412
224950
  writeObj(objPath, navMeshTriangles(mod, result.data));
224413
224951
  logErr(c.dim(` wrote ${relative5(root, objPath)} (the polygon mesh, for inspection)`));
224414
224952
  }
224415
224953
  if (!keepGeometry)
224416
224954
  unlinkSync2(geoPath);
224417
- const kb = Math.round(statSync11(navmeshPath).size / 1024);
224955
+ const kb = Math.round(statSync12(navmeshPath).size / 1024);
224418
224956
  const b = result.bounds;
224419
224957
  logErr(c.dim(` ${result.tiles} tiles, ${result.polys} polygons, ${(b.max[0] - b.min[0]).toFixed(0)} × ${(b.max[2] - b.min[2]).toFixed(0)} m, ${result.time.toFixed(1)} s`));
224420
224958
  logErr(c.green(`✓ baked → ${relative5(root, navmeshPath).replace(/\\/g, "/")} (${kb} KB)`));
224421
224959
  if (!config.data) {
224422
- logErr(c.dim(` load it with env.navmesh: { data: asset('<rel>/${basename16(navmeshPath)}') } in the scene file, or NavMesh.load(scene, { data: asset(...) })`));
224960
+ logErr(c.dim(` load it with env.navmesh: { data: asset('<rel>/${basename17(navmeshPath)}') } in the scene file, or NavMesh.load(scene, { data: asset(...) })`));
224423
224961
  }
224424
224962
  };
224425
224963
  var navmesh = async (args) => {
@@ -224446,8 +224984,131 @@ var login = async (args) => {
224446
224984
 
224447
224985
  // src/commands/shaders.ts
224448
224986
  init_manifest();
224449
- import { readFileSync as readFileSync27 } from "node:fs";
224987
+ import { readFileSync as readFileSync30 } from "node:fs";
224988
+ import { relative as relative7 } from "node:path";
224989
+ init_util();
224990
+
224991
+ // src/commands/shadersNew.ts
224992
+ init_compile();
224993
+ init_distRoot();
224450
224994
  init_util();
224995
+ import { existsSync as existsSync34, mkdirSync as mkdirSync24, readdirSync as readdirSync16, readFileSync as readFileSync29, writeFileSync as writeFileSync26 } from "node:fs";
224996
+ import { basename as basename18, join as join36, relative as relative6, resolve as resolve16 } from "node:path";
224997
+ var engineMaterialsDir = () => {
224998
+ const candidates = [
224999
+ join36(distRoot, "runtime", "materials"),
225000
+ join36(distRoot, "..", "creator-gl", "materials", "src")
225001
+ ];
225002
+ for (const c4 of candidates)
225003
+ if (existsSync34(join36(c4, "unlit.mat")))
225004
+ return c4;
225005
+ return null;
225006
+ };
225007
+ var engineMaterialNames = () => {
225008
+ const dir = engineMaterialsDir();
225009
+ if (!dir)
225010
+ return [];
225011
+ return readdirSync16(dir).filter((f3) => f3.endsWith(".mat") && !f3.startsWith("camera") && f3 !== "shader360.mat").map((f3) => f3.slice(0, -4)).sort();
225012
+ };
225013
+ var NAME_RE = /^[a-z][a-z0-9_-]*$/i;
225014
+ var matName = (name) => name.replace(/-/g, "_");
225015
+ var starterMat = (name) => `// ${name}.mat — a custom material (lecodes shaders new).
225016
+ //
225017
+ // Filament's material language: the \`material\` block declares the parameters and the render state,
225018
+ // \`fragment\` computes the surface. Read it with \`Material.load(asset('./${name}.mat'))\` or the
225019
+ // ${name}.material.ts asset beside it; set parameters with material.set('tint', '#ff8800').
225020
+ // \`lecodes shaders compile\` (or \`lecodes dev\`) compiles it for every host into .lecodes/shaders.
225021
+ //
225022
+ // The comment on a parameter's line is what the scene editor shows for it:
225023
+ // @color a colour picker (also inferred from a name like tint / color / emissive)
225024
+ // @range(min, max) a slider, @step(s) its step, @default(v) the value a new material starts with
225025
+ // the rest of the line is the tooltip
225026
+
225027
+ material {
225028
+ name : ${matName(name)},
225029
+ shadingModel : unlit,
225030
+ parameters : [
225031
+ { type : float3, name : tint }, // @color @default(#ffffff) the flat colour
225032
+ { type : float, name : glow }, // @range(0, 2) @step(0.05) @default(1) brightness multiplier
225033
+ { type : sampler2d, name : map } // the texture, white when unset
225034
+ ],
225035
+ requires : [
225036
+ uv0
225037
+ ]
225038
+ }
225039
+
225040
+ fragment {
225041
+ void material(inout MaterialInputs material) {
225042
+ prepareMaterial(material);
225043
+ vec4 base = texture(materialParams_map, getUV0());
225044
+ material.baseColor.rgb = base.rgb * materialParams.tint * materialParams.glow;
225045
+ }
225046
+ }
225047
+ `;
225048
+ var refMat = (name, ref, source) => {
225049
+ const renamed = source.replace(/(\bname\s*:\s*)([A-Za-z_][A-Za-z0-9_]*)/, `$1${matName(name)}`);
225050
+ return `// ${name}.mat — started from the engine's ${ref}.mat (lecodes shaders new --ref ${ref}); edit freely,
225051
+ // the engine keeps its own. \`lecodes shaders compile\` builds it for every host into .lecodes/shaders.
225052
+ // Load it with Material.load(asset('./${name}.mat')) or the ${name}.material.ts asset beside it.
225053
+
225054
+ ${renamed}`;
225055
+ };
225056
+ var materialAsset = (name, mat) => {
225057
+ const schema = parseShaderSchema(mat);
225058
+ const lines = [];
225059
+ for (const p2 of schema.params) {
225060
+ if (p2.editor === "texture") {
225061
+ lines.push(` // ${p2.name}: asset('./texture.png'), // a sampler takes an image asset`);
225062
+ continue;
225063
+ }
225064
+ if (p2.default === null || p2.default === undefined)
225065
+ continue;
225066
+ lines.push(` ${p2.name}: ${JSON.stringify(p2.default)},${p2.tooltip ? ` // ${p2.tooltip}` : ""}`);
225067
+ }
225068
+ return `// ${name}.material.ts — the material as a project asset: ONE shared instance a scene file takes as
225069
+ // \`material: ${camel(name)}\` and code loads with \`await ${camel(name)}.load()\`. The parameters and their
225070
+ // widgets come from the annotations in ${name}.mat.
225071
+ export default defineMaterial({
225072
+ shader: asset('./${name}.mat'),
225073
+ params: {
225074
+ ${lines.join(`
225075
+ `)}
225076
+ },
225077
+ })
225078
+ `;
225079
+ };
225080
+ var camel = (name) => name.replace(/[-_]+(.)/g, (_, ch) => ch.toUpperCase());
225081
+ var writeNewShader = (o2) => {
225082
+ const name = o2.name.endsWith(".mat") ? o2.name.slice(0, -4) : o2.name;
225083
+ if (!NAME_RE.test(name))
225084
+ throw new CliError(`"${o2.name}" is not a shader name — letters, digits, - and _ (starting with a letter), e.g. hologram or rim-light`);
225085
+ const dir = resolve16(o2.cwd ?? process.cwd(), o2.dir ?? ".");
225086
+ const matPath = join36(dir, `${name}.mat`), assetPath = join36(dir, `${name}.material.ts`);
225087
+ if (!o2.force) {
225088
+ for (const p2 of [matPath, assetPath])
225089
+ if (existsSync34(p2))
225090
+ throw new CliError(`${relative6(o2.cwd ?? process.cwd(), p2) || basename18(p2)} exists — pick another name or pass --force to overwrite`);
225091
+ }
225092
+ let mat;
225093
+ let ref = null;
225094
+ if (o2.ref) {
225095
+ ref = o2.ref.endsWith(".mat") ? o2.ref.slice(0, -4) : o2.ref;
225096
+ const srcDir = engineMaterialsDir();
225097
+ if (!srcDir)
225098
+ throw new CliError("this CLI carries no engine material sources (runtime/materials) — --ref needs a CLI built with them");
225099
+ const known = engineMaterialNames();
225100
+ if (!known.includes(ref))
225101
+ throw new CliError(`no engine material "${ref}". Known: ${known.join(", ")}`);
225102
+ mat = refMat(name, ref, readFileSync29(join36(srcDir, `${ref}.mat`), "utf8"));
225103
+ } else
225104
+ mat = starterMat(name);
225105
+ mkdirSync24(dir, { recursive: true });
225106
+ writeFileSync26(matPath, mat);
225107
+ writeFileSync26(assetPath, materialAsset(name, mat));
225108
+ return { mat: matPath, asset: assetPath, ref };
225109
+ };
225110
+
225111
+ // src/commands/shaders.ts
224451
225112
  init_compile();
224452
225113
  var collectShaders = () => {
224453
225114
  let root;
@@ -224478,7 +225139,7 @@ var status2 = async () => {
224478
225139
  if (matcId === null)
224479
225140
  state = c.dim("unknown (matc not downloaded)");
224480
225141
  else {
224481
- const text = readFileSync27(shader.absPath, "utf8");
225142
+ const text = readFileSync30(shader.absPath, "utf8");
224482
225143
  if (shaderIsCompiled(root, text, matcId))
224483
225144
  state = "compiled";
224484
225145
  else {
@@ -224487,7 +225148,7 @@ var status2 = async () => {
224487
225148
  }
224488
225149
  }
224489
225150
  log(` ${shader.path.padEnd(width)} ${state}`);
224490
- const schema = parseShaderSchema(readFileSync27(shader.absPath, "utf8"));
225151
+ const schema = parseShaderSchema(readFileSync30(shader.absPath, "utf8"));
224491
225152
  for (const p2 of schema.params) {
224492
225153
  const range = p2.min !== undefined ? ` [${p2.min}..${p2.max}]` : "";
224493
225154
  const def = p2.default !== null && p2.default !== undefined ? ` = ${JSON.stringify(p2.default)}` : "";
@@ -224530,7 +225191,7 @@ var compile3 = async (args) => {
224530
225191
  }
224531
225192
  const matcExe = await resolveMatcExe();
224532
225193
  const matcId = matcIdentity(matcExe);
224533
- const wasCompiled = new Map(shaders.map((s2) => [s2.path, shaderIsCompiled(root, readFileSync27(s2.absPath, "utf8"), matcId)]));
225194
+ const wasCompiled = new Map(shaders.map((s2) => [s2.path, shaderIsCompiled(root, readFileSync30(s2.absPath, "utf8"), matcId)]));
224534
225195
  const errors2 = [];
224535
225196
  for (const shader of shaders) {
224536
225197
  try {
@@ -224552,6 +225213,16 @@ ${c.red("✗")} ${e2}`);
224552
225213
  const scope = platforms ? ` [${platforms.join(", ")}]` : "";
224553
225214
  note(`${shaders.length} shader${shaders.length === 1 ? "" : "s"} compiled${scope} → ${shadersCacheDir(root)}`);
224554
225215
  };
225216
+ var newShader = (args) => {
225217
+ const name = args._[1];
225218
+ if (!name)
225219
+ throw new CliError(`lecodes shaders new <name> [--ref ${engineMaterialNames().join("|") || "lit"}] [--dir <folder>] [--force]`);
225220
+ const res = writeNewShader({ name, ref: flagStr(args, "ref"), dir: flagStr(args, "dir"), force: args.flags.force === true });
225221
+ const rel = (p2) => relative7(process.cwd(), p2);
225222
+ log(`${rel(res.mat)} ${c.dim(res.ref ? `(from the engine's ${res.ref}.mat)` : "(the starter template)")}`);
225223
+ log(`${rel(res.asset)} ${c.dim("(defineMaterial asset — a scene takes it as `material:`)")}`);
225224
+ note(`next: edit it, then \`lecodes shaders compile\` (or just \`lecodes dev\`); in code: Material.load(asset('./${name.replace(/\.mat$/, "")}.mat'))`);
225225
+ };
224555
225226
  var shaders = async (args) => {
224556
225227
  const sub = args._[0];
224557
225228
  if (sub === undefined)
@@ -224560,12 +225231,14 @@ var shaders = async (args) => {
224560
225231
  return compile3(args);
224561
225232
  if (sub === "update")
224562
225233
  return updateMatc();
224563
- throw new CliError(`Unknown subcommand "shaders ${sub}". Use \`lecodes shaders\` (status), \`lecodes shaders compile [--platform a,b]\` or \`lecodes shaders update\`.`);
225234
+ if (sub === "new")
225235
+ return newShader(args);
225236
+ throw new CliError(`Unknown subcommand "shaders ${sub}". Use \`lecodes shaders\` (status), \`lecodes shaders compile [--platform a,b]\`, \`lecodes shaders new <name> [--ref lit]\` or \`lecodes shaders update\`.`);
224564
225237
  };
224565
225238
 
224566
225239
  // src/commands/render.ts
224567
- import { mkdirSync as mkdirSync23, readFileSync as readFileSync29, writeFileSync as writeFileSync24 } from "node:fs";
224568
- import { dirname as dirname15, resolve as resolve17 } from "node:path";
225240
+ import { mkdirSync as mkdirSync25, readFileSync as readFileSync32, writeFileSync as writeFileSync27 } from "node:fs";
225241
+ import { dirname as dirname17, resolve as resolve19 } from "node:path";
224569
225242
 
224570
225243
  // src/compile/headlessBundle.ts
224571
225244
  init_api();
@@ -224649,23 +225322,23 @@ var compileHeadlessBundle = async (args, opts = {}) => {
224649
225322
  // src/compile/screenEntry.ts
224650
225323
  init_designMeta();
224651
225324
  init_util();
224652
- import { existsSync as existsSync30, readFileSync as readFileSync28, statSync as statSync12 } from "node:fs";
224653
- import { isAbsolute, join as join32, relative as relative6, resolve as resolve15 } from "node:path";
225325
+ import { existsSync as existsSync35, readFileSync as readFileSync31, statSync as statSync13 } from "node:fs";
225326
+ import { isAbsolute, join as join37, relative as relative8, resolve as resolve17 } from "node:path";
224654
225327
  var SCREEN_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs"];
224655
225328
  var SCREEN_ENTRY_PATH = "/.screen-entry.ts";
224656
225329
  var DEFAULT_DESIGN_DIR = "design";
224657
225330
  var screenArg = (args) => args._[0];
224658
225331
  var hasSeparator = (file) => file.includes("/") || file.includes("\\");
224659
225332
  var findScreenFile = (root, file, dirFlag) => {
224660
- const candidates = [resolve15(process.cwd(), file)];
225333
+ const candidates = [resolve17(process.cwd(), file)];
224661
225334
  if (!hasSeparator(file)) {
224662
- candidates.push(join32(root, (dirFlag ?? DEFAULT_DESIGN_DIR).replace(/[/\\]+$/, ""), "screens", file));
225335
+ candidates.push(join37(root, (dirFlag ?? DEFAULT_DESIGN_DIR).replace(/[/\\]+$/, ""), "screens", file));
224663
225336
  }
224664
225337
  for (const base of candidates) {
224665
- if (existsSync30(base) && statSync12(base).isFile())
225338
+ if (existsSync35(base) && statSync13(base).isFile())
224666
225339
  return base;
224667
225340
  for (const ext2 of SCREEN_EXTS)
224668
- if (existsSync30(base + ext2))
225341
+ if (existsSync35(base + ext2))
224669
225342
  return base + ext2;
224670
225343
  }
224671
225344
  throw new CliError(`No screen file at "${file}".` + (hasSeparator(file) ? "" : ` Pass a path, or a screen id that exists in ${dirFlag ?? DEFAULT_DESIGN_DIR}/screens/.`));
@@ -224674,17 +225347,17 @@ var designDirOf = (root, key) => {
224674
225347
  const m = /^\/(.*)screens\/([^/]+)\.[^./]+$/.exec(key);
224675
225348
  if (!m)
224676
225349
  return null;
224677
- const dir = join32(root, m[1]);
224678
- return existsSync30(join32(dir, "meta.json")) ? { dir, id: m[2] } : null;
225350
+ const dir = join37(root, m[1]);
225351
+ return existsSync35(join37(dir, "meta.json")) ? { dir, id: m[2] } : null;
224679
225352
  };
224680
225353
  var resolveScreenTarget = (root, file, opts = {}) => {
224681
225354
  const abs = findScreenFile(root, file, opts.dir);
224682
- const rel = relative6(root, abs);
225355
+ const rel = relative8(root, abs);
224683
225356
  if (rel.startsWith("..") || isAbsolute(rel)) {
224684
225357
  throw new CliError(`"${file}" is outside the project (${root}) — a screen renders as part of its project, so its ` + `imports and asset() files resolve from the same tree.`);
224685
225358
  }
224686
225359
  const key = toKey(rel);
224687
- if (!/^\s*export\s+default\b/m.test(readFileSync28(abs, "utf8"))) {
225360
+ if (!/^\s*export\s+default\b/m.test(readFileSync31(abs, "utf8"))) {
224688
225361
  throw new CliError(`${key.slice(1)} has no default export. A screen file ends with ` + `\`export default UIScreen([...])\` — or \`export default (state) => UIScreen([...])\` for screen states.`);
224689
225362
  }
224690
225363
  const invoke = opts.state !== undefined ? `screen(${JSON.stringify(opts.state)})` : "screen()";
@@ -224723,11 +225396,11 @@ init_manifest();
224723
225396
 
224724
225397
  // src/desktopScript.ts
224725
225398
  init_util();
224726
- import { resolve as resolve16 } from "node:path";
225399
+ import { resolve as resolve18 } from "node:path";
224727
225400
  var isPoint = (v) => typeof v === "object" && v !== null && typeof v.x === "number" && typeof v.y === "number";
224728
225401
  var FRAME_MS = 16;
224729
225402
  var ACTIONS = ["tap", "type", "fill", "scroll", "wait", "frames", "drag", "hold", "key", "keyDown", "keyUp", "look", "mouseTo", "wheel", "stick", "gamepad", "camera", "view"];
224730
- var SEMANTIC_EXPECT = ["text", "notText", "node", "notNode", "screen", "value"];
225403
+ var SEMANTIC_EXPECT = ["text", "notText", "node", "notNode", "screen", "value", "audio"];
224731
225404
  var VIEW_NAMES = ["iso", "top", "front", "side", "back"];
224732
225405
  var isVec3 = (v) => Array.isArray(v) && v.length === 3 && v.every((n2) => typeof n2 === "number");
224733
225406
  var pt = (p2) => `(${p2.x},${p2.y})`;
@@ -224924,7 +225597,7 @@ var translateScenario = (rawSteps, opts) => {
224924
225597
  if (s2.screenshot !== undefined) {
224925
225598
  if (typeof s2.screenshot !== "string")
224926
225599
  throw new CliError(`${at}: "screenshot" must be a path.`);
224927
- plan.screenshot = resolve16(opts.outDir, s2.screenshot);
225600
+ plan.screenshot = resolve18(opts.outDir, s2.screenshot);
224928
225601
  plan.lines.push(emit(`frames 2`));
224929
225602
  plan.lines.push(emit(`shot ${plan.screenshot}`));
224930
225603
  budgetMs += 3 * FRAME_MS;
@@ -225004,6 +225677,7 @@ var evaluateDesktopRun = (plan, output) => {
225004
225677
 
225005
225678
  // src/commands/render.ts
225006
225679
  init_util();
225680
+ init_peerInstall();
225007
225681
  var DEVICE_SAFE_AREA = {
225008
225682
  none: { insets: [0, 0, 0, 0], bars: [] },
225009
225683
  iphone: { insets: [59, 0, 34, 0], bars: [] },
@@ -225024,18 +225698,12 @@ var resolveSafeAreaChoice = (explicit, device) => {
225024
225698
  }
225025
225699
  return;
225026
225700
  };
225027
- var requireRenderer = async () => {
225028
- try {
225029
- return await import("lecodes-renderer/headless");
225030
- } catch {
225031
- throw new CliError("The renderer isn't installed. Run: npm install -g lecodes-renderer");
225032
- }
225033
- };
225701
+ var requireRenderer = async () => loadPeer("lecodes-renderer", "headless", { for: "lecodes render" });
225034
225702
  var fileScenarioIO = (dir) => ({
225035
225703
  write: (relPath, data) => {
225036
- const abs = resolve17(dir, relPath);
225037
- mkdirSync23(dirname15(abs), { recursive: true });
225038
- writeFileSync24(abs, data);
225704
+ const abs = resolve19(dir, relPath);
225705
+ mkdirSync25(dirname17(abs), { recursive: true });
225706
+ writeFileSync27(abs, data);
225039
225707
  }
225040
225708
  });
225041
225709
  var resolveFixtures = (value, baseDir) => {
@@ -225043,9 +225711,9 @@ var resolveFixtures = (value, baseDir) => {
225043
225711
  return;
225044
225712
  let map = value;
225045
225713
  if (typeof value === "string") {
225046
- const abs = resolve17(baseDir, value);
225714
+ const abs = resolve19(baseDir, value);
225047
225715
  try {
225048
- map = JSON.parse(readFileSync29(abs, "utf8"));
225716
+ map = JSON.parse(readFileSync32(abs, "utf8"));
225049
225717
  } catch (e2) {
225050
225718
  throw new CliError(`Can't read fixtures "${value}": ${e2 instanceof Error ? e2.message : String(e2)}`);
225051
225719
  }
@@ -225058,7 +225726,7 @@ var resolveFixtures = (value, baseDir) => {
225058
225726
  var readScenarioFile = (path2) => {
225059
225727
  let raw;
225060
225728
  try {
225061
- raw = JSON.parse(readFileSync29(resolve17(path2), "utf8"));
225729
+ raw = JSON.parse(readFileSync32(resolve19(path2), "utf8"));
225062
225730
  } catch (e2) {
225063
225731
  throw new CliError(`Can't read scenario "${path2}": ${e2 instanceof Error ? e2.message : String(e2)}`);
225064
225732
  }
@@ -225109,23 +225777,25 @@ var renderDesktop = async (args) => {
225109
225777
  if ((camera || view) && scenario)
225110
225778
  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.');
225111
225779
  const desktopRenderer = resolveDesktopRenderer(root, args);
225780
+ const { dirs: pluginDirs } = desktopPluginLibraries(root);
225112
225781
  const { js, resources } = await buildBundle(args, undefined, {
225113
225782
  localAssets: true,
225114
225783
  desktopRenderer,
225115
225784
  extraEntries: screen2 ? [screen2.entry] : undefined,
225116
225785
  entryOverride: screen2?.entryPath
225117
225786
  });
225118
- mkdirSync23(dirname15(resolve17(pngFlag)), { recursive: true });
225787
+ mkdirSync25(dirname17(resolve19(pngFlag)), { recursive: true });
225119
225788
  if (scenario) {
225120
225789
  const outDir = flagStr(args, "out-dir") ?? ".";
225121
225790
  const plan = translateScenario(scenario.steps, { outDir, headroomFrames: frames });
225122
225791
  for (const st of plan.steps)
225123
225792
  if (st.screenshot)
225124
- mkdirSync23(dirname15(st.screenshot), { recursive: true });
225793
+ mkdirSync25(dirname17(st.screenshot), { recursive: true });
225125
225794
  const timeoutMs2 = Number(flagStr(args, "timeout")) || 60000 + plan.steps.length * 5000;
225126
225795
  note(`Running ${plan.steps.length} step${plan.steps.length === 1 ? "" : "s"} (native)…`);
225127
225796
  const { output } = await runDesktopRender(js, resources, {
225128
225797
  renderer: desktopRenderer,
225798
+ pluginDirs,
225129
225799
  width,
225130
225800
  height,
225131
225801
  frames: plan.frameCap,
@@ -225158,6 +225828,7 @@ var renderDesktop = async (args) => {
225158
225828
  note(screen2 ? `Rendering ${screen2.label} (native)…` : "Rendering (native)…");
225159
225829
  await runDesktopRender(js, resources, {
225160
225830
  renderer: desktopRenderer,
225831
+ pluginDirs,
225161
225832
  width,
225162
225833
  height,
225163
225834
  frames,
@@ -225225,7 +225896,7 @@ var render = async (args) => {
225225
225896
  const settleStr = flagStr(args, "settle") ?? (scenario?.settle !== undefined ? String(scenario.settle) : undefined);
225226
225897
  const timeStr = flagStr(args, "time");
225227
225898
  const onConsole = flagBool(args, "logs") ? (level, text2) => logErr(`${c.dim(`[${level}]`)} ${text2}`) : undefined;
225228
- const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname15(resolve17(scriptFlag)) : ".");
225899
+ const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname17(resolve19(scriptFlag)) : ".");
225229
225900
  const flagFixtures = resolveFixtures(flagStr(args, "fixtures"), ".");
225230
225901
  const fixtures = envelopeFixtures || flagFixtures ? { ...envelopeFixtures, ...flagFixtures } : undefined;
225231
225902
  if (fixtures) {
@@ -225264,7 +225935,7 @@ var render = async (args) => {
225264
225935
  ${c.red(st.error ?? "failed")}`);
225265
225936
  }
225266
225937
  if (!result2.ok && result2.failureArtifacts.length > 0) {
225267
- logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p2) => resolve17(outDir, p2)).join(", ")}`));
225938
+ logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p2) => resolve19(outDir, p2)).join(", ")}`));
225268
225939
  }
225269
225940
  logErr(result2.ok ? c.green(`✓ ${result2.steps.length}/${steps.length} steps passed`) : c.red(`✗ failed at step ${result2.steps.length} of ${steps.length}`));
225270
225941
  return done((cb) => cb(), result2.ok ? 0 : 1);
@@ -225285,10 +225956,10 @@ var render = async (args) => {
225285
225956
  for (const w of [...warnings, ...mapWarnings])
225286
225957
  warnErr(w);
225287
225958
  const pngPath = mapFlag.endsWith(".png") ? mapFlag : mapFlag + ".png";
225288
- mkdirSync23(dirname15(resolve17(pngPath)), { recursive: true });
225289
- writeFileSync24(resolve17(pngPath), png2);
225959
+ mkdirSync25(dirname17(resolve19(pngPath)), { recursive: true });
225960
+ writeFileSync27(resolve19(pngPath), png2);
225290
225961
  const jsonPath = pngPath.slice(0, -4) + ".json";
225291
- writeFileSync24(resolve17(jsonPath), JSON.stringify(data, null, 2));
225962
+ writeFileSync27(resolve19(jsonPath), JSON.stringify(data, null, 2));
225292
225963
  note(`Wrote ${pngPath} + ${jsonPath}`);
225293
225964
  return done((cb) => cb());
225294
225965
  }
@@ -225308,8 +225979,8 @@ var render = async (args) => {
225308
225979
  if (isNullSink(pngFlag)) {
225309
225980
  note(`Discarded PNG (${pngFlag})`);
225310
225981
  } else {
225311
- mkdirSync23(dirname15(resolve17(pngFlag)), { recursive: true });
225312
- writeFileSync24(resolve17(pngFlag), png2);
225982
+ mkdirSync25(dirname17(resolve19(pngFlag)), { recursive: true });
225983
+ writeFileSync27(resolve19(pngFlag), png2);
225313
225984
  note(`Wrote ${pngFlag} (${png2.length} bytes, ${outW}x${outH}${clip !== undefined ? ", clipped" : ""})`);
225314
225985
  }
225315
225986
  return done((cb) => cb());
@@ -225324,7 +225995,7 @@ var render = async (args) => {
225324
225995
  return done((cb) => cb());
225325
225996
  }
225326
225997
  if (out) {
225327
- writeFileSync24(resolve17(out), result.text);
225998
+ writeFileSync27(resolve19(out), result.text);
225328
225999
  note(`Wrote ${out} (${result.text.length} bytes)`);
225329
226000
  return done((cb) => cb());
225330
226001
  }
@@ -225339,8 +226010,8 @@ init_api();
225339
226010
  init_config();
225340
226011
  init_manifest();
225341
226012
  init_util();
225342
- import { appendFileSync as appendFileSync2, existsSync as existsSync31, readFileSync as readFileSync30, writeFileSync as writeFileSync25 } from "node:fs";
225343
- import { join as join33 } from "node:path";
226013
+ import { appendFileSync as appendFileSync2, existsSync as existsSync36, readFileSync as readFileSync33, writeFileSync as writeFileSync28 } from "node:fs";
226014
+ import { join as join38 } from "node:path";
225344
226015
  var ENV_VAR = "LECODES_PUSH_KEY";
225345
226016
  var context = () => {
225346
226017
  const root = findProjectRoot(process.cwd());
@@ -225484,11 +226155,11 @@ var pnKeysList = async (ctx) => {
225484
226155
  };
225485
226156
  var writeEnv = (root, key) => {
225486
226157
  const touched = [];
225487
- const envPath = join33(root, ".env");
226158
+ const envPath = join38(root, ".env");
225488
226159
  const line = `${ENV_VAR}=${key}`;
225489
- const existing = existsSync31(envPath) ? readFileSync30(envPath, "utf8") : "";
226160
+ const existing = existsSync36(envPath) ? readFileSync33(envPath, "utf8") : "";
225490
226161
  if (new RegExp(`^${ENV_VAR}=`, "m").test(existing)) {
225491
- writeFileSync25(envPath, existing.replace(new RegExp(`^${ENV_VAR}=.*$`, "m"), line));
226162
+ writeFileSync28(envPath, existing.replace(new RegExp(`^${ENV_VAR}=.*$`, "m"), line));
225492
226163
  } else {
225493
226164
  const separator = !existing || existing.endsWith(`
225494
226165
  `) ? "" : `
@@ -225497,8 +226168,8 @@ var writeEnv = (root, key) => {
225497
226168
  `);
225498
226169
  }
225499
226170
  touched.push(".env");
225500
- const ignorePath = join33(root, ".gitignore");
225501
- const ignore = existsSync31(ignorePath) ? readFileSync30(ignorePath, "utf8") : "";
226171
+ const ignorePath = join38(root, ".gitignore");
226172
+ const ignore = existsSync36(ignorePath) ? readFileSync33(ignorePath, "utf8") : "";
225502
226173
  if (!ignore.split(`
225503
226174
  `).some((l2) => l2.trim() === ".env")) {
225504
226175
  const separator = !ignore || ignore.endsWith(`
@@ -225597,8 +226268,8 @@ var pn = async (args) => {
225597
226268
  init_api();
225598
226269
  init_config();
225599
226270
  init_manifest();
225600
- import { existsSync as existsSync32, mkdirSync as mkdirSync24, rmSync as rmSync11, writeFileSync as writeFileSync26 } from "node:fs";
225601
- import { dirname as dirname16, join as join34 } from "node:path";
226271
+ import { existsSync as existsSync37, mkdirSync as mkdirSync26, rmSync as rmSync11, writeFileSync as writeFileSync29 } from "node:fs";
226272
+ import { dirname as dirname18, join as join39 } from "node:path";
225602
226273
  init_types4();
225603
226274
  init_localFiles();
225604
226275
  init_util();
@@ -225621,8 +226292,8 @@ var pull = async (args) => {
225621
226292
  const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a2) => a2.id === id)?.type !== "folder").map(([, path2]) => path2));
225622
226293
  for (const path2 of Object.keys(manifest.files)) {
225623
226294
  if (!remotePaths.has(path2)) {
225624
- const abs = join34(root, ...path2.replace(/^\//, "").split("/"));
225625
- if (existsSync32(abs))
226295
+ const abs = join39(root, ...path2.replace(/^\//, "").split("/"));
226296
+ if (existsSync37(abs))
225626
226297
  rmSync11(abs);
225627
226298
  }
225628
226299
  }
@@ -225660,18 +226331,18 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
225660
226331
  const files = { ...manifest.files };
225661
226332
  for (const asset of targets) {
225662
226333
  const key = pathById.get(asset.id);
225663
- const abs = join34(root, ...key.replace(/^\//, "").split("/"));
225664
- mkdirSync24(dirname16(abs), { recursive: true });
226334
+ const abs = join39(root, ...key.replace(/^\//, "").split("/"));
226335
+ mkdirSync26(dirname18(abs), { recursive: true });
225665
226336
  let sha;
225666
226337
  if (asset.type === "file") {
225667
226338
  if (!asset.file?.src)
225668
226339
  continue;
225669
226340
  const bytes = await downloadBlob(apiUrl, asset.file.src);
225670
- writeFileSync26(abs, bytes);
226341
+ writeFileSync29(abs, bytes);
225671
226342
  sha = sha256(bytes);
225672
226343
  } else {
225673
226344
  const text = asset.text ?? "";
225674
- writeFileSync26(abs, text);
226345
+ writeFileSync29(abs, text);
225675
226346
  sha = sha256(text);
225676
226347
  }
225677
226348
  files[key] = { id: asset.id, type: kindOf3(asset.type), sha };
@@ -225684,8 +226355,8 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
225684
226355
 
225685
226356
  // src/commands/scene.ts
225686
226357
  init_manifest();
225687
- import { existsSync as existsSync34, readFileSync as readFileSync32, readdirSync as readdirSync15, statSync as statSync13, writeFileSync as writeFileSync28 } from "node:fs";
225688
- import { basename as basename17, dirname as dirname17, join as join36, resolve as resolve18 } from "node:path";
226358
+ import { existsSync as existsSync39, readFileSync as readFileSync35, readdirSync as readdirSync17, statSync as statSync14, writeFileSync as writeFileSync31 } from "node:fs";
226359
+ import { basename as basename19, dirname as dirname19, join as join41, resolve as resolve20 } from "node:path";
225689
226360
 
225690
226361
  // src/compile/sceneCompile.ts
225691
226362
  init_localFiles();
@@ -225694,18 +226365,18 @@ init_distRoot();
225694
226365
  init_util();
225695
226366
  init_projectCompile();
225696
226367
  import { createHash as createHash8 } from "node:crypto";
225697
- import { existsSync as existsSync33, mkdirSync as mkdirSync25, readFileSync as readFileSync31, writeFileSync as writeFileSync27 } from "node:fs";
225698
- import { join as join35 } from "node:path";
226368
+ import { existsSync as existsSync38, mkdirSync as mkdirSync27, readFileSync as readFileSync34, writeFileSync as writeFileSync30 } from "node:fs";
226369
+ import { join as join40 } from "node:path";
225699
226370
  var TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i;
225700
226371
  var rel = (key) => key.replace(/^\//, "");
225701
226372
  var resolveHarness = (loadSceneHarness2) => {
225702
- const inRepo = join35(distRoot, "..", "projects", "src", "scene-editor");
226373
+ const inRepo = join40(distRoot, "..", "projects", "src", "scene-editor");
225703
226374
  try {
225704
- return loadSceneHarness2(existsSync33(inRepo) ? inRepo : undefined);
226375
+ return loadSceneHarness2(existsSync38(inRepo) ? inRepo : undefined);
225705
226376
  } catch {
225706
- const vendored = join35(distRoot, "runtime", "scene-harness.json");
225707
- if (existsSync33(vendored))
225708
- return JSON.parse(readFileSync31(vendored, "utf8"));
226377
+ const vendored = join40(distRoot, "runtime", "scene-harness.json");
226378
+ if (existsSync38(vendored))
226379
+ return JSON.parse(readFileSync34(vendored, "utf8"));
225709
226380
  throw new CliError("scene-editor harness sources not found — reinstall the CLI or set LECODES_SCENE_HARNESS_DIR.");
225710
226381
  }
225711
226382
  };
@@ -225717,9 +226388,9 @@ var compileSceneEditor = async (opts) => {
225717
226388
  for (const file of scan2.files) {
225718
226389
  const path2 = "./" + rel(file.path);
225719
226390
  if (TEXT_EXT.test(path2)) {
225720
- entries.push({ path: path2, type: "text", text: readFileSync31(file.absPath, "utf8") });
226391
+ entries.push({ path: path2, type: "text", text: readFileSync34(file.absPath, "utf8") });
225721
226392
  } else if (/\.mat$/i.test(path2)) {
225722
- entries.push({ path: path2, type: "shader", text: readFileSync31(file.absPath, "utf8"), absPath: file.absPath });
226393
+ entries.push({ path: path2, type: "shader", text: readFileSync34(file.absPath, "utf8"), absPath: file.absPath });
225723
226394
  } else {
225724
226395
  entries.push({ path: path2, type: "resource", fileSrc: "/project/" + rel(file.path) });
225725
226396
  }
@@ -225735,10 +226406,10 @@ var compileSceneEditor = async (opts) => {
225735
226406
  continue;
225736
226407
  const hash = createHash8("sha256").update(e2.text ?? "").digest("hex").slice(0, 16);
225737
226408
  const dir = shadersCacheDir(root);
225738
- mkdirSync25(dir, { recursive: true });
226409
+ mkdirSync27(dir, { recursive: true });
225739
226410
  const liteName = `${hash}_webgl-lite.filamat`;
225740
- if (!existsSync33(join35(dir, liteName)))
225741
- writeFileSync27(join35(dir, liteName), JSON.stringify(buildLiteMaterialArtifact2(e2.text ?? "")));
226411
+ if (!existsSync38(join40(dir, liteName)))
226412
+ writeFileSync30(join40(dir, liteName), JSON.stringify(buildLiteMaterialArtifact2(e2.text ?? "")));
225742
226413
  const src = `/project/.lecodes/shaders/${hash}_opengl.filamat`;
225743
226414
  e2.shaders = ["opengl", "webgl-lite"].map((platform) => ({ platform, src }));
225744
226415
  }
@@ -225768,13 +226439,14 @@ var compileSceneEditor = async (opts) => {
225768
226439
  // src/commands/scene.ts
225769
226440
  init_browserAuth();
225770
226441
  init_util();
226442
+ init_peerInstall();
225771
226443
  var DEFAULT_PORT3 = 4488;
225772
226444
  var findAppRoot = (start) => {
225773
226445
  let dir = start;
225774
226446
  while (true) {
225775
- if (existsSync34(join36(dir, "app.json")))
226447
+ if (existsSync39(join41(dir, "app.json")))
225776
226448
  return dir;
225777
- const parent = dirname17(dir);
226449
+ const parent = dirname19(dir);
225778
226450
  if (parent === dir)
225779
226451
  return null;
225780
226452
  dir = parent;
@@ -225798,26 +226470,26 @@ var getContext2 = (args) => {
225798
226470
  manifest = null;
225799
226471
  const pathArg = args._[0];
225800
226472
  if (pathArg) {
225801
- const abs = resolve18(process.cwd(), pathArg);
225802
- if (existsSync34(abs) && statSync13(abs).isDirectory())
226473
+ const abs = resolve20(process.cwd(), pathArg);
226474
+ if (existsSync39(abs) && statSync14(abs).isDirectory())
225803
226475
  root = abs;
225804
- else if (abs.endsWith(".scene.ts") && existsSync34(abs))
225805
- root = findAppRoot(dirname17(abs)) ?? dirname17(abs);
226476
+ else if (abs.endsWith(".scene.ts") && existsSync39(abs))
226477
+ root = findAppRoot(dirname19(abs)) ?? dirname19(abs);
225806
226478
  else
225807
226479
  throw new CliError(`No such folder or .scene.ts: ${pathArg}`);
225808
226480
  } else {
225809
226481
  root = findAppRoot(root) ?? root;
225810
226482
  }
225811
226483
  }
225812
- return { root, manifest, name: manifest?.name ?? basename17(root) };
226484
+ return { root, manifest, name: manifest?.name ?? basename19(root) };
225813
226485
  };
225814
226486
  var writeMcpConfig2 = (root, port) => {
225815
- const path2 = join36(root, ".mcp.json");
226487
+ const path2 = join41(root, ".mcp.json");
225816
226488
  const entry = { type: "http", url: `http://localhost:${port}/mcp` };
225817
226489
  let config = {};
225818
- if (existsSync34(path2)) {
226490
+ if (existsSync39(path2)) {
225819
226491
  try {
225820
- config = JSON.parse(readFileSync32(path2, "utf8"));
226492
+ config = JSON.parse(readFileSync35(path2, "utf8"));
225821
226493
  } catch {
225822
226494
  warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-scene server by hand.");
225823
226495
  return;
@@ -225830,17 +226502,17 @@ var writeMcpConfig2 = (root, port) => {
225830
226502
  return;
225831
226503
  }
225832
226504
  config.mcpServers["lecodes-scene"] = entry;
225833
- writeFileSync28(path2, JSON.stringify(config, null, 2) + `
226505
+ writeFileSync31(path2, JSON.stringify(config, null, 2) + `
225834
226506
  `);
225835
226507
  note(`${existing ? "updated" : "created"} .mcp.json (lecodes-scene MCP → ${entry.url})`);
225836
226508
  };
225837
226509
  var countScenes = (dir) => {
225838
226510
  let n2 = 0;
225839
- for (const name of readdirSync15(dir)) {
226511
+ for (const name of readdirSync17(dir)) {
225840
226512
  if (name === "node_modules" || name.startsWith("."))
225841
226513
  continue;
225842
- const full = join36(dir, name);
225843
- if (statSync13(full).isDirectory())
226514
+ const full = join41(dir, name);
226515
+ if (statSync14(full).isDirectory())
225844
226516
  n2 += countScenes(full);
225845
226517
  else if (name.endsWith(".scene.ts"))
225846
226518
  n2++;
@@ -225849,12 +226521,7 @@ var countScenes = (dir) => {
225849
226521
  };
225850
226522
  var scene = async (args) => {
225851
226523
  const ctx = getContext2(args);
225852
- let sceneServer;
225853
- try {
225854
- sceneServer = await import("lecodes-3d-editor/server");
225855
- } catch {
225856
- throw new CliError("The scene editor isn't installed. Run: npm install -g lecodes-3d-editor");
225857
- }
226524
+ const sceneServer = await loadPeer("lecodes-3d-editor", "server", { for: "lecodes scene" });
225858
226525
  const port = Number(flagStr(args, "port")) || DEFAULT_PORT3;
225859
226526
  if (countScenes(ctx.root) === 0) {
225860
226527
  warnErr(`No .scene.ts file under ${ctx.root} yet — add one and the editor will pick it up.`);
@@ -225882,11 +226549,12 @@ var scene = async (args) => {
225882
226549
  };
225883
226550
 
225884
226551
  // src/commands/thumbs.ts
225885
- import { createRequire as createRequire5 } from "node:module";
226552
+ init_peerInstall();
226553
+ import { createRequire as createRequire7 } from "node:module";
225886
226554
  import { execFileSync as execFileSync7 } from "node:child_process";
225887
- import { existsSync as existsSync35, mkdtempSync as mkdtempSync6, readdirSync as readdirSync16, rmSync as rmSync12, statSync as statSync14 } from "node:fs";
226555
+ import { existsSync as existsSync40, mkdtempSync as mkdtempSync6, readdirSync as readdirSync18, rmSync as rmSync12, statSync as statSync15 } from "node:fs";
225888
226556
  import { tmpdir as tmpdir6 } from "node:os";
225889
- import { extname, join as join37, relative as relative7, resolve as resolve19 } from "node:path";
226557
+ import { extname, join as join42, relative as relative9, resolve as resolve21 } from "node:path";
225890
226558
  init_util();
225891
226559
  var DEFAULT_SIZE = 256;
225892
226560
  var IMAGE_EXT = /\.(png|jpe?g|webp|bmp|gif|tiff?|ktx2)$/i;
@@ -225897,13 +226565,13 @@ var findKtx = () => {
225897
226565
  process.env.LECODES_KTX,
225898
226566
  process.env.KTX_CLI,
225899
226567
  process.env.KTX_TOKTX?.replace(/toktx(\.exe)?$/i, exe),
225900
- process.platform === "win32" ? join37(process.env.LOCALAPPDATA ?? "", "Programs", "KTX-Software", "bin", exe) : null,
225901
- process.platform === "win32" ? join37(process.env.ProgramFiles ?? "", "KTX-Software", "bin", exe) : null,
226568
+ process.platform === "win32" ? join42(process.env.LOCALAPPDATA ?? "", "Programs", "KTX-Software", "bin", exe) : null,
226569
+ process.platform === "win32" ? join42(process.env.ProgramFiles ?? "", "KTX-Software", "bin", exe) : null,
225902
226570
  "/usr/local/bin/ktx",
225903
226571
  "/opt/homebrew/bin/ktx"
225904
226572
  ].filter((p2) => !!p2);
225905
226573
  for (const path2 of candidates) {
225906
- if (existsSync35(path2))
226574
+ if (existsSync40(path2))
225907
226575
  return path2;
225908
226576
  }
225909
226577
  try {
@@ -225915,8 +226583,8 @@ var findKtx = () => {
225915
226583
  };
225916
226584
  var sharpResizer = () => {
225917
226585
  try {
225918
- const req = createRequire5(import.meta.url);
225919
- const sharp = createRequire5(req.resolve("lecodes-assets/cli"))("sharp");
226586
+ const req = createRequire7(import.meta.url);
226587
+ const sharp = createRequire7(req.resolve("lecodes-assets/cli"))("sharp");
225920
226588
  return async (file, size) => sharp(file).resize({ width: size, height: size, fit: "inside", withoutEnlargement: true }).png().toBuffer();
225921
226589
  } catch {
225922
226590
  return null;
@@ -225930,22 +226598,22 @@ var jimpResizer = () => async (file, size) => {
225930
226598
  return await img.getBuffer("image/png");
225931
226599
  };
225932
226600
  var ktxToPng = (ktx, file, out) => {
225933
- const run2 = (args) => execFileSync7(ktx, args, { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
226601
+ const run3 = (args) => execFileSync7(ktx, args, { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
225934
226602
  try {
225935
- run2(["extract", "--transcode", "rgba8", "--level", "0", file, out]);
226603
+ run3(["extract", "--transcode", "rgba8", "--level", "0", file, out]);
225936
226604
  } catch (e2) {
225937
- run2(["extract", "--level", "0", file, out]);
225938
- if (!existsSync35(out))
226605
+ run3(["extract", "--level", "0", file, out]);
226606
+ if (!existsSync40(out))
225939
226607
  throw e2;
225940
226608
  }
225941
226609
  };
225942
226610
  var collect = (dir) => {
225943
226611
  const out = [];
225944
- for (const name of readdirSync16(dir)) {
226612
+ for (const name of readdirSync18(dir)) {
225945
226613
  if (name === "node_modules" || name.startsWith("."))
225946
226614
  continue;
225947
- const full = join37(dir, name);
225948
- if (statSync14(full).isDirectory())
226615
+ const full = join42(dir, name);
226616
+ if (statSync15(full).isDirectory())
225949
226617
  out.push(...collect(full));
225950
226618
  else if (IMAGE_EXT.test(name))
225951
226619
  out.push(full);
@@ -225954,22 +226622,17 @@ var collect = (dir) => {
225954
226622
  };
225955
226623
  var thumbs = async (args) => {
225956
226624
  const root = projectRootOf(process.cwd());
225957
- let sceneServer;
225958
- try {
225959
- sceneServer = await import("lecodes-3d-editor/server");
225960
- } catch {
225961
- throw new CliError("The scene editor isn't installed (it owns the preview cache). Run: npm install -g lecodes-3d-editor");
225962
- }
226625
+ const sceneServer = await loadPeer("lecodes-3d-editor", "server", { for: "lecodes thumbs" });
225963
226626
  const size = Number(flagStr(args, "size")) || DEFAULT_SIZE;
225964
226627
  const force = flagBool(args, "force");
225965
226628
  let files = [];
225966
226629
  const rendered = [];
225967
226630
  if (args._.length > 0) {
225968
226631
  for (const arg of args._) {
225969
- const abs = resolve19(process.cwd(), arg);
225970
- if (!existsSync35(abs))
226632
+ const abs = resolve21(process.cwd(), arg);
226633
+ if (!existsSync40(abs))
225971
226634
  throw new CliError(`No such file or folder: ${arg}`);
225972
- if (statSync14(abs).isDirectory())
226635
+ if (statSync15(abs).isDirectory())
225973
226636
  files.push(...collect(abs));
225974
226637
  else if (IMAGE_EXT.test(abs))
225975
226638
  files.push(abs);
@@ -225989,12 +226652,12 @@ var thumbs = async (args) => {
225989
226652
  const ktx = files.some((f3) => f3.toLowerCase().endsWith(".ktx2")) ? findKtx() : null;
225990
226653
  const sharp = sharpResizer();
225991
226654
  const resize = sharp ?? jimpResizer();
225992
- const tmp = mkdtempSync6(join37(tmpdir6(), "lecodes-thumbs-"));
226655
+ const tmp = mkdtempSync6(join42(tmpdir6(), "lecodes-thumbs-"));
225993
226656
  let made = 0, fresh = 0;
225994
226657
  const skipped = [];
225995
226658
  try {
225996
226659
  for (const [i2, file] of files.entries()) {
225997
- const asset = "./" + toKey(relative7(root, file));
226660
+ const asset = "./" + toKey(relative9(root, file));
225998
226661
  progress(i2, files.length, asset);
225999
226662
  const hash = sceneServer.hashAsset(root, asset);
226000
226663
  if (!hash)
@@ -226015,7 +226678,7 @@ var thumbs = async (args) => {
226015
226678
  skipped.push(`${asset} (KTX2 needs KTX-Software's \`ktx\` — set LECODES_KTX or put it on PATH)`);
226016
226679
  continue;
226017
226680
  }
226018
- source = join37(tmp, `${i2}.png`);
226681
+ source = join42(tmp, `${i2}.png`);
226019
226682
  ktxToPng(ktx, file, source);
226020
226683
  }
226021
226684
  sceneServer.writePreviewFile(root, asset, await resize(source, size));
@@ -226035,16 +226698,13 @@ var thumbs = async (args) => {
226035
226698
  if (rendered.length > 0) {
226036
226699
  warnErr(`Models, materials and prefabs are RENDERED, not decoded — the scene editor does those: ${rendered.join(", ")}`);
226037
226700
  }
226038
- note(`Cache: ${join37(root, ".lecodes", "preview")} (re-runs skip everything unchanged)`);
226701
+ note(`Cache: ${join42(root, ".lecodes", "preview")} (re-runs skip everything unchanged)`);
226039
226702
  };
226040
226703
 
226041
- // src/commands/assets.ts
226042
- init_util();
226043
-
226044
226704
  // src/cmgenTool.ts
226045
- import { existsSync as existsSync36, readdirSync as readdirSync17 } from "node:fs";
226046
- import { homedir as homedir8 } from "node:os";
226047
- import { join as join38, resolve as resolve20 } from "node:path";
226705
+ import { existsSync as existsSync41, readdirSync as readdirSync19 } from "node:fs";
226706
+ import { homedir as homedir9 } from "node:os";
226707
+ import { join as join43, resolve as resolve22 } from "node:path";
226048
226708
  init_util();
226049
226709
  var exeName2 = () => process.platform === "win32" ? "cmgen.exe" : "cmgen";
226050
226710
  var platformKey3 = () => {
@@ -226054,10 +226714,10 @@ var platformKey3 = () => {
226054
226714
  return "linux-x64";
226055
226715
  throw new CliError(process.platform === "darwin" ? "cmgen isn't published for macOS yet — build filament's host tools locally and set LECODES_CMGEN." : `cmgen isn't published for ${process.platform}.`);
226056
226716
  };
226057
- var cacheRoot3 = () => join38(homedir8(), ".lecodes", "cmgen");
226717
+ var cacheRoot3 = () => join43(homedir9(), ".lecodes", "cmgen");
226058
226718
  var findCmgenIn = (dir) => {
226059
- for (const entry of readdirSync17(dir, { withFileTypes: true })) {
226060
- const p2 = join38(dir, entry.name);
226719
+ for (const entry of readdirSync19(dir, { withFileTypes: true })) {
226720
+ const p2 = join43(dir, entry.name);
226061
226721
  if (entry.isFile() && entry.name === exeName2())
226062
226722
  return p2;
226063
226723
  if (entry.isDirectory()) {
@@ -226070,18 +226730,21 @@ var findCmgenIn = (dir) => {
226070
226730
  };
226071
226731
  var installedCmgenVersions = () => {
226072
226732
  try {
226073
- return readdirSync17(cacheRoot3(), { withFileTypes: true }).filter((e2) => e2.isDirectory() && e2.name.startsWith("cmgen-")).map((e2) => e2.name.slice("cmgen-".length)).filter((v) => findCmgenIn(join38(cacheRoot3(), `cmgen-${v}`)) !== null).sort(semverNewer);
226733
+ return readdirSync19(cacheRoot3(), { withFileTypes: true }).filter((e2) => e2.isDirectory() && e2.name.startsWith("cmgen-")).map((e2) => e2.name.slice("cmgen-".length)).filter((v) => findCmgenIn(join43(cacheRoot3(), `cmgen-${v}`)) !== null).sort(semverNewer);
226074
226734
  } catch {
226075
226735
  return [];
226076
226736
  }
226077
226737
  };
226078
226738
  var fetchCmgenManifest = () => fetchReleaseProduct("cmgen", "cmgen (environment tool)");
226079
- var downloadCmgen = async (version, versions, dest) => {
226739
+ var pickCmgen = (versions) => {
226080
226740
  const key = platformKey3();
226081
- const file = versions.find((v) => v.version === version)?.files[key];
226082
- if (!file) {
226083
- throw new CliError(`cmgen ${version} [${key}] isn't published — build filament's host tools locally and point ` + `LECODES_CMGEN at the binary.`);
226741
+ const hit = newestForPlatform(versions, key);
226742
+ if (!hit) {
226743
+ throw new CliError(`cmgen isn't published for ${key} — build filament's host tools locally and point ` + `LECODES_CMGEN at the binary.`);
226084
226744
  }
226745
+ return hit;
226746
+ };
226747
+ var downloadCmgen = async (version, file, dest) => {
226085
226748
  note(`Downloading the environment tool (cmgen ${version}, ${(file.size / 1024 / 1024).toFixed(1)} MB)…`);
226086
226749
  await downloadReleaseArchive(file, {
226087
226750
  cacheRoot: cacheRoot3(),
@@ -226092,20 +226755,21 @@ var downloadCmgen = async (version, versions, dest) => {
226092
226755
  var resolveCmgenExeCached = () => {
226093
226756
  const override = process.env.LECODES_CMGEN;
226094
226757
  if (override) {
226095
- if (!existsSync36(override))
226758
+ if (!existsSync41(override))
226096
226759
  throw new CliError(`LECODES_CMGEN points at "${override}" which doesn't exist.`);
226097
- return resolve20(override);
226760
+ return resolve22(override);
226098
226761
  }
226099
226762
  const cached = installedCmgenVersions()[0];
226100
- return cached ? findCmgenIn(join38(cacheRoot3(), `cmgen-${cached}`)) : null;
226763
+ return cached ? findCmgenIn(join43(cacheRoot3(), `cmgen-${cached}`)) : null;
226101
226764
  };
226102
226765
  var resolveCmgenExe = async () => {
226103
226766
  const cached = resolveCmgenExeCached();
226104
226767
  if (cached)
226105
226768
  return cached;
226106
- const { latest, versions } = await fetchCmgenManifest();
226107
- const dir = join38(cacheRoot3(), `cmgen-${latest}`);
226108
- await downloadCmgen(latest, versions, dir);
226769
+ const { versions } = await fetchCmgenManifest();
226770
+ const { version, file } = pickCmgen(versions);
226771
+ const dir = join43(cacheRoot3(), `cmgen-${version}`);
226772
+ await downloadCmgen(version, file, dir);
226109
226773
  const exe = findCmgenIn(dir);
226110
226774
  if (!exe)
226111
226775
  throw new CliError(`cmgen cache at ${dir} is broken — delete the folder and retry.`);
@@ -226113,13 +226777,9 @@ var resolveCmgenExe = async () => {
226113
226777
  };
226114
226778
 
226115
226779
  // src/commands/assets.ts
226780
+ init_peerInstall();
226116
226781
  var assets = async (args) => {
226117
- let mod;
226118
- try {
226119
- mod = await import("lecodes-assets/cli");
226120
- } catch {
226121
- throw new CliError("The asset pipeline isn't installed. Run: npm install -g lecodes-assets");
226122
- }
226782
+ const mod = await loadPeer("lecodes-assets", "cli", { for: "lecodes assets" });
226123
226783
  const raw = args._raw ?? args._;
226124
226784
  if (raw[0] === "sky" && !process.env.LECODES_CMGEN) {
226125
226785
  process.env.LECODES_CMGEN = await resolveCmgenExe();
@@ -226162,8 +226822,8 @@ var status3 = async (_args) => {
226162
226822
  };
226163
226823
 
226164
226824
  // src/commands/test.ts
226165
- import { existsSync as existsSync37, mkdirSync as mkdirSync26, readFileSync as readFileSync33, readdirSync as readdirSync18 } from "node:fs";
226166
- import { dirname as dirname18, resolve as resolve21 } from "node:path";
226825
+ import { existsSync as existsSync42, mkdirSync as mkdirSync28, readFileSync as readFileSync36, readdirSync as readdirSync20 } from "node:fs";
226826
+ import { dirname as dirname20, resolve as resolve23 } from "node:path";
226167
226827
  init_manifest();
226168
226828
  init_util();
226169
226829
  var deriveDesignFlows = (meta, entryScreen) => {
@@ -226226,7 +226886,7 @@ var slug3 = (s2) => s2.replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "").
226226
226886
  var readFlowFile = (abs, displayName) => {
226227
226887
  let raw;
226228
226888
  try {
226229
- raw = JSON.parse(readFileSync33(abs, "utf8"));
226889
+ raw = JSON.parse(readFileSync36(abs, "utf8"));
226230
226890
  } catch (e2) {
226231
226891
  throw new CliError(`Can't read "${displayName}": ${e2 instanceof Error ? e2.message : String(e2)}`);
226232
226892
  }
@@ -226240,7 +226900,7 @@ var readFlowFile = (abs, displayName) => {
226240
226900
  name: file.name ?? displayName,
226241
226901
  kind: "file",
226242
226902
  steps: file.steps,
226243
- fixtures: resolveFixtures(file.fixtures, dirname18(abs)),
226903
+ fixtures: resolveFixtures(file.fixtures, dirname20(abs)),
226244
226904
  width: file.width,
226245
226905
  height: file.height,
226246
226906
  settle: file.settle,
@@ -226281,40 +226941,40 @@ var test = async (args) => {
226281
226941
  if (!Number.isFinite(nativeFixedDt) || nativeFixedDt < 0 || nativeFixedDt > 1000)
226282
226942
  throw new CliError(`--fixed-dt wants a number of ms (0 = wall clock, max 1000), got "${fixedDtFlag}".`);
226283
226943
  const projectViewport = desktopDefaultViewport(root);
226284
- const testsDir = resolve21(root, flagStr(args, "dir") ?? "tests");
226285
- const designMetaPath = resolve21(root, flagStr(args, "design-dir") ?? "design", "meta.json");
226944
+ const testsDir = resolve23(root, flagStr(args, "dir") ?? "tests");
226945
+ const designMetaPath = resolve23(root, flagStr(args, "design-dir") ?? "design", "meta.json");
226286
226946
  const asJson = flagBool(args, "json");
226287
226947
  const out = asJson ? logErr : log;
226288
226948
  const cases = [];
226289
226949
  if (args._.length > 0) {
226290
226950
  for (const given of args._) {
226291
- const candidates = [resolve21(given), resolve21(testsDir, given), resolve21(testsDir, `${given}.flow.json`)];
226292
- const abs = candidates.find((p2) => existsSync37(p2));
226951
+ const candidates = [resolve23(given), resolve23(testsDir, given), resolve23(testsDir, `${given}.flow.json`)];
226952
+ const abs = candidates.find((p2) => existsSync42(p2));
226293
226953
  if (!abs)
226294
226954
  throw new CliError(`No such test: "${given}" (looked in ${testsDir}).`);
226295
226955
  cases.push(readFlowFile(abs, given));
226296
226956
  }
226297
- } else if (existsSync37(testsDir)) {
226298
- for (const f3 of readdirSync18(testsDir).filter((f4) => f4.endsWith(".flow.json")).sort()) {
226299
- cases.push(readFlowFile(resolve21(testsDir, f3), f3));
226957
+ } else if (existsSync42(testsDir)) {
226958
+ for (const f3 of readdirSync20(testsDir).filter((f4) => f4.endsWith(".flow.json")).sort()) {
226959
+ cases.push(readFlowFile(resolve23(testsDir, f3), f3));
226300
226960
  }
226301
226961
  }
226302
226962
  const fixturesFlag = flagStr(args, "fixtures");
226303
- const defaultPack = resolve21(testsDir, "fixtures.json");
226304
- const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") : existsSync37(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
226963
+ const defaultPack = resolve23(testsDir, "fixtures.json");
226964
+ const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") : existsSync42(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
226305
226965
  const flowsOnly = flagBool(args, "flows");
226306
226966
  const wantFlows = !flagBool(args, "no-flows") && args._.length === 0;
226307
226967
  if (flowsOnly)
226308
226968
  cases.length = 0;
226309
226969
  let skipped = [];
226310
226970
  let entryScreen = null;
226311
- if (desktop2 && wantFlows && existsSync37(designMetaPath)) {
226971
+ if (desktop2 && wantFlows && existsSync42(designMetaPath)) {
226312
226972
  note("Design-derived flows skipped with --desktop (they tap by name; run `lecodes test` headless for them).");
226313
226973
  }
226314
- if (!desktop2 && (wantFlows || flowsOnly) && existsSync37(designMetaPath)) {
226974
+ if (!desktop2 && (wantFlows || flowsOnly) && existsSync42(designMetaPath)) {
226315
226975
  let meta;
226316
226976
  try {
226317
- meta = JSON.parse(readFileSync33(designMetaPath, "utf8"));
226977
+ meta = JSON.parse(readFileSync36(designMetaPath, "utf8"));
226318
226978
  } catch (e2) {
226319
226979
  throw new CliError(`Can't read ${designMetaPath}: ${e2 instanceof Error ? e2.message : String(e2)}`);
226320
226980
  }
@@ -226346,18 +227006,19 @@ var test = async (args) => {
226346
227006
  }, timeoutMs);
226347
227007
  killer.unref?.();
226348
227008
  const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
226349
- const artifactsRoot = resolve21(root, flagStr(args, "artifacts") ?? resolve21(testsDir, ".artifacts"));
227009
+ const artifactsRoot = resolve23(root, flagStr(args, "artifacts") ?? resolve23(testsDir, ".artifacts"));
226350
227010
  const results = [];
226351
227011
  const runNativeCase = async (kase) => {
226352
- const caseDir = resolve21(artifactsRoot, slug3(kase.name));
227012
+ const caseDir = resolve23(artifactsRoot, slug3(kase.name));
226353
227013
  const plan = translateScenario(kase.steps, { outDir: caseDir, headroomFrames: nativeFrames });
226354
227014
  for (const st of plan.steps)
226355
227015
  if (st.screenshot)
226356
- mkdirSync26(dirname18(st.screenshot), { recursive: true });
226357
- mkdirSync26(caseDir, { recursive: true });
226358
- const finalPng = resolve21(caseDir, "final.png");
227016
+ mkdirSync28(dirname20(st.screenshot), { recursive: true });
227017
+ mkdirSync28(caseDir, { recursive: true });
227018
+ const finalPng = resolve23(caseDir, "final.png");
226359
227019
  const { output } = await runDesktopRender(native.js, native.resources, {
226360
227020
  renderer: desktopRenderer,
227021
+ pluginDirs: desktopPluginLibraries(root).dirs,
226361
227022
  width: Number(flagStr(args, "width")) || kase.width || projectViewport?.width || 390,
226362
227023
  height: Number(flagStr(args, "height")) || kase.height || projectViewport?.height || 844,
226363
227024
  frames: plan.frameCap,
@@ -226407,14 +227068,14 @@ var test = async (args) => {
226407
227068
  onConsole
226408
227069
  });
226409
227070
  try {
226410
- const io = fileScenarioIO(resolve21(artifactsRoot, slug3(kase.name)));
227071
+ const io = fileScenarioIO(resolve23(artifactsRoot, slug3(kase.name)));
226411
227072
  const outcome = await renderer.runScenario(session, steps, io);
226412
227073
  result = {
226413
227074
  name: kase.name,
226414
227075
  kind: kase.kind,
226415
227076
  ok: outcome.ok,
226416
227077
  steps: outcome.steps.map((s2) => ({ desc: s2.desc, ok: s2.ok, error: s2.error })),
226417
- artifacts: outcome.failureArtifacts.map((p2) => resolve21(artifactsRoot, slug3(kase.name), p2))
227078
+ artifacts: outcome.failureArtifacts.map((p2) => resolve23(artifactsRoot, slug3(kase.name), p2))
226418
227079
  };
226419
227080
  } finally {
226420
227081
  session.dispose();
@@ -226477,41 +227138,14 @@ var test = async (args) => {
226477
227138
  };
226478
227139
 
226479
227140
  // src/commands/update.ts
226480
- import { spawnSync as spawnSync7 } from "node:child_process";
226481
- import { existsSync as existsSync39, readdirSync as readdirSync19 } from "node:fs";
226482
- import { homedir as homedir9 } from "node:os";
226483
- import { dirname as dirname20, join as join40, resolve as resolve23 } from "node:path";
227141
+ init_package();
226484
227142
  init_util();
226485
-
226486
- // src/peers.ts
226487
- import { existsSync as existsSync38, readFileSync as readFileSync34 } from "node:fs";
226488
- import { createRequire as createRequire6 } from "node:module";
226489
- import { dirname as dirname19, join as join39 } from "node:path";
226490
- var OPTIONAL_PEERS = [
226491
- ["lecodes-renderer", "lecodes-renderer/headless"],
226492
- ["lecodes-design", "lecodes-design/server"],
226493
- ["lecodes-3d-editor", "lecodes-3d-editor/server"],
226494
- ["lecodes-assets", "lecodes-assets/cli"]
226495
- ];
226496
- var peerVersion = (name, probe) => {
226497
- try {
226498
- const require2 = createRequire6(import.meta.url);
226499
- let dir = dirname19(require2.resolve(probe));
226500
- for (let depth = 0;depth < 16; depth++) {
226501
- const manifest = join39(dir, "package.json");
226502
- if (existsSync38(manifest)) {
226503
- const parsed = JSON.parse(readFileSync34(manifest, "utf8"));
226504
- if (parsed.name === name)
226505
- return parsed.version ?? "unknown";
226506
- }
226507
- const parent = dirname19(dir);
226508
- if (parent === dir)
226509
- break;
226510
- dir = parent;
226511
- }
226512
- } catch {}
226513
- return null;
226514
- };
227143
+ init_peers();
227144
+ init_peerInstall();
227145
+ import { spawnSync as spawnSync9 } from "node:child_process";
227146
+ import { existsSync as existsSync43, readdirSync as readdirSync21 } from "node:fs";
227147
+ import { homedir as homedir10 } from "node:os";
227148
+ import { dirname as dirname21, join as join44, resolve as resolve25 } from "node:path";
226515
227149
 
226516
227150
  // src/commands/types.ts
226517
227151
  init_api();
@@ -226519,11 +227153,11 @@ init_config();
226519
227153
  init_manifest();
226520
227154
  init_types4();
226521
227155
  init_util();
226522
- import { resolve as resolve22 } from "node:path";
227156
+ import { resolve as resolve24 } from "node:path";
226523
227157
  var label = (bundle) => `${Object.keys(bundle).length} file(s)`;
226524
227158
  var types4 = async (args) => {
226525
227159
  const dirArg = args._[0];
226526
- const root = dirArg ? resolve22(dirArg) : (() => {
227160
+ const root = dirArg ? resolve24(dirArg) : (() => {
226527
227161
  try {
226528
227162
  return findProjectRoot(process.cwd());
226529
227163
  } catch {
@@ -226583,12 +227217,12 @@ var types4 = async (args) => {
226583
227217
 
226584
227218
  // src/commands/update.ts
226585
227219
  var findProjectDir = (start) => {
226586
- const home = resolve23(homedir9());
226587
- let dir = resolve23(start);
227220
+ const home = resolve25(homedir10());
227221
+ let dir = resolve25(start);
226588
227222
  while (true) {
226589
- if (dir !== home && (existsSync39(join40(dir, "app.json")) || existsSync39(join40(dir, ".lecodes", "manifest.json")) || existsSync39(join40(dir, ".lecodes", "types"))))
227223
+ if (dir !== home && (existsSync43(join44(dir, "app.json")) || existsSync43(join44(dir, ".lecodes", "manifest.json")) || existsSync43(join44(dir, ".lecodes", "types"))))
226590
227224
  return dir;
226591
- const parent = dirname20(dir);
227225
+ const parent = dirname21(dir);
226592
227226
  if (parent === dir)
226593
227227
  return null;
226594
227228
  dir = parent;
@@ -226616,12 +227250,12 @@ var semverNewer2 = (a2, b) => {
226616
227250
  return 0;
226617
227251
  };
226618
227252
  var FROM_SOURCE = /[\\/]src[\\/]commands[\\/]/.test(import.meta.url);
226619
- var STANDALONE = existsSync39(join40(dirname20(process.execPath), "runtime")) && existsSync39(join40(dirname20(process.execPath), process.platform === "win32" ? "chisel.exe" : "chisel"));
227253
+ var STANDALONE = existsSync43(join44(dirname21(process.execPath), "runtime")) && existsSync43(join44(dirname21(process.execPath), process.platform === "win32" ? "chisel.exe" : "chisel"));
226620
227254
  var update = async (args) => {
226621
227255
  const steps = [];
226622
227256
  const heading = (name, what) => log(`
226623
227257
  ${c.bold(name)} ${c.dim(what)}`);
226624
- const run2 = async (name, what, fn) => {
227258
+ const run3 = async (name, what, fn) => {
226625
227259
  heading(name, what);
226626
227260
  try {
226627
227261
  const r2 = await fn();
@@ -226636,77 +227270,88 @@ ${c.bold(name)} ${c.dim(what)}`);
226636
227270
  }
226637
227271
  };
226638
227272
  const root = findProjectDir(process.cwd());
226639
- await run2("desktop", "LeCodes Desktop host (desktop run / render --desktop)", async () => {
227273
+ await run3("desktop", "LeCodes Desktop host (desktop run / render --desktop)", async () => {
226640
227274
  if (process.env.LECODES_DESKTOP_EXE)
226641
227275
  return ["skipped", `LECODES_DESKTOP_EXE=${process.env.LECODES_DESKTOP_EXE} (a local build; \`lecodes desktop update\` refreshes the cache anyway)`];
226642
227276
  await updateDesktop();
226643
227277
  return "ok";
226644
227278
  });
226645
- await run2("matc", "Filament material compiler for local shader compiles", async () => {
227279
+ await run3("matc", "Filament material compiler for local shader compiles", async () => {
226646
227280
  if (process.env.LECODES_MATC)
226647
227281
  return ["skipped", `LECODES_MATC=${process.env.LECODES_MATC} (a local matc)`];
226648
227282
  await updateMatc();
226649
227283
  return "ok";
226650
227284
  });
226651
- await run2("shaders", "recompile the project's .mat shaders with the current matc", async () => {
227285
+ await run3("shaders", "recompile the project's .mat shaders with the current matc", async () => {
226652
227286
  if (!root)
226653
227287
  return ["skipped", "not inside a project"];
226654
227288
  await shaders({ _: ["compile"], flags: {} });
226655
227289
  return "ok";
226656
227290
  });
226657
- await run2("types", flagBool(args, "local") ? "IDE types from the SDK bundled in this CLI" : "IDE types from the server", async () => {
227291
+ await run3("types", flagBool(args, "local") ? "IDE types from the SDK bundled in this CLI" : "IDE types from the server", async () => {
226658
227292
  if (!root)
226659
227293
  return ["skipped", "not inside a project"];
226660
227294
  await types4({ _: [], flags: flagBool(args, "local") ? { local: true } : {} });
226661
227295
  return "ok";
226662
227296
  });
226663
- await run2("shells", "ios/ + android/ pins → newest SDK release + plugin tags, then re-sync", async () => {
227297
+ await run3("shells", "ios/ + android/ pins → newest SDK release + plugin tags, then re-sync", async () => {
226664
227298
  if (!root)
226665
227299
  return ["skipped", "not inside a project"];
226666
227300
  if (flagBool(args, "no-shells"))
226667
227301
  return ["skipped", "--no-shells"];
226668
- const present = ["ios", "android"].filter((d) => existsSync39(join40(root, d)) && readdirSync19(join40(root, d)).length > 0);
227302
+ const present = ["ios", "android"].filter((d) => existsSync43(join44(root, d)) && readdirSync21(join44(root, d)).length > 0);
226669
227303
  if (present.length === 0)
226670
227304
  return ["skipped", "no ios/ or android/ shell in this project (`lecodes app init ios|android`)"];
226671
227305
  await app({ _: ["update"], flags: {} });
226672
227306
  return "ok";
226673
227307
  });
226674
- await run2("cli", "lecodes-cli + its optional peers (global npm installs)", async () => {
227308
+ await run3("cli", "lecodes-cli + its optional peers (global npm installs)", async () => {
226675
227309
  if (flagBool(args, "no-cli"))
226676
227310
  return ["skipped", "--no-cli"];
226677
227311
  if (FROM_SOURCE)
226678
227312
  return ["skipped", "running from the repo checkout — the CLI and its peers are the repo's own"];
226679
227313
  const rows = [];
226680
227314
  for (const [name, probe] of OPTIONAL_PEERS) {
227315
+ const where = peerSource(name, probe);
226681
227316
  const installed = peerVersion(name, probe);
226682
- rows.push({ name, installed, latest: installed ? await npmLatest(name) : null });
227317
+ const latest = !installed ? null : where === "prefix" && name === "lecodes-renderer" ? package_default.version : await npmLatest(name);
227318
+ rows.push({ name, installed, latest, where });
226683
227319
  }
226684
- rows.push({ name: "lecodes-cli", installed: package_default.version, latest: await npmLatest("lecodes-cli") });
226685
- if (rows.every((r3) => r3.latest === null))
227320
+ rows.push({ name: "lecodes-cli", installed: package_default.version, latest: await npmLatest("lecodes-cli"), where: "global" });
227321
+ if (rows.every((r2) => r2.latest === null))
226686
227322
  return ["skipped", "npm unreachable"];
226687
- const behind = rows.filter((r3) => r3.installed && r3.latest && semverNewer2(r3.latest, r3.installed) > 0);
226688
- for (const r3 of rows) {
226689
- const state = !r3.installed ? c.dim("not installed (optional — npm install -g " + r3.name + ")") : !r3.latest ? c.dim("(npm: unknown)") : behind.includes(r3) ? c.yellow("→ " + r3.latest) : c.dim("(up to date)");
226690
- log(" " + r3.name.padEnd(18) + " " + (r3.installed ?? "").padEnd(8) + " " + state);
227323
+ const behind = rows.filter((r2) => r2.installed && r2.latest && semverNewer2(r2.latest, r2.installed) > 0);
227324
+ for (const r2 of rows) {
227325
+ const state = !r2.installed ? c.dim("not installed (optional — installed on first use)") : !r2.latest ? c.dim("(npm: unknown)") : behind.includes(r2) ? c.yellow("→ " + r2.latest) : c.dim("(up to date)");
227326
+ log(" " + r2.name.padEnd(18) + " " + (r2.installed ?? "").padEnd(8) + " " + (r2.where === "prefix" ? c.dim("peers ") : "") + state);
226691
227327
  }
226692
227328
  if (behind.length === 0)
226693
227329
  return "ok";
226694
- const specs = behind.map((r3) => r3.name + "@latest");
226695
- if (flagBool(args, "check"))
226696
- return ["updated", behind.length + " behind — run: npm install -g " + specs.join(" ")];
226697
- if (STANDALONE && behind.some((r3) => r3.name === "lecodes-cli")) {
227330
+ const prefixBehind = behind.filter((r2) => r2.where === "prefix");
227331
+ const globalSpecs = behind.filter((r2) => r2.where !== "prefix").map((r2) => r2.name + "@latest");
227332
+ if (flagBool(args, "check")) {
227333
+ return ["updated", behind.length + " behind — run: lecodes update" + (globalSpecs.length ? " (global: npm install -g " + globalSpecs.join(" ") + ")" : "")];
227334
+ }
227335
+ const done = [];
227336
+ if (prefixBehind.length) {
227337
+ await installPeers(prefixBehind.map((r2) => r2.name), "lecodes update", { yes: true });
227338
+ done.push(...prefixBehind.map((r2) => r2.name));
227339
+ }
227340
+ if (STANDALONE && globalSpecs.some((s2) => s2.startsWith("lecodes-cli@"))) {
226698
227341
  note("this is the standalone build — the CLI updates by re-installing the release archive, not through npm; peers still go through npm");
226699
227342
  }
226700
- const toInstall = STANDALONE ? specs.filter((s2) => !s2.startsWith("lecodes-cli@")) : specs;
226701
- if (toInstall.length === 0)
227343
+ const toInstall = STANDALONE ? globalSpecs.filter((s2) => !s2.startsWith("lecodes-cli@")) : globalSpecs;
227344
+ if (toInstall.length) {
227345
+ log(" " + c.dim("$") + " npm install -g " + toInstall.join(" "));
227346
+ const r2 = spawnSync9("npm", ["install", "-g", ...toInstall], { stdio: "inherit", shell: true });
227347
+ if (r2.status !== 0)
227348
+ throw new CliError("npm install -g exited with " + (r2.status ?? "a signal"));
227349
+ done.push(...toInstall.map((s2) => s2.replace("@latest", "")));
227350
+ }
227351
+ if (done.length === 0)
226702
227352
  return ["skipped", "only the standalone CLI itself is behind — download the release"];
226703
- log(" " + c.dim("$") + " npm install -g " + toInstall.join(" "));
226704
- const r2 = spawnSync7("npm", ["install", "-g", ...toInstall], { stdio: "inherit", shell: true });
226705
- if (r2.status !== 0)
226706
- throw new CliError("npm install -g exited with " + (r2.status ?? "a signal"));
226707
- const names3 = toInstall.map((s2) => s2.replace("@latest", "")).join(", ");
226708
- const cliToo = toInstall.some((s2) => s2.startsWith("lecodes-cli@"));
226709
- return ["updated", names3 + " → latest" + (cliToo ? " (the new CLI runs from your next command)" : "")];
227353
+ const cliToo = done.includes("lecodes-cli");
227354
+ return ["updated", done.join(", ") + " → latest" + (cliToo ? " (the new CLI runs from your next command)" : "")];
226710
227355
  });
226711
227356
  log(`
226712
227357
  ${c.bold("Summary")}`);
@@ -226720,27 +227365,27 @@ ${c.bold("Summary")}`);
226720
227365
  };
226721
227366
 
226722
227367
  // src/index.ts
226723
- import { existsSync as existsSync42, realpathSync as realpathSync2 } from "node:fs";
226724
- import { delimiter as delimiter2, dirname as dirname23, join as join43 } from "node:path";
227368
+ import { existsSync as existsSync46, realpathSync as realpathSync2 } from "node:fs";
227369
+ import { delimiter as delimiter3, dirname as dirname24, join as join47 } from "node:path";
226725
227370
 
226726
227371
  // src/commands/install.ts
226727
227372
  init_util();
226728
227373
  import { execFileSync as execFileSync8 } from "node:child_process";
226729
- import { appendFileSync as appendFileSync3, chmodSync as chmodSync2, cpSync as cpSync5, existsSync as existsSync40, mkdirSync as mkdirSync27, readFileSync as readFileSync35, realpathSync, rmSync as rmSync13, symlinkSync as symlinkSync2 } from "node:fs";
226730
- import { homedir as homedir10 } from "node:os";
226731
- import { basename as basename18, delimiter, dirname as dirname21, join as join41, resolve as resolve24, sep as sep2 } from "node:path";
227374
+ import { appendFileSync as appendFileSync3, chmodSync as chmodSync2, cpSync as cpSync5, existsSync as existsSync44, mkdirSync as mkdirSync29, readFileSync as readFileSync37, realpathSync, rmSync as rmSync13, symlinkSync as symlinkSync2 } from "node:fs";
227375
+ import { homedir as homedir11 } from "node:os";
227376
+ import { basename as basename20, delimiter as delimiter2, dirname as dirname22, join as join45, resolve as resolve26, sep as sep2 } from "node:path";
226732
227377
  var isWin = process.platform === "win32";
226733
- var HOME = homedir10();
227378
+ var HOME = homedir11();
226734
227379
  var safeRealpath = (p2) => {
226735
227380
  try {
226736
227381
  return realpathSync(p2);
226737
227382
  } catch {
226738
- return resolve24(p2);
227383
+ return resolve26(p2);
226739
227384
  }
226740
227385
  };
226741
- var onPath = (dir) => {
227386
+ var onPath2 = (dir) => {
226742
227387
  const want = safeRealpath(dir);
226743
- return (process.env.PATH ?? "").split(delimiter).some((p2) => {
227388
+ return (process.env.PATH ?? "").split(delimiter2).some((p2) => {
226744
227389
  if (!p2)
226745
227390
  return false;
226746
227391
  try {
@@ -226751,25 +227396,25 @@ var onPath = (dir) => {
226751
227396
  });
226752
227397
  };
226753
227398
  var install = async (args) => {
226754
- const bundle = dirname21(process.execPath);
226755
- const exeName3 = basename18(process.execPath);
227399
+ const bundle = dirname22(process.execPath);
227400
+ const exeName3 = basename20(process.execPath);
226756
227401
  const chiselName = isWin ? "chisel.exe" : "chisel";
226757
- if (!existsSync40(join41(bundle, "runtime")) || !existsSync40(join41(bundle, chiselName))) {
227402
+ if (!existsSync44(join45(bundle, "runtime")) || !existsSync44(join45(bundle, chiselName))) {
226758
227403
  throw new CliError("`lecodes install` only works from the packaged release build (the archive containing lecodes + " + "chisel + runtime/). You appear to be running from source or an npm install — install globally " + "with your package manager there instead.");
226759
227404
  }
226760
- const companions = [exeName3, chiselName, "runtime", "README.txt"].filter((f3) => existsSync40(join41(bundle, f3)));
227405
+ const companions2 = [exeName3, chiselName, "runtime", "README.txt"].filter((f3) => existsSync44(join45(bundle, f3)));
226761
227406
  if (isWin)
226762
- return installWindows(args, bundle, companions);
226763
- return installUnix(args, bundle, companions);
227407
+ return installWindows(args, bundle, companions2);
227408
+ return installUnix(args, bundle, companions2);
226764
227409
  };
226765
- var copyBundle = (bundle, target, companions) => {
226766
- mkdirSync27(target, { recursive: true });
227410
+ var copyBundle = (bundle, target, companions2) => {
227411
+ mkdirSync29(target, { recursive: true });
226767
227412
  if (safeRealpath(target) === safeRealpath(bundle))
226768
227413
  return;
226769
- for (const f3 of companions) {
226770
- const dest = join41(target, f3);
227414
+ for (const f3 of companions2) {
227415
+ const dest = join45(target, f3);
226771
227416
  rmSync13(dest, { recursive: true, force: true });
226772
- cpSync5(join41(bundle, f3), dest, { recursive: true });
227417
+ cpSync5(join45(bundle, f3), dest, { recursive: true });
226773
227418
  }
226774
227419
  };
226775
227420
  var permError = (e2, dir, system) => {
@@ -226779,43 +227424,43 @@ var permError = (e2, dir, system) => {
226779
227424
  }
226780
227425
  return new CliError(`Install failed: ${e2 instanceof Error ? e2.message : String(e2)}`);
226781
227426
  };
226782
- var installUnix = (args, bundle, companions) => {
227427
+ var installUnix = (args, bundle, companions2) => {
226783
227428
  const system = flagBool(args, "system");
226784
- const libDir = resolve24(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join41(HOME, ".local", "lib", "lecodes-cli")));
226785
- const binDir = resolve24(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join41(HOME, ".local", "bin")));
226786
- const link2 = join41(binDir, "lecodes");
227429
+ const libDir = resolve26(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join45(HOME, ".local", "lib", "lecodes-cli")));
227430
+ const binDir = resolve26(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join45(HOME, ".local", "bin")));
227431
+ const link2 = join45(binDir, "lecodes");
226787
227432
  try {
226788
- copyBundle(bundle, libDir, companions);
226789
- chmodSync2(join41(libDir, "lecodes"), 493);
226790
- chmodSync2(join41(libDir, "chisel"), 493);
227433
+ copyBundle(bundle, libDir, companions2);
227434
+ chmodSync2(join45(libDir, "lecodes"), 493);
227435
+ chmodSync2(join45(libDir, "chisel"), 493);
226791
227436
  } catch (e2) {
226792
227437
  throw permError(e2, libDir, system);
226793
227438
  }
226794
227439
  try {
226795
- mkdirSync27(binDir, { recursive: true });
227440
+ mkdirSync29(binDir, { recursive: true });
226796
227441
  rmSync13(link2, { force: true });
226797
- symlinkSync2(join41(libDir, "lecodes"), link2);
227442
+ symlinkSync2(join45(libDir, "lecodes"), link2);
226798
227443
  } catch (e2) {
226799
227444
  throw permError(e2, binDir, system);
226800
227445
  }
226801
227446
  success(`Installed to ${c.bold(libDir)}`);
226802
- log(` ${c.dim(link2 + " → " + join41(libDir, "lecodes"))}`);
227447
+ log(` ${c.dim(link2 + " → " + join45(libDir, "lecodes"))}`);
226803
227448
  finishPathUnix(args, binDir);
226804
227449
  };
226805
227450
  var shellRc = () => {
226806
- const shell = basename18(process.env.SHELL ?? "");
227451
+ const shell = basename20(process.env.SHELL ?? "");
226807
227452
  if (shell === "zsh")
226808
- return { file: join41(HOME, ".zshrc"), name: "~/.zshrc" };
227453
+ return { file: join45(HOME, ".zshrc"), name: "~/.zshrc" };
226809
227454
  if (shell === "bash")
226810
- return { file: join41(HOME, ".bashrc"), name: "~/.bashrc" };
226811
- return { file: join41(HOME, ".profile"), name: "~/.profile" };
227455
+ return { file: join45(HOME, ".bashrc"), name: "~/.bashrc" };
227456
+ return { file: join45(HOME, ".profile"), name: "~/.profile" };
226812
227457
  };
226813
227458
  var PATH_MARKER = "# Added by `lecodes install`";
226814
227459
  var appendPathToShellRc = (binDir) => {
226815
227460
  const { file, name } = shellRc();
226816
227461
  const line = `export PATH="${binDir}:$PATH"`;
226817
227462
  try {
226818
- const existing = existsSync40(file) ? readFileSync35(file, "utf8") : "";
227463
+ const existing = existsSync44(file) ? readFileSync37(file, "utf8") : "";
226819
227464
  if (existing.includes(line))
226820
227465
  return name;
226821
227466
  appendFileSync3(file, `${existing && !existing.endsWith(`
@@ -226830,7 +227475,7 @@ ${line}
226830
227475
  }
226831
227476
  };
226832
227477
  var finishPathUnix = (args, binDir) => {
226833
- if (onPath(binDir)) {
227478
+ if (onPath2(binDir)) {
226834
227479
  log(` Run ${c.bold("lecodes --version")} in a new shell to verify.`);
226835
227480
  return;
226836
227481
  }
@@ -226852,15 +227497,15 @@ var finishPathUnix = (args, binDir) => {
226852
227497
  log(` • let install do it: re-run with ${c.bold("--modify-path")}`);
226853
227498
  log(` • or install system-wide (no PATH edit): ${c.bold("sudo lecodes install --system")}`);
226854
227499
  };
226855
- var installWindows = (args, bundle, companions) => {
226856
- const target = resolve24(flagStr(args, "dir") ?? join41(process.env.LOCALAPPDATA ?? join41(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
227500
+ var installWindows = (args, bundle, companions2) => {
227501
+ const target = resolve26(flagStr(args, "dir") ?? join45(process.env.LOCALAPPDATA ?? join45(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
226857
227502
  try {
226858
- copyBundle(bundle, target, companions);
227503
+ copyBundle(bundle, target, companions2);
226859
227504
  } catch (e2) {
226860
227505
  throw new CliError(`Install failed writing to ${target}: ${e2 instanceof Error ? e2.message : String(e2)}`);
226861
227506
  }
226862
227507
  success(`Installed to ${c.bold(target)}`);
226863
- if (onPath(target)) {
227508
+ if (onPath2(target)) {
226864
227509
  log(` Already on PATH — run ${c.bold("lecodes --version")} in a new terminal.`);
226865
227510
  return;
226866
227511
  }
@@ -226892,9 +227537,9 @@ var addToUserPathWindows = (dir) => {
226892
227537
  init_util();
226893
227538
 
226894
227539
  // src/projectEnv.ts
226895
- import { existsSync as existsSync41, readFileSync as readFileSync36 } from "node:fs";
226896
- import { homedir as homedir11 } from "node:os";
226897
- import { dirname as dirname22, join as join42, resolve as resolve25 } from "node:path";
227540
+ import { existsSync as existsSync45, readFileSync as readFileSync38 } from "node:fs";
227541
+ import { homedir as homedir12 } from "node:os";
227542
+ import { dirname as dirname23, join as join46, resolve as resolve27 } from "node:path";
226898
227543
  var KEY_RE = /^(LECODES|CREATOR)_[A-Z0-9_]+$/;
226899
227544
  var parseDotEnv = (text) => {
226900
227545
  const out = {};
@@ -226929,19 +227574,22 @@ var applyDotEnv = (parsed, dir, env) => {
226929
227574
  continue;
226930
227575
  let value = raw;
226931
227576
  if (value.startsWith("./") || value.startsWith("../"))
226932
- value = resolve25(dir, value);
227577
+ value = resolve27(dir, value);
226933
227578
  else if (value === "~" || value.startsWith("~/"))
226934
- value = join42(homedir11(), value.slice(1));
227579
+ value = join46(homedir12(), value.slice(1));
226935
227580
  out[key] = value;
226936
227581
  }
226937
227582
  return out;
226938
227583
  };
226939
227584
  var findEnvRoot = (start) => {
226940
- let dir = resolve25(start);
227585
+ let dir = resolve27(start);
227586
+ const home = resolve27(homedir12());
226941
227587
  while (true) {
226942
- if (existsSync41(join42(dir, "app.json")) || existsSync41(join42(dir, ".lecodes")))
227588
+ if (dir === home)
227589
+ return null;
227590
+ if (existsSync45(join46(dir, "app.json")) || existsSync45(join46(dir, ".lecodes")))
226943
227591
  return dir;
226944
- const parent = dirname22(dir);
227592
+ const parent = dirname23(dir);
226945
227593
  if (parent === dir)
226946
227594
  return null;
226947
227595
  dir = parent;
@@ -226952,10 +227600,10 @@ var loadProjectEnv = (cwd = process.cwd()) => {
226952
227600
  const root = findEnvRoot(cwd);
226953
227601
  if (!root)
226954
227602
  return [];
226955
- const file = join42(root, ".env");
226956
- if (!existsSync41(file))
227603
+ const file = join46(root, ".env");
227604
+ if (!existsSync45(file))
226957
227605
  return [];
226958
- const added = applyDotEnv(parseDotEnv(readFileSync36(file, "utf8")), root, process.env);
227606
+ const added = applyDotEnv(parseDotEnv(readFileSync38(file, "utf8")), root, process.env);
226959
227607
  for (const [k, v] of Object.entries(added))
226960
227608
  process.env[k] = v;
226961
227609
  return Object.keys(added);
@@ -226965,10 +227613,11 @@ var loadProjectEnv = (cwd = process.cwd()) => {
226965
227613
  };
226966
227614
 
226967
227615
  // src/index.ts
227616
+ init_peers();
226968
227617
  var STANDALONE2 = (() => {
226969
- const dir = dirname23(process.execPath);
227618
+ const dir = dirname24(process.execPath);
226970
227619
  const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
226971
- return existsSync42(join43(dir, "runtime")) && existsSync42(join43(dir, chisel));
227620
+ return existsSync46(join47(dir, "runtime")) && existsSync46(join47(dir, chisel));
226972
227621
  })();
226973
227622
  var INSTALLED = STANDALONE2 && (() => {
226974
227623
  const exe = process.platform === "win32" ? "lecodes.exe" : "lecodes";
@@ -226979,12 +227628,12 @@ var INSTALLED = STANDALONE2 && (() => {
226979
227628
  return process.execPath;
226980
227629
  }
226981
227630
  })();
226982
- return (process.env.PATH ?? "").split(delimiter2).some((d) => {
227631
+ return (process.env.PATH ?? "").split(delimiter3).some((d) => {
226983
227632
  if (!d)
226984
227633
  return false;
226985
227634
  try {
226986
- const cand = join43(d, exe);
226987
- return existsSync42(cand) && realpathSync2(cand) === self2;
227635
+ const cand = join47(d, exe);
227636
+ return existsSync46(cand) && realpathSync2(cand) === self2;
226988
227637
  } catch {
226989
227638
  return false;
226990
227639
  }
@@ -227052,6 +227701,17 @@ ${c.bold("Commands:")}
227052
227701
  --host <ip> LAN address to advertise (default: auto-detect)
227053
227702
  --entry <path> override the entrypoint
227054
227703
  --no-qr don't render the QR code
227704
+ --android open the dev url in the LeCodes app on the phone
227705
+ plugged in over adb (adb reverse: no Wi-Fi/LAN
227706
+ address needed). --serial <id> picks a device,
227707
+ --apk <file> installs first, --package <name>
227708
+ (default com.letary.lecodes). Device logs stream
227709
+ here. --time <s> --png <file>: bounded check —
227710
+ screenshot after <s> seconds, force-stop, exit
227711
+ (exit 1 on a crash). Ctrl+C force-stops the app.
227712
+ --web also open the app in the browser (Web Lite host:
227713
+ same bundle + hot reload, camera/map plugins
227714
+ have web twins; the /web/ URL is served always)
227055
227715
  --desktop also open the app in the native desktop host
227056
227716
  (same hot reload; window from app.json's
227057
227717
  desktop block; closing it keeps the server)
@@ -227244,12 +227904,15 @@ ${c.bold("Commands:")}
227244
227904
  --cell <m> (0.25) --cell-height <m> (0.1) --tile <cells> (48)
227245
227905
  --obj also write <n>.navmesh.obj to inspect
227246
227906
  --frames <n> load headroom (900) --logs host output
227247
- shaders [compile|update] The project's custom shaders (.mat) and their local compile:
227907
+ shaders [compile|update|new] The project's custom shaders (.mat) and their local compile:
227248
227908
  no args lists the shaders + their compile state; compile
227249
227909
  compiles them all into .lecodes/shaders (the same pass
227250
227910
  'lecodes dev' runs on the fly — via Filament's matc,
227251
227911
  downloaded on first use to ~/.lecodes/matc); update
227252
- downloads the newest matc (run after an engine upgrade)
227912
+ downloads the newest matc (run after an engine upgrade);
227913
+ new <name> [--ref lit] [--dir d] [--force] writes a fresh
227914
+ .mat + .material.ts: the starter, or a copy of the engine's
227915
+ own lit / unlit / particles / … source to start from
227253
227916
  scene [path] LeCodes Scene Editor — visually edit a .scene.ts (tree, inspector,
227254
227917
  gizmo) on local files (needs the optional 'lecodes-3d-editor' package;
227255
227918
  works inside a cloned project or standalone in any folder)
@@ -227364,12 +228027,13 @@ LECODES_DESKTOP_EXE, LECODES_MATC, LECODES_NOTARY_PROFILE, LECODES_MACOS_SIGN_ID
227364
228027
  var commands = { init: init2, login, create, clone: clone3, link, status: status3, diff: diff2, pull, push, compile: compile2, preview: previewLegacy, render, test, design, scene, thumbs, dev, app, pn, desktop, shaders, assets, lightmap, navmesh, update };
227365
228028
  if (STANDALONE2)
227366
228029
  commands.install = install;
227367
- var readVersion = () => package_default.version ?? "unknown";
228030
+ var readVersion2 = () => package_default.version ?? "unknown";
227368
228031
  var printVersion = () => {
227369
- const rows = [["lecodes-cli", readVersion()]];
228032
+ const rows = [["lecodes-cli", readVersion2()]];
227370
228033
  for (const [name, probe] of OPTIONAL_PEERS) {
227371
228034
  const version = peerVersion(name, probe);
227372
- rows.push([name, version ?? c.dim("not installed")]);
228035
+ const where = peerSource(name, probe);
228036
+ rows.push([name, version ? where === "prefix" ? `${version} ${c.dim("(~/.lecodes/peers)")}` : version : c.dim("not installed — installed on first use")]);
227373
228037
  }
227374
228038
  const desktopVersion = selectedDesktopVersion();
227375
228039
  rows.push(["lecodes-desktop", installedDesktopVersions().includes(desktopVersion) ? desktopVersion : `${desktopVersion} ${c.dim("(not downloaded — fetched on first `render --desktop`)")}`]);
@@ -227383,6 +228047,8 @@ var main = async () => {
227383
228047
  const argv = process.argv.slice(2);
227384
228048
  const command = argv[0];
227385
228049
  loadProjectEnv();
228050
+ if (argv.includes("--yes") || argv.includes("-y"))
228051
+ process.env.LECODES_YES = "1";
227386
228052
  if (!command || command === "help" || command === "--help" || command === "-h") {
227387
228053
  log(HELP);
227388
228054
  return;
@@ -227391,14 +228057,14 @@ var main = async () => {
227391
228057
  printVersion();
227392
228058
  return;
227393
228059
  }
227394
- const run2 = commands[command];
227395
- if (!run2) {
228060
+ const run3 = commands[command];
228061
+ if (!run3) {
227396
228062
  log(c.red(`Unknown command: ${command}`));
227397
228063
  log(HELP);
227398
228064
  process.exitCode = 1;
227399
228065
  return;
227400
228066
  }
227401
- await run2(Object.assign(parseArgs(argv.slice(1)), { _raw: argv.slice(1) }));
228067
+ await run3(Object.assign(parseArgs(argv.slice(1)), { _raw: argv.slice(1) }));
227402
228068
  };
227403
228069
  main().catch((e2) => {
227404
228070
  if (e2 instanceof CliError)