lecodes-cli 0.18.1 → 0.18.2

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 (33) hide show
  1. package/dist/index.js +1008 -488
  2. package/package.json +1 -1
  3. package/runtime/scene-harness.json +1 -1
  4. package/runtime/sdk/gl/Camera.ts +39 -0
  5. package/runtime/sdk/gl/CameraPlace.ts +52 -52
  6. package/runtime/sdk/gl/Scene.ts +13 -0
  7. package/runtime/sdk/gl/Trigger.ts +45 -45
  8. package/runtime/sdk/gl/scenarios.ts +291 -291
  9. package/runtime/sdk/gl/state.ts +6 -6
  10. package/runtime/sdk/runtime/device.ts +102 -1
  11. package/runtime/sdk/scene/defineScene.ts +1227 -1227
  12. package/runtime/sdk/scene/gizmos.ts +148 -148
  13. package/runtime/sdk-types.json +1 -1
  14. package/runtime/sdk/compile/aspectMacro.ts +0 -86
  15. package/runtime/sdk/compile/assetIconMacro.ts +0 -384
  16. package/runtime/sdk/compile/assetMacro.ts +0 -146
  17. package/runtime/sdk/compile/assetName.ts +0 -50
  18. package/runtime/sdk/compile/bundler.ts +0 -287
  19. package/runtime/sdk/compile/compileProject.ts +0 -129
  20. package/runtime/sdk/compile/detectEntry.ts +0 -128
  21. package/runtime/sdk/compile/fontMacro.ts +0 -459
  22. package/runtime/sdk/compile/fontRegistry.ts +0 -78
  23. package/runtime/sdk/compile/header.ts +0 -67
  24. package/runtime/sdk/compile/index.ts +0 -100
  25. package/runtime/sdk/compile/libraryImports.ts +0 -52
  26. package/runtime/sdk/compile/liteMaterial.ts +0 -247
  27. package/runtime/sdk/compile/sceneEditor.ts +0 -88
  28. package/runtime/sdk/compile/serverSplit.ts +0 -233
  29. package/runtime/sdk/compile/serverTypes.ts +0 -227
  30. package/runtime/sdk/compile/sfnt.ts +0 -98
  31. package/runtime/sdk/compile/shaderSchema.ts +0 -202
  32. package/runtime/sdk/compile/shaderTargets.ts +0 -81
  33. package/runtime/sdk/compile/sourcemap.ts +0 -25
package/dist/index.js CHANGED
@@ -418,7 +418,7 @@ node_modules/
418
418
  return "updated";
419
419
  };
420
420
  var init_ignore = __esm(() => {
421
- BUILTIN_PATTERNS = ["node_modules/", ".git/", "_*", "*.log", ".DS_Store", ".artifacts/", "dist/"];
421
+ BUILTIN_PATTERNS = ["node_modules/", ".git/", "_*", "*.log", ".DS_Store", ".artifacts/", "dist/", ".env", ".env.*"];
422
422
  });
423
423
 
424
424
  // src/localFiles.ts
@@ -3174,7 +3174,7 @@ var require_source_map_support = __commonJS((exports, module) => {
3174
3174
 
3175
3175
  // ../../node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/typescript.js
3176
3176
  var require_typescript = __commonJS((exports, module) => {
3177
- var __dirname = "C:\\Users\\Den\\Desktop\\LeCodes\\lecodes\\node_modules\\.bun\\typescript@5.8.3\\node_modules\\typescript\\lib", __filename = "C:\\Users\\Den\\Desktop\\LeCodes\\lecodes\\node_modules\\.bun\\typescript@5.8.3\\node_modules\\typescript\\lib\\typescript.js";
3177
+ var __dirname = "/Users/den/Desktop/Ingenuity/le-codes/node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib", __filename = "/Users/den/Desktop/Ingenuity/le-codes/node_modules/.bun/typescript@5.8.3/node_modules/typescript/lib/typescript.js";
3178
3178
  /*! *****************************************************************************
3179
3179
  Copyright (c) Microsoft Corporation. All rights reserved.
3180
3180
  Licensed under the Apache License, Version 2.0 (the "License"); you may not use
@@ -173350,7 +173350,7 @@ var init_fonts = __esm(() => {
173350
173350
 
173351
173351
  // src/compile/assetIcons.ts
173352
173352
  import { createHash as createHash5 } from "node:crypto";
173353
- import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "node:fs";
173353
+ import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "node:fs";
173354
173354
  import { homedir as homedir3 } from "node:os";
173355
173355
  import { dirname as dirname7, join as join14 } from "node:path";
173356
173356
  var DEFAULT_REGISTRY, isHttp2 = (s) => /^https?:/i.test(s), sha122 = (s) => createHash5("sha256").update(s).digest("hex").slice(0, 12), readText = async (registry, rel) => {
@@ -173363,13 +173363,13 @@ var DEFAULT_REGISTRY, isHttp2 = (s) => /^https?:/i.test(s), sha122 = (s) => crea
173363
173363
  }
173364
173364
  }
173365
173365
  try {
173366
- return readFileSync11(join14(registry, rel), "utf8");
173366
+ return readFileSync12(join14(registry, rel), "utf8");
173367
173367
  } catch {
173368
173368
  return null;
173369
173369
  }
173370
173370
  }, safeRead = (path) => {
173371
173371
  try {
173372
- return readFileSync11(path, "utf8");
173372
+ return readFileSync12(path, "utf8");
173373
173373
  } catch {
173374
173374
  return null;
173375
173375
  }
@@ -173391,7 +173391,7 @@ var DEFAULT_REGISTRY, isHttp2 = (s) => /^https?:/i.test(s), sha122 = (s) => crea
173391
173391
  return JSON.parse(raw);
173392
173392
  }, readLock2 = (path) => {
173393
173393
  try {
173394
- const lock = JSON.parse(readFileSync11(path, "utf8"));
173394
+ const lock = JSON.parse(readFileSync12(path, "utf8"));
173395
173395
  if (lock && typeof lock === "object" && lock.icons && lock.packs)
173396
173396
  return lock;
173397
173397
  } catch {}
@@ -214120,65 +214120,65 @@ var init_esm30 = __esm(() => {
214120
214120
  });
214121
214121
 
214122
214122
  // src/types.ts
214123
- import { dirname as dirname13, join as join21, relative as relative3, sep } from "node:path";
214124
- import { existsSync as existsSync20, mkdirSync as mkdirSync14, readdirSync as readdirSync9, readFileSync as readFileSync17, rmSync as rmSync7, statSync as statSync7, writeFileSync as writeFileSync16 } from "node:fs";
214123
+ import { dirname as dirname13, join as join22, relative as relative3, sep } from "node:path";
214124
+ 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";
214125
214125
  var TSCONFIG, listBundleFiles = (dir, base = dir) => {
214126
- if (!existsSync20(dir))
214126
+ if (!existsSync21(dir))
214127
214127
  return [];
214128
214128
  const out = [];
214129
- for (const name of readdirSync9(dir)) {
214130
- const path2 = join21(dir, name);
214131
- if (statSync7(path2).isDirectory())
214129
+ for (const name of readdirSync10(dir)) {
214130
+ const path2 = join22(dir, name);
214131
+ if (statSync8(path2).isDirectory())
214132
214132
  out.push(...listBundleFiles(path2, base));
214133
214133
  else
214134
214134
  out.push(relative3(base, path2).split(sep).join("/"));
214135
214135
  }
214136
214136
  return out;
214137
214137
  }, pruneEmptyDirs = (dir, root) => {
214138
- if (!existsSync20(dir) || !statSync7(dir).isDirectory())
214138
+ if (!existsSync21(dir) || !statSync8(dir).isDirectory())
214139
214139
  return;
214140
- for (const name of readdirSync9(dir))
214141
- pruneEmptyDirs(join21(dir, name), root);
214142
- if (dir !== root && readdirSync9(dir).length === 0)
214143
- rmSync7(dir, { recursive: true });
214140
+ for (const name of readdirSync10(dir))
214141
+ pruneEmptyDirs(join22(dir, name), root);
214142
+ if (dir !== root && readdirSync10(dir).length === 0)
214143
+ rmSync8(dir, { recursive: true });
214144
214144
  }, writeTypeBundle = (root, bundle) => {
214145
- const typesDir = join21(root, LECODES_DIR, "types");
214145
+ const typesDir = join22(root, LECODES_DIR, "types");
214146
214146
  const before = new Set(listBundleFiles(typesDir));
214147
214147
  const stats = { added: 0, updated: 0, removed: 0, total: Object.keys(bundle).length };
214148
214148
  for (const [path2, text] of Object.entries(bundle)) {
214149
- const dest = join21(typesDir, path2);
214149
+ const dest = join22(typesDir, path2);
214150
214150
  if (!before.has(path2))
214151
214151
  stats.added++;
214152
- else if (readFileSync17(dest, "utf8") !== text)
214152
+ else if (readFileSync18(dest, "utf8") !== text)
214153
214153
  stats.updated++;
214154
214154
  else
214155
214155
  continue;
214156
- mkdirSync14(dirname13(dest), { recursive: true });
214157
- writeFileSync16(dest, text);
214156
+ mkdirSync15(dirname13(dest), { recursive: true });
214157
+ writeFileSync17(dest, text);
214158
214158
  }
214159
214159
  for (const path2 of before) {
214160
214160
  if (path2 in bundle)
214161
214161
  continue;
214162
- rmSync7(join21(typesDir, path2));
214162
+ rmSync8(join22(typesDir, path2));
214163
214163
  stats.removed++;
214164
214164
  }
214165
214165
  pruneEmptyDirs(typesDir, typesDir);
214166
214166
  return stats;
214167
214167
  }, bundleHash = (bundle) => sha256(Object.keys(bundle).sort().map((k) => `${k}\x00${bundle[k]}`).join("\x00")), readProjectTypeBundle = (root) => {
214168
- const typesDir = join21(root, LECODES_DIR, "types");
214168
+ const typesDir = join22(root, LECODES_DIR, "types");
214169
214169
  const bundle = {};
214170
214170
  for (const path2 of listBundleFiles(typesDir))
214171
- bundle[path2] = readFileSync17(join21(typesDir, path2), "utf8");
214171
+ bundle[path2] = readFileSync18(join22(typesDir, path2), "utf8");
214172
214172
  return bundle;
214173
214173
  }, materializeTypes = async (root, apiUrl) => writeTypeBundle(root, await getTypes(apiUrl)), readLocalTypeBundle = () => {
214174
214174
  const candidates = [
214175
- join21(distRoot, "runtime", "sdk-types.json"),
214176
- join21(distRoot, "..", "sdk", "dist", "types.json")
214175
+ join22(distRoot, "runtime", "sdk-types.json"),
214176
+ join22(distRoot, "..", "sdk", "dist", "types.json")
214177
214177
  ];
214178
214178
  for (const path2 of candidates) {
214179
- if (existsSync20(path2)) {
214179
+ if (existsSync21(path2)) {
214180
214180
  try {
214181
- return JSON.parse(readFileSync17(path2, "utf8"));
214181
+ return JSON.parse(readFileSync18(path2, "utf8"));
214182
214182
  } catch {}
214183
214183
  }
214184
214184
  }
@@ -214190,7 +214190,7 @@ var TSCONFIG, listBundleFiles = (dir, base = dir) => {
214190
214190
  writeTypeBundle(root, bundle);
214191
214191
  return true;
214192
214192
  }, writeTsconfig = (root) => {
214193
- writeFileSync16(join21(root, "tsconfig.json"), JSON.stringify(TSCONFIG, null, 2));
214193
+ writeFileSync17(join22(root, "tsconfig.json"), JSON.stringify(TSCONFIG, null, 2));
214194
214194
  };
214195
214195
  var init_types4 = __esm(() => {
214196
214196
  init_api();
@@ -214216,14 +214216,14 @@ var init_types4 = __esm(() => {
214216
214216
  });
214217
214217
 
214218
214218
  // src/compile/designCompile.ts
214219
- import { readFileSync as readFileSync18 } from "node:fs";
214220
- import { basename as basename8 } from "node:path";
214219
+ import { readFileSync as readFileSync19 } from "node:fs";
214220
+ import { basename as basename9 } from "node:path";
214221
214221
  var LOCAL_ASSET_SCHEME = "lecodesfile://", compileDesignScreen = async (opts) => {
214222
214222
  const { root, manifest, designDir, designKey, screenId, state } = opts;
214223
214223
  const entries = [];
214224
214224
  const scan2 = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest?.files ?? {})) });
214225
214225
  for (const file of scan2.files) {
214226
- const type = manifest?.files[file.path]?.type ?? classifyKind(basename8(file.path));
214226
+ const type = manifest?.files[file.path]?.type ?? classifyKind(basename9(file.path));
214227
214227
  if (type === "resource") {
214228
214228
  entries.push({
214229
214229
  path: file.path,
@@ -214234,7 +214234,7 @@ var LOCAL_ASSET_SCHEME = "lecodesfile://", compileDesignScreen = async (opts) =>
214234
214234
  } else if (type === "shader") {
214235
214235
  entries.push({ path: file.path, type, shaders: [] });
214236
214236
  } else {
214237
- entries.push({ path: file.path, type, text: readFileSync18(file.absPath, "utf8") });
214237
+ entries.push({ path: file.path, type, text: readFileSync19(file.absPath, "utf8") });
214238
214238
  }
214239
214239
  }
214240
214240
  const invoke = state !== undefined ? `screen(${JSON.stringify(state)})` : "screen()";
@@ -214279,7 +214279,7 @@ var init_designCompile = __esm(() => {
214279
214279
 
214280
214280
  // src/browserAuth.ts
214281
214281
  import { createServer } from "node:http";
214282
- import { spawn as spawn4 } from "node:child_process";
214282
+ import { spawn as spawn5 } from "node:child_process";
214283
214283
  import { randomBytes as randomBytes2 } from "node:crypto";
214284
214284
  import { hostname } from "node:os";
214285
214285
  var SUCCESS_HTML = `<!doctype html><meta charset="utf-8"><title>lecodes</title>
@@ -214304,16 +214304,16 @@ var SUCCESS_HTML = `<!doctype html><meta charset="utf-8"><title>lecodes</title>
214304
214304
  args = [url];
214305
214305
  }
214306
214306
  try {
214307
- const child = spawn4(cmd, args, { stdio: "ignore", detached: true });
214307
+ const child = spawn5(cmd, args, { stdio: "ignore", detached: true });
214308
214308
  child.on("error", () => {});
214309
214309
  child.unref();
214310
214310
  } catch {}
214311
214311
  }, browserLogin = async (webBase, timeoutMs = 5 * 60 * 1000, open = openBrowser) => {
214312
214312
  const state = randomBytes2(16).toString("hex");
214313
- let resolve10;
214313
+ let resolve11;
214314
214314
  let reject;
214315
214315
  const tokenPromise = new Promise((res, rej) => {
214316
- resolve10 = res;
214316
+ resolve11 = res;
214317
214317
  reject = rej;
214318
214318
  });
214319
214319
  const server2 = createServer((req, res) => {
@@ -214331,7 +214331,7 @@ var SUCCESS_HTML = `<!doctype html><meta charset="utf-8"><title>lecodes</title>
214331
214331
  }
214332
214332
  res.writeHead(200, { "content-type": "text/html" });
214333
214333
  res.end(SUCCESS_HTML);
214334
- resolve10(token);
214334
+ resolve11(token);
214335
214335
  });
214336
214336
  await new Promise((res, rej) => {
214337
214337
  server2.once("error", rej);
@@ -214831,11 +214831,11 @@ var init_designTemplates = __esm(() => {
214831
214831
  });
214832
214832
 
214833
214833
  // src/designMeta.ts
214834
- import { readFileSync as readFileSync19 } from "node:fs";
214835
- import { join as join23 } from "node:path";
214834
+ import { readFileSync as readFileSync20 } from "node:fs";
214835
+ import { join as join24 } from "node:path";
214836
214836
  var PHONE_SIZE, readMetaFrames = (designDir) => {
214837
214837
  try {
214838
- const meta = JSON.parse(readFileSync19(join23(designDir, "meta.json"), "utf8"));
214838
+ const meta = JSON.parse(readFileSync20(join24(designDir, "meta.json"), "utf8"));
214839
214839
  const d = meta?.defaultSize;
214840
214840
  const valid = Array.isArray(d) && d.length === 2 && d.every((n2) => typeof n2 === "number" && Number.isFinite(n2) && n2 >= 64 && n2 <= 8192);
214841
214841
  return {
@@ -214859,8 +214859,8 @@ __export(exports_design, {
214859
214859
  flushDesignComments: () => flushDesignComments,
214860
214860
  design: () => design
214861
214861
  });
214862
- import { existsSync as existsSync23, mkdirSync as mkdirSync17, readFileSync as readFileSync20, readdirSync as readdirSync12, writeFileSync as writeFileSync18, appendFileSync } from "node:fs";
214863
- import { basename as basename9, join as join24 } from "node:path";
214862
+ import { existsSync as existsSync24, mkdirSync as mkdirSync18, readFileSync as readFileSync21, readdirSync as readdirSync13, writeFileSync as writeFileSync19, appendFileSync } from "node:fs";
214863
+ import { basename as basename10, join as join25 } from "node:path";
214864
214864
  var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214865
214865
  let root;
214866
214866
  let manifest;
@@ -214875,13 +214875,13 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214875
214875
  return {
214876
214876
  root,
214877
214877
  manifest,
214878
- name: manifest?.name ?? basename9(root),
214878
+ name: manifest?.name ?? basename10(root),
214879
214879
  dirName,
214880
- designDir: join24(root, dirName),
214880
+ designDir: join25(root, dirName),
214881
214881
  designKey: "/" + dirName.replace(/\\/g, "/")
214882
214882
  };
214883
214883
  }, loadDesignServer = async (ctx, needs = []) => {
214884
- if (!existsSync23(ctx.designDir)) {
214884
+ if (!existsSync24(ctx.designDir)) {
214885
214885
  throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
214886
214886
  }
214887
214887
  let mod;
@@ -214913,17 +214913,17 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214913
214913
  `).find((l2) => l2.trim())?.trim() ?? "";
214914
214914
  return line.length > max ? line.slice(0, max - 1) + "…" : line;
214915
214915
  }, listScreens = (designDir) => {
214916
- const dir = join24(designDir, "screens");
214917
- if (!existsSync23(dir))
214916
+ const dir = join25(designDir, "screens");
214917
+ if (!existsSync24(dir))
214918
214918
  return [];
214919
- return readdirSync12(dir).filter((f3) => f3.endsWith(".ts") && !f3.startsWith(".")).map((f3) => f3.slice(0, -3)).sort();
214919
+ return readdirSync13(dir).filter((f3) => f3.endsWith(".ts") && !f3.startsWith(".")).map((f3) => f3.slice(0, -3)).sort();
214920
214920
  }, writeMcpConfig = (ctx, port) => {
214921
- const path2 = join24(ctx.root, ".mcp.json");
214921
+ const path2 = join25(ctx.root, ".mcp.json");
214922
214922
  const entry = { type: "http", url: `http://localhost:${port}/mcp` };
214923
214923
  let config = {};
214924
- if (existsSync23(path2)) {
214924
+ if (existsSync24(path2)) {
214925
214925
  try {
214926
- config = JSON.parse(readFileSync20(path2, "utf8"));
214926
+ config = JSON.parse(readFileSync21(path2, "utf8"));
214927
214927
  } catch {
214928
214928
  warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-design server by hand.");
214929
214929
  return;
@@ -214936,7 +214936,7 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214936
214936
  return;
214937
214937
  }
214938
214938
  config.mcpServers["lecodes-design"] = entry;
214939
- writeFileSync18(path2, JSON.stringify(config, null, 2) + `
214939
+ writeFileSync19(path2, JSON.stringify(config, null, 2) + `
214940
214940
  `);
214941
214941
  log(`${c.green(existing ? "updated" : "created")} .mcp.json (lecodes-design MCP → ${entry.url})`);
214942
214942
  }, init = async (ctx, args) => {
@@ -214945,26 +214945,26 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214945
214945
  ["spec.md", SPEC_MD],
214946
214946
  ["README.md", README_HINT],
214947
214947
  ["CLAUDE.md", CLAUDE_MD],
214948
- [join24("shared", "tokens.ts"), TOKENS_TS],
214949
- [join24("shared", "tabs.ts"), TABS_TS],
214950
- [join24("screens", "home.ts"), HOME_SCREEN_TS]
214948
+ [join25("shared", "tokens.ts"), TOKENS_TS],
214949
+ [join25("shared", "tabs.ts"), TABS_TS],
214950
+ [join25("screens", "home.ts"), HOME_SCREEN_TS]
214951
214951
  ];
214952
- mkdirSync17(join24(ctx.designDir, "screens"), { recursive: true });
214953
- mkdirSync17(join24(ctx.designDir, "shared"), { recursive: true });
214952
+ mkdirSync18(join25(ctx.designDir, "screens"), { recursive: true });
214953
+ mkdirSync18(join25(ctx.designDir, "shared"), { recursive: true });
214954
214954
  let created = 0;
214955
214955
  for (const [rel, content] of files) {
214956
- const abs = join24(ctx.designDir, rel);
214957
- if (existsSync23(abs)) {
214956
+ const abs = join25(ctx.designDir, rel);
214957
+ if (existsSync24(abs)) {
214958
214958
  note(`kept ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
214959
214959
  continue;
214960
214960
  }
214961
- writeFileSync18(abs, content);
214961
+ writeFileSync19(abs, content);
214962
214962
  log(`${c.green("created")} ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
214963
214963
  created++;
214964
214964
  }
214965
- const ignorePath = join24(ctx.root, ".lecodesignore");
214965
+ const ignorePath = join25(ctx.root, ".lecodesignore");
214966
214966
  const ignoreLines = [`${ctx.dirName}/.snapshots/`, `${ctx.dirName}/comments/`];
214967
- const current = existsSync23(ignorePath) ? readFileSync20(ignorePath, "utf8") : "";
214967
+ const current = existsSync24(ignorePath) ? readFileSync21(ignorePath, "utf8") : "";
214968
214968
  const present = new Set(current.split(/\r?\n/));
214969
214969
  const missing = ignoreLines.filter((line) => !present.has(line));
214970
214970
  if (missing.length) {
@@ -214979,8 +214979,8 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214979
214979
  writeMcpConfig(ctx, Number(flagStr(args, "port")) || DEFAULT_PORT);
214980
214980
  const wroteTypes = materializeTypesLocal(ctx.designDir);
214981
214981
  writeTsconfig(ctx.designDir);
214982
- mkdirSync17(join24(ctx.designDir, ".lecodes", "types"), { recursive: true });
214983
- writeFileSync18(join24(ctx.designDir, ".lecodes", "types", "design-macros.d.ts"), DESIGN_MACROS_DTS);
214982
+ mkdirSync18(join25(ctx.designDir, ".lecodes", "types"), { recursive: true });
214983
+ writeFileSync19(join25(ctx.designDir, ".lecodes", "types", "design-macros.d.ts"), DESIGN_MACROS_DTS);
214984
214984
  log(`${c.green("created")} ${ctx.dirName}/tsconfig.json${wroteTypes ? ` + ${ctx.dirName}/.lecodes/types/` : ""} (IDE types)`);
214985
214985
  if (!wroteTypes)
214986
214986
  warnErr("Bundled SDK types not found — tsconfig written, but globals may show unresolved until the CLI is rebuilt with vendored types.");
@@ -215028,16 +215028,16 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
215028
215028
  killer.unref?.();
215029
215029
  const png2 = flagBool(args, "png") || flagStr(args, "png") !== undefined;
215030
215030
  const frames = readMetaFrames(ctx.designDir);
215031
- const outDir = flagStr(args, "out-dir") ?? join24(ctx.designDir, ".snapshots");
215031
+ const outDir = flagStr(args, "out-dir") ?? join25(ctx.designDir, ".snapshots");
215032
215032
  const single = targets.length === 1 && requested.length === 1 && !png2 && !allStates && !flagStr(args, "out-dir");
215033
215033
  const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
215034
215034
  if (!single)
215035
- mkdirSync17(outDir, { recursive: true });
215035
+ mkdirSync18(outDir, { recursive: true });
215036
215036
  for (const id of targets) {
215037
215037
  const [width, height] = frames.screens[id]?.size ?? frames.defaultSize;
215038
215038
  let plan = [{ state: undefined, bases: [id] }];
215039
215039
  if (discover) {
215040
- const { states, canonical } = discover(readFileSync20(join24(ctx.designDir, "screens", `${id}.ts`), "utf8"));
215040
+ const { states, canonical } = discover(readFileSync21(join25(ctx.designDir, "screens", `${id}.ts`), "utf8"));
215041
215041
  if (stateFlag !== undefined) {
215042
215042
  if (!states.includes(stateFlag)) {
215043
215043
  if (requested.length === 1)
@@ -215078,11 +215078,11 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
215078
215078
  for (const w of pngResult.warnings)
215079
215079
  warnErr(`${tag}: ${w}`);
215080
215080
  for (const base of bases) {
215081
- writeFileSync18(join24(outDir, `${base}.json`), result.text);
215082
- log(`${c.green("✓")} ${tag} → ${join24(outDir, `${base}.json`)}`);
215081
+ writeFileSync19(join25(outDir, `${base}.json`), result.text);
215082
+ log(`${c.green("✓")} ${tag} → ${join25(outDir, `${base}.json`)}`);
215083
215083
  if (pngResult) {
215084
- writeFileSync18(join24(outDir, `${base}.png`), pngResult.png);
215085
- log(`${c.green("✓")} ${tag} → ${join24(outDir, `${base}.png`)}`);
215084
+ writeFileSync19(join25(outDir, `${base}.png`), pngResult.png);
215085
+ log(`${c.green("✓")} ${tag} → ${join25(outDir, `${base}.png`)}`);
215086
215086
  }
215087
215087
  }
215088
215088
  }
@@ -215384,7 +215384,7 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
215384
215384
  note("Read them with: lecodes design comments");
215385
215385
  }, flushDesignComments = async (args) => {
215386
215386
  const ctx = getContext(args);
215387
- if (!existsSync23(ctx.designDir))
215387
+ if (!existsSync24(ctx.designDir))
215388
215388
  return null;
215389
215389
  const platform = optionalPlatform(ctx);
215390
215390
  if (!platform)
@@ -216526,7 +216526,7 @@ var require_main = __commonJS((exports, module) => {
216526
216526
  // package.json
216527
216527
  var package_default = {
216528
216528
  name: "lecodes-cli",
216529
- version: "0.18.1",
216529
+ version: "0.18.2",
216530
216530
  dependencies: {
216531
216531
  "@letary/chisel": "^0.10.0",
216532
216532
  jimp: "^1.6.1"
@@ -216593,10 +216593,10 @@ var package_default = {
216593
216593
  // src/commands/app.ts
216594
216594
  init_manifest();
216595
216595
  init_config();
216596
- import { execFileSync as execFileSync4 } from "node:child_process";
216597
- import { cpSync as cpSync3, existsSync as existsSync19, mkdirSync as mkdirSync13, readFileSync as readFileSync16, readdirSync as readdirSync8, rmSync as rmSync6, statSync as statSync6, writeFileSync as writeFileSync15 } from "node:fs";
216596
+ import { execFileSync as execFileSync6 } from "node:child_process";
216597
+ 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";
216598
216598
  import { homedir as homedir7 } from "node:os";
216599
- import { basename as basename7, dirname as dirname12, join as join20, resolve as resolve7 } from "node:path";
216599
+ import { basename as basename8, dirname as dirname12, join as join21, resolve as resolve8 } from "node:path";
216600
216600
 
216601
216601
  // src/commands/compile.ts
216602
216602
  init_api();
@@ -216721,7 +216721,7 @@ init_fonts();
216721
216721
  init_compile();
216722
216722
  import { execFile } from "node:child_process";
216723
216723
  import { createHash as createHash4, randomBytes } from "node:crypto";
216724
- import { copyFileSync, existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync10, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync7 } from "node:fs";
216724
+ import { copyFileSync, existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync11, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync7 } from "node:fs";
216725
216725
  import { tmpdir } from "node:os";
216726
216726
  import { basename as basename3, join as join13 } from "node:path";
216727
216727
 
@@ -216733,10 +216733,18 @@ import { join as join12, resolve } from "node:path";
216733
216733
  // src/releases.ts
216734
216734
  init_util();
216735
216735
  import { createHash as createHash3 } from "node:crypto";
216736
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, mkdtempSync, renameSync, rmSync, writeFileSync as writeFileSync6 } from "node:fs";
216736
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, mkdtempSync, readFileSync as readFileSync10, renameSync, rmSync, writeFileSync as writeFileSync6 } from "node:fs";
216737
216737
  import { join as join11 } from "node:path";
216738
216738
  import { spawnSync as spawnSync2 } from "node:child_process";
216739
216739
  var MANIFEST_URL = "https://storage.yandexcloud.net/le-codes/releases/versions.json";
216740
+ var ARTIFACT_STAMP = ".lecodes-artifact.json";
216741
+ var cachedArtifactSha = (dir) => {
216742
+ try {
216743
+ return JSON.parse(readFileSync10(join11(dir, ARTIFACT_STAMP), "utf8")).sha256 ?? null;
216744
+ } catch {
216745
+ return null;
216746
+ }
216747
+ };
216740
216748
  var semverNewer = (a, b) => {
216741
216749
  const pa = a.split(/[.-]/).map(Number), pb = b.split(/[.-]/).map(Number);
216742
216750
  for (let i = 0;i < 3; i++)
@@ -216778,15 +216786,20 @@ var downloadReleaseArchive = async (file, opts) => {
216778
216786
  const archiveName = file.url.split("/").pop() ?? "archive";
216779
216787
  const archive = join11(stage, archiveName);
216780
216788
  writeFileSync6(archive, bytes);
216781
- const winTar = join11(process.env.SystemRoot ?? "C:\\Windows", "System32", "tar.exe");
216782
- const tarExe = process.platform === "win32" && existsSync9(winTar) ? winTar : "tar";
216783
- const tar = spawnSync2(tarExe, ["-xf", archiveName], { cwd: stage, stdio: ["ignore", "ignore", "pipe"] });
216784
- if (tar.status !== 0)
216785
- throw new CliError(`Couldn't extract the archive: ${tar.stderr?.toString() ?? "tar failed"}`);
216789
+ if (opts.extract) {
216790
+ opts.extract(archive, stage);
216791
+ } else {
216792
+ const winTar = join11(process.env.SystemRoot ?? "C:\\Windows", "System32", "tar.exe");
216793
+ const tarExe = process.platform === "win32" && existsSync9(winTar) ? winTar : "tar";
216794
+ const tar = spawnSync2(tarExe, ["-xf", archiveName], { cwd: stage, stdio: ["ignore", "ignore", "pipe"] });
216795
+ if (tar.status !== 0)
216796
+ throw new CliError(`Couldn't extract the archive: ${tar.stderr?.toString() ?? "tar failed"}`);
216797
+ }
216786
216798
  rmSync(archive);
216787
216799
  const bad = opts.validate(stage);
216788
216800
  if (bad)
216789
216801
  throw new CliError(bad);
216802
+ writeFileSync6(join11(stage, ARTIFACT_STAMP), JSON.stringify({ sha256: file.sha256, url: file.url }, null, 2));
216790
216803
  try {
216791
216804
  renameSync(stage, opts.dest);
216792
216805
  } catch {
@@ -216812,7 +216825,13 @@ var platformKey = () => {
216812
216825
  return "windows-x64";
216813
216826
  if (process.platform === "linux")
216814
216827
  return "linux-x64";
216815
- throw new CliError(process.platform === "darwin" ? "matc isn't published for macOS yet — push the project once so shaders compile on the server." : `matc isn't published for ${process.platform}.`);
216828
+ if (process.platform === "darwin") {
216829
+ if (process.arch !== "arm64") {
216830
+ throw new CliError("matc isn't published for Intel macOS — set LECODES_MATC to a locally built matc " + "(creator-gl/out/cmake-release/filament/tools/matc), or push the project once so shaders " + "compile on the server.");
216831
+ }
216832
+ return "macos-arm64";
216833
+ }
216834
+ throw new CliError(`matc isn't published for ${process.platform}.`);
216816
216835
  };
216817
216836
  var cacheRoot = () => join12(homedir2(), ".lecodes", "matc");
216818
216837
  var findMatc = (dir) => {
@@ -216930,7 +216949,7 @@ var cleanMatcOutput = (err, tempName, realName) => {
216930
216949
  };
216931
216950
  var NATIVE_PLATFORMS = ["opengl", "metal", "desktop"];
216932
216951
  var ALL_NATIVE_PLATFORMS = ["opengl", "metal", "desktop", "desktop-vulkan"];
216933
- var desktopShaderPlatform = (platform, renderer) => platform === "win32" ? renderer === "vulkan" ? "desktop-vulkan" : "desktop" : "opengl";
216952
+ var desktopShaderPlatform = (platform, renderer) => platform === "win32" ? renderer === "vulkan" ? "desktop-vulkan" : "desktop" : platform === "darwin" ? "metal" : "opengl";
216934
216953
  var applyLocalShaders = async (entries, opts) => {
216935
216954
  const shaderEntries = entries.filter((e) => e.type === "shader" && e.absPath && (opts.local || !e.shaders?.length));
216936
216955
  if (shaderEntries.length === 0)
@@ -216948,7 +216967,7 @@ var applyLocalShaders = async (entries, opts) => {
216948
216967
  mkdirSync6(cacheDir, { recursive: true });
216949
216968
  const artifacts = [];
216950
216969
  for (const entry of shaderEntries) {
216951
- const text = readFileSync10(entry.absPath, "utf8");
216970
+ const text = readFileSync11(entry.absPath, "utf8");
216952
216971
  const hash = shaderHash(text, matcId);
216953
216972
  const family = artifactFamily(text);
216954
216973
  const artifact = (platform) => `${hash}_${platform}.filamat`;
@@ -217011,7 +217030,7 @@ init_manifest();
217011
217030
  init_ignore();
217012
217031
  init_util();
217013
217032
  import { execFileSync } from "node:child_process";
217014
- import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "node:fs";
217033
+ import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "node:fs";
217015
217034
  import { homedir as homedir4 } from "node:os";
217016
217035
  import { dirname as dirname8, join as join15, resolve as resolve2 } from "node:path";
217017
217036
  var FIRST_PARTY_PLUGINS = {
@@ -217029,8 +217048,13 @@ var resolveDesktopRenderer = (root, args) => {
217029
217048
  const value = String(raw).toLowerCase();
217030
217049
  if (value === "gl" || value === "opengl")
217031
217050
  return "gl";
217032
- if (value === "vulkan" || value === "vk")
217051
+ if (value === "vulkan" || value === "vk") {
217052
+ if (process.platform === "darwin") {
217053
+ note('desktop.renderer "vulkan" is Windows-only — the macOS host renders with Metal.');
217054
+ return "gl";
217055
+ }
217033
217056
  return "vulkan";
217057
+ }
217034
217058
  throw new CliError(`Unknown desktop renderer "${raw}". Use "gl" or "vulkan".`);
217035
217059
  };
217036
217060
  var desktopDefaultViewport = (root) => {
@@ -217046,7 +217070,7 @@ var readJson = (path) => {
217046
217070
  if (!existsSync13(path))
217047
217071
  return null;
217048
217072
  try {
217049
- return JSON.parse(readFileSync12(path, "utf8").replace(/^\uFEFF/, ""));
217073
+ return JSON.parse(readFileSync13(path, "utf8").replace(/^\uFEFF/, ""));
217050
217074
  } catch {
217051
217075
  throw new CliError(`Invalid JSON in ${path}`);
217052
217076
  }
@@ -217077,7 +217101,7 @@ ${tail}`);
217077
217101
  };
217078
217102
  var ensureShellIgnored = (root, dir) => {
217079
217103
  const ignorePath = join15(root, ".lecodesignore");
217080
- const ignore = existsSync13(ignorePath) ? readFileSync12(ignorePath, "utf8") : DEFAULT_IGNORE_FILE;
217104
+ const ignore = existsSync13(ignorePath) ? readFileSync13(ignorePath, "utf8") : DEFAULT_IGNORE_FILE;
217081
217105
  if (new RegExp(`^${dir}\\/$`, "m").test(ignore))
217082
217106
  return;
217083
217107
  const comment = ignore.includes("# Native app shells") ? "" : "# Native app shells — generated by `lecodes app init`, never pushed to LeCodes.\n";
@@ -218217,14 +218241,14 @@ buildNumber) — edit it there, then re-run \`lecodes app sync\`. Plugins are ma
218217
218241
  init_manifest();
218218
218242
  import { execFileSync as execFileSync2, spawn, spawnSync as spawnSync3 } from "node:child_process";
218219
218243
  import { createHash as createHash6 } from "node:crypto";
218220
- import { chmodSync, cpSync, existsSync as existsSync16, mkdirSync as mkdirSync10, readFileSync as readFileSync14, readdirSync as readdirSync5, renameSync as renameSync2, rmSync as rmSync3, statSync as statSync3, writeFileSync as writeFileSync12 } from "node:fs";
218244
+ 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";
218221
218245
  import { homedir as homedir5 } from "node:os";
218222
218246
  import { basename as basename5, dirname as dirname10, join as join17 } from "node:path";
218223
218247
  init_util();
218224
218248
 
218225
218249
  // src/commands/appIcon.ts
218226
218250
  init_util();
218227
- import { existsSync as existsSync15, mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync11 } from "node:fs";
218251
+ import { existsSync as existsSync15, mkdirSync as mkdirSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync11 } from "node:fs";
218228
218252
  import { dirname as dirname9, join as join16, resolve as resolve4 } from "node:path";
218229
218253
  var MASTER_PX = 1024;
218230
218254
  var DENSITIES = [
@@ -218354,7 +218378,7 @@ var restampManifestIcon = (androidDir) => {
218354
218378
  const path2 = join16(androidDir, "app", "src", "main", "AndroidManifest.xml");
218355
218379
  if (!existsSync15(path2))
218356
218380
  return;
218357
- const content = readFileSync13(path2, "utf8");
218381
+ const content = readFileSync14(path2, "utf8");
218358
218382
  if (content.includes(`android:icon="@mipmap/ic_launcher"`))
218359
218383
  return;
218360
218384
  if (!/android:icon="[^"]*"/.test(content)) {
@@ -219274,7 +219298,7 @@ var shellSdkVersionAndroid = (androidDir) => {
219274
219298
  const path2 = join17(androidDir, "lecodes-runtime", "build.gradle.kts");
219275
219299
  if (!existsSync16(path2))
219276
219300
  return null;
219277
- const m = /io\.letary:lecodes-sdk(?:-(?:core|2d|3d))?:(\d+\.\d+\.\d+)@aar/.exec(readFileSync14(path2, "utf8"));
219301
+ const m = /io\.letary:lecodes-sdk(?:-(?:core|2d|3d))?:(\d+\.\d+\.\d+)@aar/.exec(readFileSync15(path2, "utf8"));
219278
219302
  return m ? m[1] : null;
219279
219303
  };
219280
219304
  var GRADLE_WRAPPER_JAR_URL = "https://raw.githubusercontent.com/gradle/gradle/v8.13.0/gradle/wrapper/gradle-wrapper.jar";
@@ -219282,7 +219306,7 @@ var GRADLE_WRAPPER_JAR_SHA256 = "81a82aaea5abcc8ff68b3dfcb58b3c3c429378efd98e743
219282
219306
  var ensureGradleWrapperJar = async () => {
219283
219307
  const cached = join17(homedir5(), ".lecodes", "gradle-wrapper", GRADLE_VERSION, "gradle-wrapper.jar");
219284
219308
  if (existsSync16(cached))
219285
- return new Uint8Array(readFileSync14(cached));
219309
+ return new Uint8Array(readFileSync15(cached));
219286
219310
  try {
219287
219311
  const res = await fetch(GRADLE_WRAPPER_JAR_URL);
219288
219312
  if (!res.ok)
@@ -219409,7 +219433,7 @@ var syncAndroid = async (root, config, prior) => {
219409
219433
  resources = prior.resources;
219410
219434
  } else {
219411
219435
  resources = await compileShell({ _: [], flags: { o: join17(assetsDir, "app.js") } }, { remotePatterns });
219412
- bundleJs = readFileSync14(join17(assetsDir, "app.js"), "utf8");
219436
+ bundleJs = readFileSync15(join17(assetsDir, "app.js"), "utf8");
219413
219437
  }
219414
219438
  const requestedVariant = config.android?.variant ?? "auto";
219415
219439
  if (requestedVariant !== "auto" && !SDK_VARIANTS.includes(requestedVariant)) {
@@ -219546,7 +219570,7 @@ var restampAppBuildGradle = (androidDir, config, gradlePlugins) => {
219546
219570
  warn("android/app/build.gradle.kts is missing — run `lecodes app init android` on a clean tree to regenerate the shell.");
219547
219571
  return;
219548
219572
  }
219549
- let content = readFileSync14(path2, "utf8");
219573
+ let content = readFileSync15(path2, "utf8");
219550
219574
  const before = content;
219551
219575
  const versionName = config.version ?? "1.0.0";
219552
219576
  const versionCode = config.android?.versionCode ?? 1;
@@ -219719,13 +219743,13 @@ var doctorAndroidChecks = async (root, config, check) => {
219719
219743
  };
219720
219744
 
219721
219745
  // src/commands/appDesktop.ts
219722
- import { execFileSync as execFileSync3, spawn as spawn3 } from "node:child_process";
219723
- import { cpSync as cpSync2, existsSync as existsSync18, mkdirSync as mkdirSync12, readdirSync as readdirSync7, rmSync as rmSync5, statSync as statSync5, writeFileSync as writeFileSync14 } from "node:fs";
219724
- import { basename as basename6, dirname as dirname11, join as join19, resolve as resolve6 } from "node:path";
219746
+ import { execFileSync as execFileSync5, spawn as spawn4 } from "node:child_process";
219747
+ 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";
219748
+ import { basename as basename7, dirname as dirname11, join as join20, resolve as resolve7 } from "node:path";
219725
219749
 
219726
219750
  // src/desktopRenderer.ts
219727
- import { spawn as spawn2 } from "node:child_process";
219728
- import { copyFileSync as copyFileSync2, existsSync as existsSync17, mkdirSync as mkdirSync11, mkdtempSync as mkdtempSync2, readdirSync as readdirSync6, readFileSync as readFileSync15, rmSync as rmSync4, statSync as statSync4, writeFileSync as writeFileSync13 } from "node:fs";
219751
+ import { execFileSync as execFileSync3, spawn as spawn2, spawnSync as spawnSync4 } from "node:child_process";
219752
+ import { copyFileSync as copyFileSync2, 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";
219729
219753
  import { homedir as homedir6, tmpdir as tmpdir2 } from "node:os";
219730
219754
  import { join as join18, resolve as resolve5 } from "node:path";
219731
219755
  init_util();
@@ -219839,9 +219863,12 @@ var platformKey2 = (renderer = "gl") => {
219839
219863
  }
219840
219864
  if (process.platform === "linux")
219841
219865
  return "ubuntu-linux-x64";
219842
- throw new CliError(process.platform === "darwin" ? "`lecodes render --desktop` isn't available on macOS yet (the macOS host ships separately)." : `\`lecodes render --desktop\` isn't available on ${process.platform}.`);
219866
+ if (process.platform === "darwin")
219867
+ return process.arch === "arm64" ? "macos-arm64" : "macos-x64";
219868
+ throw new CliError(`The LeCodes Desktop host isn't available on ${process.platform}.`);
219843
219869
  };
219844
- var exeName = () => process.platform === "win32" ? "lecodes-desktop.exe" : "lecodes-desktop";
219870
+ var isMac = () => process.platform === "darwin";
219871
+ var exeName = () => process.platform === "win32" ? "lecodes-desktop.exe" : isMac() ? "lecodes-app" : "lecodes-desktop";
219845
219872
  var findExe = (dir) => {
219846
219873
  const name = exeName();
219847
219874
  for (const entry of readdirSync6(dir, { withFileTypes: true })) {
@@ -219867,13 +219894,33 @@ var installedDesktopVersions = () => {
219867
219894
  };
219868
219895
  var selectedDesktopVersion = () => {
219869
219896
  try {
219870
- const saved = JSON.parse(readFileSync15(currentFile(), "utf8")).version;
219897
+ const saved = JSON.parse(readFileSync16(currentFile(), "utf8")).version;
219871
219898
  if (saved && /^\d+\.\d+\.\d+/.test(saved) && semverNewer(DESKTOP_RENDERER_VERSION, saved) > 0)
219872
219899
  return saved;
219873
219900
  } catch {}
219874
219901
  return DESKTOP_RENDERER_VERSION;
219875
219902
  };
219876
219903
  var fetchDesktopManifest = () => fetchReleaseProduct("desktop", "desktop");
219904
+ var pickMacVersion = (versions, wanted, key = "macos-arm64") => {
219905
+ if (versions.find((v) => v.version === wanted)?.files[key])
219906
+ return wanted;
219907
+ const eligible = versions.filter((v) => v.files[key] && semverNewer(DESKTOP_RENDERER_VERSION, v.version) >= 0).map((v) => v.version).sort(semverNewer);
219908
+ return eligible[0] ?? null;
219909
+ };
219910
+ var extractDmg = (archive, stage) => {
219911
+ const mount = join18(stage, "mnt");
219912
+ mkdirSync11(mount, { recursive: true });
219913
+ execFileSync3("hdiutil", ["attach", "-nobrowse", "-readonly", "-quiet", "-mountpoint", mount, archive]);
219914
+ try {
219915
+ const app = readdirSync6(mount).find((n2) => n2.endsWith(".app"));
219916
+ if (!app)
219917
+ throw new CliError("The downloaded DMG holds no .app bundle.");
219918
+ execFileSync3("cp", ["-R", join18(mount, app), join18(stage, app)]);
219919
+ } finally {
219920
+ spawnSync4("hdiutil", ["detach", "-quiet", "-force", mount]);
219921
+ rmSync4(mount, { recursive: true, force: true });
219922
+ }
219923
+ };
219877
219924
  var downloadDesktop = async (version, dest, renderer = "gl") => {
219878
219925
  const key = platformKey2(renderer);
219879
219926
  const entry = (await fetchDesktopManifest()).versions.find((v) => v.version === version);
@@ -219886,13 +219933,55 @@ var downloadDesktop = async (version, dest, renderer = "gl") => {
219886
219933
  throw new CliError(`LeCodes Desktop ${version} [${key}] isn't published yet — \`--desktop\` needs it for the ` + `headless render contract.
219887
219934
  Set LECODES_DESKTOP_EXE to a locally built host, or update lecodes-cli.`);
219888
219935
  }
219889
- note(`Downloading the native renderer (LeCodes Desktop ${version}, ${(file.size / 1024 / 1024).toFixed(1)} MB)…`);
219936
+ note(`Downloading LeCodes Desktop ${version} [${key}] (${(file.size / 1024 / 1024).toFixed(1)} MB)…`);
219890
219937
  await downloadReleaseArchive(file, {
219891
219938
  cacheRoot: cacheRoot2(),
219892
219939
  dest,
219893
- validate: (dir) => findExe(dir) ? null : "The downloaded archive doesn't contain the desktop host binary."
219940
+ validate: (dir) => findExe(dir) ? null : "The downloaded archive doesn't contain the desktop host binary.",
219941
+ extract: file.url.toLowerCase().endsWith(".dmg") ? extractDmg : undefined
219894
219942
  });
219895
219943
  };
219944
+ var findMacHostExe = () => {
219945
+ const candidates = [
219946
+ ...["/Applications", join18(homedir6(), "Applications")].flatMap((root) => ["LeCodes Desktop.app", "LeCodes.app"].map((app) => join18(root, app)))
219947
+ ].map((app) => join18(app, "Contents", "MacOS", "lecodes-app"));
219948
+ return candidates.find((exe) => existsSync17(exe)) ?? null;
219949
+ };
219950
+ var resolveMacExe = async (selected) => {
219951
+ const cached = findExe(join18(cacheRoot2(), `desktop-${selected}`));
219952
+ if (cached)
219953
+ return cached;
219954
+ try {
219955
+ const { versions } = await fetchDesktopManifest();
219956
+ const version = pickMacVersion(versions, selected, platformKey2());
219957
+ if (!version) {
219958
+ throw new CliError(`No LeCodes Desktop release for ${platformKey2()} is published at or above ${DESKTOP_RENDERER_VERSION}.`);
219959
+ }
219960
+ const dir = join18(cacheRoot2(), `desktop-${version}`);
219961
+ let exe = findExe(dir);
219962
+ if (!exe) {
219963
+ await downloadDesktop(version, dir);
219964
+ exe = findExe(dir);
219965
+ if (!exe)
219966
+ throw new CliError(`Renderer cache at ${dir} is broken — delete the folder and retry.`);
219967
+ }
219968
+ if (version !== selected) {
219969
+ note(`LeCodes Desktop ${selected} has no macOS build — using ${version}.`);
219970
+ mkdirSync11(cacheRoot2(), { recursive: true });
219971
+ writeFileSync13(currentFile(), JSON.stringify({ version }, null, 2));
219972
+ }
219973
+ return exe;
219974
+ } catch (e2) {
219975
+ const installed = findMacHostExe();
219976
+ if (installed) {
219977
+ warnErr(`${e2 instanceof Error ? e2.message : String(e2)}
219978
+ Falling back to the installed host: ${installed}`);
219979
+ return installed;
219980
+ }
219981
+ throw new CliError(`${e2 instanceof Error ? e2.message : String(e2)}
219982
+ ` + " No LeCodes Desktop.app is installed either — retry online (`lecodes desktop update`), install the DMG into\n" + " /Applications, or point LECODES_DESKTOP_EXE at a build (…/LeCodes Desktop.app/Contents/MacOS/lecodes-app).");
219983
+ }
219984
+ };
219896
219985
  var resolveDesktopExe = async (renderer = "gl") => {
219897
219986
  const override = process.env.LECODES_DESKTOP_EXE;
219898
219987
  if (override) {
@@ -219903,6 +219992,8 @@ var resolveDesktopExe = async (renderer = "gl") => {
219903
219992
  if (renderer === "vulkan")
219904
219993
  platformKey2(renderer);
219905
219994
  const version = selectedDesktopVersion();
219995
+ if (isMac())
219996
+ return resolveMacExe(version);
219906
219997
  const dir = join18(cacheRoot2(), `desktop-${version}${renderer === "vulkan" ? "-vulkan" : ""}`);
219907
219998
  let exe = existsSync17(dir) ? findExe(dir) : null;
219908
219999
  if (!exe) {
@@ -219914,13 +220005,23 @@ var resolveDesktopExe = async (renderer = "gl") => {
219914
220005
  return exe;
219915
220006
  };
219916
220007
  var updateDesktop = async () => {
219917
- const { latest } = await fetchDesktopManifest();
220008
+ const manifest = await fetchDesktopManifest();
220009
+ const latest = isMac() ? pickMacVersion(manifest.versions, manifest.latest, platformKey2()) : manifest.latest;
220010
+ if (!latest)
220011
+ throw new CliError(`No LeCodes Desktop release for ${platformKey2()} is published at or above ${DESKTOP_RENDERER_VERSION}.`);
219918
220012
  if (semverNewer(latest, DESKTOP_RENDERER_VERSION) > 0) {
219919
220013
  throw new CliError(`The newest published LeCodes Desktop is ${latest}, but this CLI needs ≥ ${DESKTOP_RENDERER_VERSION} ` + `for the render contract. Publish the desktop release first.`);
219920
220014
  }
219921
220015
  const before = selectedDesktopVersion();
219922
220016
  const dir = join18(cacheRoot2(), `desktop-${latest}`);
219923
- const hadIt = existsSync17(dir) && findExe(dir) !== null;
220017
+ const published = (await fetchDesktopManifest()).versions.find((v) => v.version === latest)?.files[platformKey2()];
220018
+ const cached = existsSync17(dir) && findExe(dir) !== null;
220019
+ const republished = cached && published !== undefined && cachedArtifactSha(dir) !== published.sha256;
220020
+ if (republished) {
220021
+ note(`LeCodes Desktop ${latest} was republished since this machine cached it — downloading the current build.`);
220022
+ rmSync4(dir, { recursive: true, force: true });
220023
+ }
220024
+ const hadIt = cached && !republished;
219924
220025
  if (!hadIt)
219925
220026
  await downloadDesktop(latest, dir);
219926
220027
  mkdirSync11(cacheRoot2(), { recursive: true });
@@ -220019,6 +220120,9 @@ var runDesktopDev = async (url, opts = {}) => {
220019
220120
  });
220020
220121
  };
220021
220122
  var runDesktopRender = async (js, resources, opts) => {
220123
+ if (isMac()) {
220124
+ throw new CliError("`lecodes render --desktop` isn't available on macOS yet — the macOS host has no headless mode " + "(CREATOR_HIDDEN / CREATOR_DUMP_PNG). `lecodes desktop run` and `lecodes desktop build` do work there.");
220125
+ }
220022
220126
  const exe = await resolveDesktopExe(opts.renderer);
220023
220127
  const runDir = stageBundle(js, resources);
220024
220128
  try {
@@ -220087,15 +220191,333 @@ ${tail}` : "";
220087
220191
 
220088
220192
  // src/commands/appDesktop.ts
220089
220193
  init_util();
220194
+
220195
+ // src/commands/appDesktopMac.ts
220196
+ init_util();
220197
+ import { execFileSync as execFileSync4, spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
220198
+ import { cpSync as cpSync2, existsSync as existsSync18, mkdirSync as mkdirSync12, mkdtempSync as mkdtempSync3, readdirSync as readdirSync7, rmSync as rmSync5, statSync as statSync5, writeFileSync as writeFileSync14 } from "node:fs";
220199
+ import { tmpdir as tmpdir3 } from "node:os";
220200
+ import { basename as basename6, join as join19, resolve as resolve6 } from "node:path";
220090
220201
  var mb = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)} MB`;
220091
220202
  var dirSize = (dir) => readdirSync7(dir, { withFileTypes: true }).reduce((sum, e2) => {
220092
220203
  const p2 = join19(dir, e2.name);
220093
- return sum + (e2.isDirectory() ? dirSize(p2) : statSync5(p2).size);
220204
+ return sum + (e2.isDirectory() ? dirSize(p2) : e2.isSymbolicLink() ? 0 : statSync5(p2).size);
220205
+ }, 0);
220206
+ var cleanBundleName = (name) => name.replace(/[/:]/g, "").trim() || "App";
220207
+ var macHostAppOf = (exe) => {
220208
+ const app = resolve6(exe, "..", "..", "..");
220209
+ if (!app.endsWith(".app") || !existsSync18(join19(app, "Contents", "Info.plist"))) {
220210
+ throw new CliError(`The macOS host must be an .app bundle (got ${exe}). Install LeCodes Desktop.app, or point ` + `LECODES_DESKTOP_EXE at <Build>.app/Contents/MacOS/lecodes-app — a bare SwiftPM binary has no ` + `resource bundles and can't be re-signed into an app.`);
220211
+ }
220212
+ return app;
220213
+ };
220214
+ var macHostProvidedNames = (exe) => {
220215
+ const names3 = new Set;
220216
+ const walk = (dir) => {
220217
+ for (const e2 of readdirSync7(dir, { withFileTypes: true })) {
220218
+ if (e2.isDirectory())
220219
+ walk(join19(dir, e2.name));
220220
+ else
220221
+ names3.add(e2.name);
220222
+ }
220223
+ };
220224
+ const res = join19(macHostAppOf(exe), "Contents", "Resources");
220225
+ if (existsSync18(res))
220226
+ walk(res);
220227
+ return names3;
220228
+ };
220229
+ var plistRead = (plist, key) => {
220230
+ const r2 = spawnSync5("/usr/libexec/PlistBuddy", ["-c", `Print :${key}`, plist], { encoding: "utf8" });
220231
+ return r2.status === 0 ? r2.stdout.trim() : null;
220232
+ };
220233
+ var plistSet = (plist, key, value) => {
220234
+ const quoted = `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
220235
+ const set = spawnSync5("/usr/libexec/PlistBuddy", ["-c", `Set :${key} ${quoted}`, plist], { stdio: "ignore" });
220236
+ if (set.status !== 0)
220237
+ execFileSync4("/usr/libexec/PlistBuddy", ["-c", `Add :${key} string ${quoted}`, plist], { stdio: "ignore" });
220238
+ };
220239
+ var macHostVersion = (exe) => {
220240
+ const plist = join19(macHostAppOf(exe), "Contents", "Info.plist");
220241
+ return plistRead(plist, "LeCodesHostVersion") ?? plistRead(plist, "CFBundleShortVersionString") ?? "unknown";
220242
+ };
220243
+ var macHostFeatures = (exe) => {
220244
+ const raw = plistRead(join19(macHostAppOf(exe), "Contents", "Info.plist"), "LeCodesHostFeatures") ?? "";
220245
+ return new Set(raw.split(`
220246
+ `).map((l2) => l2.trim()).filter((l2) => l2 && l2 !== "Array {" && l2 !== "}"));
220247
+ };
220248
+ var assertMacHostCanBundle = (exe) => {
220249
+ if (macHostFeatures(exe).has("project-bundle"))
220250
+ return;
220251
+ throw new CliError(`This LeCodes Desktop host (${macHostAppOf(exe)}) predates project bundles — an app built from it would ` + `open the host's home screen instead of the project.
220252
+ ` + " Run `lecodes desktop update` (needs a published macOS host with project-bundle support), or point " + "LECODES_DESKTOP_EXE at a current build (…/LeCodes Desktop.app/Contents/MacOS/lecodes-app).");
220253
+ };
220254
+ var developerIdInKeychain = () => {
220255
+ const r2 = spawnSync5("security", ["find-identity", "-v", "-p", "codesigning"], { encoding: "utf8" });
220256
+ if (r2.status !== 0)
220257
+ return null;
220258
+ return /"(Developer ID Application: [^"]+)"/.exec(r2.stdout)?.[1] ?? null;
220259
+ };
220260
+ var resolveMacSigning = (cfg, overrides, keychainDeveloperId) => {
220261
+ let identity = overrides.sign ?? overrides.env.LECODES_MACOS_SIGN_ID ?? cfg?.signing?.identity;
220262
+ if (identity === undefined || identity === "" || identity === "auto")
220263
+ identity = keychainDeveloperId() ?? "-";
220264
+ if (identity === "adhoc" || identity === "ad-hoc")
220265
+ identity = "-";
220266
+ const notaryProfile = overrides.noNotarize ? null : overrides.notarize ?? overrides.env.LECODES_NOTARY_PROFILE ?? cfg?.notarize?.profile ?? null;
220267
+ if (notaryProfile && identity === "-") {
220268
+ throw new CliError(`Notarization needs a Developer ID signature — an ad-hoc-signed app can't be notarized.
220269
+ ` + ' Set desktop.macos.signing.identity (or --sign) to a "Developer ID Application: …" certificate, ' + "or drop the notarize profile (--no-notarize).");
220270
+ }
220271
+ return { identity, notaryProfile: notaryProfile || null };
220272
+ };
220273
+ var FALLBACK_ENTITLEMENTS = `<?xml version="1.0" encoding="UTF-8"?>
220274
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
220275
+ <plist version="1.0">
220276
+ <dict>
220277
+ <key>com.apple.security.cs.allow-jit</key>
220278
+ <true/>
220279
+ </dict>
220280
+ </plist>
220281
+ `;
220282
+ var entitlementsFor = (hostApp) => {
220283
+ const r2 = spawnSync5("codesign", ["-d", "--entitlements", "-", "--xml", hostApp], { encoding: "utf8" });
220284
+ const xml = r2.status === 0 ? r2.stdout.trim() : "";
220285
+ if (xml.startsWith("<?xml") && xml.includes("com.apple.security.cs.allow-jit"))
220286
+ return xml + `
220287
+ `;
220288
+ return FALLBACK_ENTITLEMENTS;
220289
+ };
220290
+ var codesignRetrying = async (args) => {
220291
+ for (let attempt = 1;; attempt++) {
220292
+ const r2 = spawnSync5("codesign", args, { encoding: "utf8" });
220293
+ if (r2.status === 0)
220294
+ return;
220295
+ const out = (r2.stdout + `
220296
+ ` + r2.stderr).trim();
220297
+ if (!/timestamp/i.test(out) || attempt >= 5)
220298
+ throw new CliError(`codesign failed:
220299
+ ${out}`);
220300
+ const delay = attempt * 15;
220301
+ warn(`timestamp service unavailable (attempt ${attempt}/5) — retrying in ${delay}s…`);
220302
+ await new Promise((r3) => setTimeout(r3, delay * 1000));
220303
+ }
220304
+ };
220305
+ var signApp = async (app, signing, hostApp, tmp) => {
220306
+ if (signing.identity === "-") {
220307
+ note("Signing ad-hoc (runs on this Mac only — pass a Developer ID identity to distribute)…");
220308
+ await codesignRetrying(["--force", "--sign", "-", app]);
220309
+ return;
220310
+ }
220311
+ note(`Signing with "${signing.identity}" (Hardened Runtime + JIT entitlement)…`);
220312
+ const ent = join19(tmp, "entitlements.plist");
220313
+ writeFileSync14(ent, entitlementsFor(hostApp));
220314
+ await codesignRetrying(["--force", "--options", "runtime", "--timestamp", "--entitlements", ent, "--sign", signing.identity, app]);
220315
+ const verify = spawnSync5("codesign", ["--verify", "--strict", "--verbose=2", app], { encoding: "utf8" });
220316
+ if (verify.status !== 0)
220317
+ throw new CliError(`The signature doesn't verify:
220318
+ ${(verify.stdout + verify.stderr).trim()}`);
220319
+ };
220320
+ var notarizeApp = (app, profile, tmp) => {
220321
+ const zip = join19(tmp, `${basename6(app, ".app")}.zip`);
220322
+ execFileSync4("ditto", ["-c", "-k", "--keepParent", app, zip]);
220323
+ note(`Notarizing with profile "${profile}" — this waits on Apple (usually 1–5 minutes)…`);
220324
+ const r2 = spawnSync5("xcrun", ["notarytool", "submit", zip, "--keychain-profile", profile, "--wait"], { encoding: "utf8" });
220325
+ const out = (r2.stdout + `
220326
+ ` + r2.stderr).trim();
220327
+ const id = /id: ([0-9a-f-]{36})/.exec(out)?.[1];
220328
+ if (r2.status !== 0 || !/status: Accepted/.test(out)) {
220329
+ throw new CliError(`Notarization failed:
220330
+ ${out}
220331
+ ` + (id ? ` Details: xcrun notarytool log ${id} --keychain-profile ${profile}` : ""));
220332
+ }
220333
+ const staple = spawnSync5("xcrun", ["stapler", "staple", app], { encoding: "utf8" });
220334
+ if (staple.status !== 0)
220335
+ throw new CliError(`Stapling failed:
220336
+ ${(staple.stdout + staple.stderr).trim()}`);
220337
+ success("Notarized + stapled.");
220338
+ };
220339
+ var gatekeeperVerdict = (app) => {
220340
+ const r2 = spawnSync5("spctl", ["-a", "-t", "exec", "-vv", app], { encoding: "utf8" });
220341
+ const line = (r2.stdout + r2.stderr).trim().split(`
220342
+ `).filter((l2) => l2.includes("source=") || l2.includes("rejected") || l2.includes("accepted")).join("; ");
220343
+ return { accepted: r2.status === 0, line: line || (r2.stdout + r2.stderr).trim() };
220344
+ };
220345
+ var writeIcns = async (root, config, dest, tmp) => {
220346
+ const master = await loadIconMaster(root, config);
220347
+ const { Jimp: Jimp2 } = await Promise.resolve().then(() => (init_esm30(), exports_esm2));
220348
+ const SIZE = 1024, INSET = 100, RADIUS = 185;
220349
+ const tileSize = SIZE - 2 * INSET;
220350
+ const tile = new Jimp2({ width: tileSize, height: tileSize, color: master.background });
220351
+ const art = master.image.clone();
220352
+ art.resize({ w: tileSize, h: tileSize });
220353
+ tile.composite(art, 0, 0);
220354
+ const { data } = tile.bitmap;
220355
+ for (let y2 = 0;y2 < tileSize; y2++) {
220356
+ for (let x2 = 0;x2 < tileSize; x2++) {
220357
+ const cx = x2 < RADIUS ? RADIUS : x2 >= tileSize - RADIUS ? tileSize - RADIUS - 1 : x2;
220358
+ const cy = y2 < RADIUS ? RADIUS : y2 >= tileSize - RADIUS ? tileSize - RADIUS - 1 : y2;
220359
+ const dx = x2 - cx, dy = y2 - cy;
220360
+ if (dx * dx + dy * dy > RADIUS * RADIUS)
220361
+ data[(y2 * tileSize + x2) * 4 + 3] = 0;
220362
+ }
220363
+ }
220364
+ const canvas = new Jimp2({ width: SIZE, height: SIZE, color: 0 });
220365
+ canvas.composite(tile, INSET, INSET);
220366
+ const iconset = join19(tmp, "AppIcon.iconset");
220367
+ mkdirSync12(iconset, { recursive: true });
220368
+ for (const pt of [16, 32, 128, 256, 512]) {
220369
+ for (const scale of [1, 2]) {
220370
+ const px = pt * scale;
220371
+ const frame = canvas.clone();
220372
+ frame.resize({ w: px, h: px });
220373
+ writeFileSync14(join19(iconset, `icon_${pt}x${pt}${scale === 2 ? "@2x" : ""}.png`), await frame.getBuffer("image/png"));
220374
+ }
220375
+ }
220376
+ execFileSync4("iconutil", ["-c", "icns", "-o", dest, iconset]);
220377
+ };
220378
+ var probeMacApp = async (app, out, args) => {
220379
+ const after = Number(flagStr(args, "after")) || 8;
220380
+ const shot = join19(out, `${basename6(app, ".app")}-probe.png`);
220381
+ rmSync5(shot, { force: true });
220382
+ note(`Probe: launching the built app for ${after + 2}s (the window will show)…`);
220383
+ await new Promise((done, fail) => {
220384
+ const child = spawn3(join19(app, "Contents", "MacOS", "lecodes-app"), [], {
220385
+ cwd: "/",
220386
+ env: { ...process.env, LECODES_SNAPSHOT: shot, LECODES_SNAPSHOT_AFTER: String(after), LECODES_EXIT_AFTER: String(after + 2) },
220387
+ stdio: ["ignore", "pipe", "pipe"]
220388
+ });
220389
+ const tail = [];
220390
+ const sink = (d) => {
220391
+ tail.push(String(d));
220392
+ while (tail.length > 60)
220393
+ tail.shift();
220394
+ };
220395
+ child.stdout.on("data", sink);
220396
+ child.stderr.on("data", sink);
220397
+ const killer = setTimeout(() => {
220398
+ child.kill();
220399
+ fail(new CliError("The probe timed out after 120s."));
220400
+ }, 120000);
220401
+ child.on("error", (e2) => {
220402
+ clearTimeout(killer);
220403
+ fail(new CliError(`Probe couldn't run: ${e2.message}`));
220404
+ });
220405
+ child.on("exit", (code) => {
220406
+ clearTimeout(killer);
220407
+ const detail = tail.length > 0 ? `
220408
+ ${tail.join("").trimEnd()}` : "";
220409
+ if (code === 0 && existsSync18(shot)) {
220410
+ success(`Probe: ${shot}`);
220411
+ done();
220412
+ } else
220413
+ fail(new CliError(`Probe failed (exit ${code}).${detail}`));
220414
+ });
220415
+ });
220416
+ };
220417
+ var buildMacApp = async (input) => {
220418
+ const { root, config, name, slug, out, js, shipped, hostExe, args } = input;
220419
+ const hostApp = macHostAppOf(hostExe);
220420
+ assertMacHostCanBundle(hostExe);
220421
+ const mac = config.desktop?.macos ?? {};
220422
+ const appName = cleanBundleName(name);
220423
+ const app = join19(out, `${appName}.app`);
220424
+ const tmp = mkdtempSync3(join19(tmpdir3(), "lecodes-macapp-"));
220425
+ try {
220426
+ note(` host: LeCodes Desktop ${macHostVersion(hostExe)} (${hostApp})`);
220427
+ rmSync5(out, { recursive: true, force: true });
220428
+ mkdirSync12(out, { recursive: true });
220429
+ execFileSync4("cp", ["-R", hostApp, app]);
220430
+ execFileSync4("xattr", ["-cr", app]);
220431
+ rmSync5(join19(app, "Contents", "_CodeSignature"), { recursive: true, force: true });
220432
+ rmSync5(join19(app, "Contents", "CodeResources"), { force: true });
220433
+ const proj = join19(app, "Contents", "Resources", "project");
220434
+ mkdirSync12(join19(proj, "assets"), { recursive: true });
220435
+ writeFileSync14(join19(proj, "app.js"), js);
220436
+ let assetBytes = 0, assetCount = 0;
220437
+ for (const r2 of shipped) {
220438
+ if (!r2.absPath || !existsSync18(r2.absPath) || !statSync5(r2.absPath).isFile()) {
220439
+ warn(`asset "${r2.name}" (${r2.path}) has no local file — skipped`);
220440
+ continue;
220441
+ }
220442
+ cpSync2(r2.absPath, join19(proj, "assets", r2.name));
220443
+ assetBytes += statSync5(r2.absPath).size;
220444
+ assetCount++;
220445
+ }
220446
+ note(` assets: ${assetCount} files, ${mb(assetBytes)} → Contents/Resources/project/assets/`);
220447
+ writeJson(join19(proj, "app.json"), { name, ...config.desktop ? { desktop: { window: config.desktop.window } } : {} });
220448
+ const plist = join19(app, "Contents", "Info.plist");
220449
+ const bundleId = mac.bundleId ?? `codes.le.app.${slug.toLowerCase().replace(/[^a-z0-9.-]+/g, "-")}`;
220450
+ plistSet(plist, "CFBundleName", appName);
220451
+ plistSet(plist, "CFBundleDisplayName", appName);
220452
+ plistSet(plist, "CFBundleIdentifier", bundleId);
220453
+ plistSet(plist, "CFBundleShortVersionString", config.version ?? "1.0");
220454
+ plistSet(plist, "CFBundleVersion", String(mac.buildNumber ?? 1));
220455
+ if (mac.category)
220456
+ plistSet(plist, "LSApplicationCategoryType", mac.category);
220457
+ if (config.icon) {
220458
+ await writeIcns(root, config, join19(app, "Contents", "Resources", "AppIcon.icns"), tmp);
220459
+ plistSet(plist, "CFBundleIconFile", "AppIcon");
220460
+ note(" icon: AppIcon.icns rendered from " + config.icon);
220461
+ }
220462
+ note(` bundle: ${bundleId} ${config.version ?? "1.0"} (${mac.buildNumber ?? 1})`);
220463
+ const signing = resolveMacSigning(mac, {
220464
+ sign: flagStr(args, "sign"),
220465
+ notarize: flagStr(args, "notarize"),
220466
+ noNotarize: flagBool(args, "no-notarize"),
220467
+ env: process.env
220468
+ }, developerIdInKeychain);
220469
+ await signApp(app, signing, hostApp, tmp);
220470
+ if (signing.notaryProfile) {
220471
+ notarizeApp(app, signing.notaryProfile, tmp);
220472
+ } else if (signing.identity !== "-") {
220473
+ warn("Signed with Developer ID but NOT notarized — Gatekeeper refuses a downloaded copy on every other Mac.");
220474
+ warn(" Set desktop.macos.notarize.profile in app.json (a `xcrun notarytool store-credentials` profile) or pass --notarize <profile>.");
220475
+ }
220476
+ const verdict = gatekeeperVerdict(app);
220477
+ log(`${verdict.accepted ? c.green("gatekeeper") : c.yellow("gatekeeper")} ${verdict.line}`);
220478
+ if (signing.notaryProfile && !verdict.accepted) {
220479
+ throw new CliError("The app was notarized but Gatekeeper still rejects it — see the spctl line above.");
220480
+ }
220481
+ writeFileSync14(join19(out, "README.txt"), readmeText(appName, !!signing.notaryProfile, signing.identity !== "-"));
220482
+ success(`App: ${app} (${mb(dirSize(app))})`);
220483
+ if (flagBool(args, "probe"))
220484
+ await probeMacApp(app, out, args);
220485
+ if (flagBool(args, "zip")) {
220486
+ const zip = join19(out, `${appName}.zip`);
220487
+ rmSync5(zip, { force: true });
220488
+ execFileSync4("ditto", ["-c", "-k", "--keepParent", app, zip]);
220489
+ success(`Zip: ${zip} (${mb(statSync5(zip).size)})`);
220490
+ } else {
220491
+ note("Tip: --zip packs the .app for sending (ditto, structure-preserving).");
220492
+ }
220493
+ } finally {
220494
+ rmSync5(tmp, { recursive: true, force: true });
220495
+ }
220496
+ };
220497
+ var readmeText = (name, notarized, developerId) => `${name}
220498
+ ${"=".repeat(Math.max(name.length, 4))}
220499
+
220500
+ A LeCodes desktop app for macOS (Apple Silicon). Nothing to install: unzip and double-click
220501
+ ${name}.app — or drag it to Applications first.
220502
+
220503
+ ${notarized ? `The app is signed and notarized by Apple: macOS asks once whether to open a downloaded app,
220504
+ then it runs.` : developerId ? `The app is signed but NOT notarized: macOS will refuse to open the downloaded copy. To run it
220505
+ anyway: System Settings › Privacy & Security › "Open Anyway" (after the first refused launch),
220506
+ or remove the quarantine flag in Terminal: xattr -dr com.apple.quarantine "` + name + '.app"' : `The app is not signed for distribution: macOS will refuse to open the downloaded copy. To run it
220507
+ anyway: System Settings › Privacy & Security › "Open Anyway" (after the first refused launch),
220508
+ or remove the quarantine flag in Terminal: xattr -dr com.apple.quarantine "` + name + '.app"'}
220509
+ `;
220510
+
220511
+ // src/commands/appDesktop.ts
220512
+ var mb2 = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)} MB`;
220513
+ var dirSize2 = (dir) => readdirSync8(dir, { withFileTypes: true }).reduce((sum, e2) => {
220514
+ const p2 = join20(dir, e2.name);
220515
+ return sum + (e2.isDirectory() ? dirSize2(p2) : statSync6(p2).size);
220094
220516
  }, 0);
220095
220517
  var initDesktop = async (args) => {
220096
220518
  const root = projectRoot();
220097
- const existing = readJson(join19(root, "app.json")) ?? {};
220098
- const name = flagStr(args, "name") ?? existing.name ?? basename6(root);
220519
+ const existing = readJson(join20(root, "app.json")) ?? {};
220520
+ const name = flagStr(args, "name") ?? existing.name ?? basename7(root);
220099
220521
  const window2 = {
220100
220522
  width: 1280,
220101
220523
  height: 800,
@@ -220105,7 +220527,7 @@ var initDesktop = async (args) => {
220105
220527
  ...flagBool(args, "fullscreen") ? { fullscreen: true } : {}
220106
220528
  };
220107
220529
  const config = { ...existing, name, desktop: { ...existing.desktop, window: window2 } };
220108
- writeJson(join19(root, "app.json"), config);
220530
+ writeJson(join20(root, "app.json"), config);
220109
220531
  log(`${c.green("written")} app.json (desktop.window ${window2.width}×${window2.height}${window2.fullscreen ? ", fullscreen" : ""})`);
220110
220532
  log(`Next: ${c.bold("lecodes dev --desktop")} to develop, ${c.bold("lecodes app build desktop")} to package.`);
220111
220533
  };
@@ -220114,13 +220536,13 @@ var ASSETS_DIR = "assets";
220114
220536
  var RUNTIME_DIR = "runtime";
220115
220537
  var buildDesktop = async (args) => {
220116
220538
  const root = projectRoot();
220117
- const config = readJson(join19(root, "app.json")) ?? {};
220118
- const name = (config.name ?? basename6(root)).trim() || "app";
220539
+ const config = readJson(join20(root, "app.json")) ?? {};
220540
+ const name = (config.name ?? basename7(root)).trim() || "app";
220119
220541
  const slug = flagStr(args, "slug") ?? cleanFileName(name);
220120
- const out = resolve6(root, flagStr(args, "o", "out") ?? join19(root, "dist", slug));
220121
- if (existsSync18(out)) {
220122
- const entries = readdirSync7(out);
220123
- const looksLikeBuild = entries.length === 0 || entries.includes("app.js");
220542
+ const out = resolve7(root, flagStr(args, "o", "out") ?? join20(root, "dist", slug));
220543
+ if (existsSync19(out)) {
220544
+ const entries = readdirSync8(out);
220545
+ const looksLikeBuild = entries.length === 0 || entries.includes("app.js") || entries.some((e2) => e2.endsWith(".app"));
220124
220546
  if (!looksLikeBuild) {
220125
220547
  throw new CliError(`${out} exists and doesn't look like a previous desktop build — pick another -o or clear it yourself.`);
220126
220548
  }
@@ -220128,7 +220550,8 @@ var buildDesktop = async (args) => {
220128
220550
  if (config.desktop?.entry && flagStr(args, "entry") === undefined)
220129
220551
  args.flags.entry = config.desktop.entry;
220130
220552
  const renderer = resolveDesktopRenderer(root, args);
220131
- note(`Building "${name}" for desktop${renderer === "vulkan" ? " (Vulkan)" : ""}…`);
220553
+ const mac = process.platform === "darwin";
220554
+ note(`Building "${name}" for ${mac ? "macOS" : "desktop"}${renderer === "vulkan" ? " (Vulkan)" : ""}…`);
220132
220555
  const { js, resources } = await buildBundle(args, { remotePatterns: config.resources?.remote ?? [] }, { localShaders: true, desktopRenderer: renderer });
220133
220556
  assertRendererSupportsBundle(renderer, js);
220134
220557
  const wanted = new Set([...js.matchAll(/fetchLocal\("([^"]+)"\)/g)].map((m) => m[1]));
@@ -220152,55 +220575,59 @@ var buildDesktop = async (args) => {
220152
220575
  const exe = await resolveDesktopExe(renderer);
220153
220576
  const hostDir = dirname11(exe);
220154
220577
  const IMPLICIT_PRELOADS = new Set(["uberarchive", "capture"]);
220155
- const hostProvided = new Set;
220156
- try {
220157
- for (const f3 of readdirSync7(join19(hostDir, "assets")))
220158
- hostProvided.add(f3);
220159
- } catch {}
220578
+ const hostProvided = mac ? macHostProvidedNames(exe) : new Set;
220579
+ if (!mac) {
220580
+ try {
220581
+ for (const f3 of readdirSync8(join20(hostDir, "assets")))
220582
+ hostProvided.add(f3);
220583
+ } catch {}
220584
+ }
220160
220585
  const shippedNames = new Set(shipped.map((r2) => r2.name));
220161
220586
  const missing = [...wanted].filter((n2) => !shippedNames.has(n2) && !hostProvided.has(n2) && !IMPLICIT_PRELOADS.has(n2));
220162
220587
  if (missing.length > 0) {
220163
220588
  throw new CliError(`The bundle references assets with no local file: ${missing.join(", ")}`);
220164
220589
  }
220590
+ if (mac)
220591
+ return buildMacApp({ root, config, name, slug, out, js, shipped, hostExe: exe, args });
220165
220592
  const version = process.env.LECODES_DESKTOP_EXE ? "local build" : selectedDesktopVersion();
220166
- rmSync5(out, { recursive: true, force: true });
220167
- mkdirSync12(out, { recursive: true });
220168
- for (const entry of readdirSync7(hostDir, { withFileTypes: true })) {
220593
+ rmSync6(out, { recursive: true, force: true });
220594
+ mkdirSync13(out, { recursive: true });
220595
+ for (const entry of readdirSync8(hostDir, { withFileTypes: true })) {
220169
220596
  if (entry.isFile() && entry.name.toLowerCase().endsWith(".dll")) {
220170
- cpSync2(join19(hostDir, entry.name), join19(out, entry.name));
220597
+ cpSync3(join20(hostDir, entry.name), join20(out, entry.name));
220171
220598
  } else if (entry.isDirectory() && entry.name === "assets") {
220172
- cpSync2(join19(hostDir, entry.name), join19(out, RUNTIME_DIR), { recursive: true });
220599
+ cpSync3(join20(hostDir, entry.name), join20(out, RUNTIME_DIR), { recursive: true });
220173
220600
  } else if (entry.isDirectory() && entry.name === "fonts") {
220174
- cpSync2(join19(hostDir, entry.name), join19(out, entry.name), { recursive: true });
220601
+ cpSync3(join20(hostDir, entry.name), join20(out, entry.name), { recursive: true });
220175
220602
  }
220176
220603
  }
220177
- if (!existsSync18(join19(out, RUNTIME_DIR, "initial.js"))) {
220604
+ if (!existsSync19(join20(out, RUNTIME_DIR, "initial.js"))) {
220178
220605
  throw new CliError(`The host at ${hostDir} has no assets/initial.js next to it — a staged build can't boot. ` + `(LECODES_DESKTOP_EXE must point at an exe with its runtime assets/ beside it.)`);
220179
220606
  }
220180
220607
  const exeName2 = process.platform === "win32" ? `${cleanFileName(name)}.exe` : cleanFileName(name);
220181
- cpSync2(exe, join19(out, exeName2));
220608
+ cpSync3(exe, join20(out, exeName2));
220182
220609
  note(` host: LeCodes Desktop ${version}`);
220183
- writeFileSync14(join19(out, "app.js"), js);
220610
+ writeFileSync15(join20(out, "app.js"), js);
220184
220611
  let assetBytes = 0, assetCount = 0;
220185
220612
  if (shipped.length > 0)
220186
- mkdirSync12(join19(out, ASSETS_DIR), { recursive: true });
220613
+ mkdirSync13(join20(out, ASSETS_DIR), { recursive: true });
220187
220614
  for (const r2 of shipped) {
220188
- if (!r2.absPath || !existsSync18(r2.absPath) || !statSync5(r2.absPath).isFile()) {
220615
+ if (!r2.absPath || !existsSync19(r2.absPath) || !statSync6(r2.absPath).isFile()) {
220189
220616
  warn(`asset "${r2.name}" (${r2.path}) has no local file — skipped`);
220190
220617
  continue;
220191
220618
  }
220192
- cpSync2(r2.absPath, join19(out, ASSETS_DIR, r2.name));
220193
- assetBytes += statSync5(r2.absPath).size;
220619
+ cpSync3(r2.absPath, join20(out, ASSETS_DIR, r2.name));
220620
+ assetBytes += statSync6(r2.absPath).size;
220194
220621
  assetCount++;
220195
220622
  }
220196
- note(` assets: ${assetCount} files, ${mb(assetBytes)} → ${ASSETS_DIR}/`);
220197
- writeJson(join19(out, "app.json"), { name, ...config.desktop ? { desktop: { window: config.desktop.window } } : {} });
220623
+ note(` assets: ${assetCount} files, ${mb2(assetBytes)} → ${ASSETS_DIR}/`);
220624
+ writeJson(join20(out, "app.json"), { name, ...config.desktop ? { desktop: { window: config.desktop.window } } : {} });
220198
220625
  if (config.icon) {
220199
- await writeIco(root, config, join19(out, "app.ico"));
220626
+ await writeIco(root, config, join20(out, "app.ico"));
220200
220627
  note(" app.ico rendered from " + config.icon);
220201
220628
  }
220202
- writeFileSync14(join19(out, "README.txt"), "\uFEFF" + readmeText(name, exeName2, version));
220203
- success(`Portable build: ${out} (${mb(dirSize(out))})`);
220629
+ writeFileSync15(join20(out, "README.txt"), "\uFEFF" + readmeText2(name, exeName2, version));
220630
+ success(`Portable build: ${out} (${mb2(dirSize2(out))})`);
220204
220631
  if (flagBool(args, "probe"))
220205
220632
  await probeBuild(out, exeName2, config, args);
220206
220633
  if (flagBool(args, "zip"))
@@ -220232,15 +220659,15 @@ var writeIco = async (root, config, dest) => {
220232
220659
  header.writeUInt32LE(offset, e2 + 12);
220233
220660
  offset += pngs[i2].length;
220234
220661
  });
220235
- writeFileSync14(dest, Buffer.concat([header, ...pngs]));
220662
+ writeFileSync15(dest, Buffer.concat([header, ...pngs]));
220236
220663
  };
220237
220664
  var probeBuild = async (out, exeName2, config, args) => {
220238
220665
  const win = config.desktop?.window;
220239
220666
  const frames = Number(flagStr(args, "frames")) || 90;
220240
- const shot = join19(dirname11(out), `${basename6(out)}-probe.png`);
220667
+ const shot = join20(dirname11(out), `${basename7(out)}-probe.png`);
220241
220668
  note(`Probe: hidden render of the staged folder (${frames} frames)…`);
220242
220669
  await new Promise((done, fail) => {
220243
- const child = spawn3(join19(out, exeName2), [], {
220670
+ const child = spawn4(join20(out, exeName2), [], {
220244
220671
  cwd: out,
220245
220672
  env: {
220246
220673
  ...process.env,
@@ -220269,7 +220696,7 @@ var probeBuild = async (out, exeName2, config, args) => {
220269
220696
  });
220270
220697
  child.on("exit", (code) => {
220271
220698
  clearTimeout(killer);
220272
- if (code === 0 && existsSync18(shot)) {
220699
+ if (code === 0 && existsSync19(shot)) {
220273
220700
  success(`Probe: ${shot}`);
220274
220701
  done();
220275
220702
  } else {
@@ -220282,11 +220709,11 @@ ${tail.join("").trimEnd()}` : "";
220282
220709
  };
220283
220710
  var zipBuild = (out) => {
220284
220711
  const zip = `${out}.zip`;
220285
- rmSync5(zip, { force: true });
220712
+ rmSync6(zip, { force: true });
220286
220713
  note("Zipping…");
220287
220714
  if (process.platform === "win32") {
220288
220715
  const q = (s2) => s2.replaceAll("'", "''");
220289
- execFileSync3("powershell", ["-NoProfile", "-Command", [
220716
+ execFileSync5("powershell", ["-NoProfile", "-Command", [
220290
220717
  "Add-Type -AssemblyName System.IO.Compression.FileSystem;",
220291
220718
  `$src='${q(out)}'; $base=Split-Path $src -Parent;`,
220292
220719
  `$zip=[System.IO.Compression.ZipFile]::Open('${q(zip)}','Create');`,
@@ -220296,11 +220723,11 @@ var zipBuild = (out) => {
220296
220723
  "$zip.Dispose()"
220297
220724
  ].join(" ")], { stdio: ["ignore", "ignore", "pipe"] });
220298
220725
  } else {
220299
- execFileSync3("zip", ["-rq", zip, basename6(out)], { cwd: dirname11(out) });
220726
+ execFileSync5("zip", ["-rq", zip, basename7(out)], { cwd: dirname11(out) });
220300
220727
  }
220301
- success(`Zip: ${zip} (${mb(statSync5(zip).size)})`);
220728
+ success(`Zip: ${zip} (${mb2(statSync6(zip).size)})`);
220302
220729
  };
220303
- var readmeText = (name, exeName2, version) => `${name}
220730
+ var readmeText2 = (name, exeName2, version) => `${name}
220304
220731
  ${"=".repeat(Math.max(name.length, 4))}
220305
220732
 
220306
220733
  A LeCodes desktop app. Nothing to install: unpack the WHOLE folder anywhere and run ${exeName2}.
@@ -220319,10 +220746,10 @@ var SDK_REPO = "letary/lecodes-ios-sdk";
220319
220746
  var SDK_MIN_VERSION = "1.2.0";
220320
220747
  var SDK_IDENTITY_VERSION = "1.6.0";
220321
220748
  var shellSdkVersion = (iosDir) => {
220322
- const path2 = join20(iosDir, "LeCodesRuntime", "Package.swift");
220323
- if (!existsSync19(path2))
220749
+ const path2 = join21(iosDir, "LeCodesRuntime", "Package.swift");
220750
+ if (!existsSync20(path2))
220324
220751
  return null;
220325
- const m = /lecodes-ios-sdk\.git",\s*(?:exact|from):\s*"([^"]+)"/.exec(readFileSync16(path2, "utf8"));
220752
+ const m = /lecodes-ios-sdk\.git",\s*(?:exact|from):\s*"([^"]+)"/.exec(readFileSync17(path2, "utf8"));
220326
220753
  return m && VERSION_RE.test(m[1]) ? m[1] : null;
220327
220754
  };
220328
220755
  var IOS_SDK_OPTS = { repo: SDK_REPO, minVersion: SDK_MIN_VERSION, configKey: "ios.sdk" };
@@ -220336,11 +220763,11 @@ var appInit = async (args) => {
220336
220763
  throw new CliError(platform ? `Unsupported platform "${platform}" — use ios, android or desktop.` : "Usage: lecodes app init <ios|android|desktop> [--name <display name>] [--bundle-id <id>] [--team <team id>] [--variant auto|core|2d|3d|full] [--app-id <application id>] [--sdk <version|latest>]");
220337
220764
  }
220338
220765
  const root = projectRoot();
220339
- const iosDir = join20(root, "ios");
220340
- if (existsSync19(iosDir)) {
220766
+ const iosDir = join21(root, "ios");
220767
+ if (existsSync20(iosDir)) {
220341
220768
  throw new CliError("ios/ already exists — run `lecodes app sync` to update it.");
220342
220769
  }
220343
- const existing = readJson(join20(root, "app.json")) ?? {};
220770
+ const existing = readJson(join21(root, "app.json")) ?? {};
220344
220771
  const variant = flagStr(args, "variant") ?? existing.ios?.variant ?? "auto";
220345
220772
  if (variant !== "auto" && !SDK_VARIANTS.includes(variant)) {
220346
220773
  throw new CliError(`--variant must be auto, ${SDK_VARIANTS.join(", ")} (got "${variant}")`);
@@ -220348,7 +220775,7 @@ var appInit = async (args) => {
220348
220775
  const sdkFlag = flagStr(args, "sdk") ?? existing.ios?.sdk;
220349
220776
  const sdkVersion = await resolveSdkVersion(process.env.LECODES_IOS_SDK ?? sdkFlag ?? "latest", null, IOS_SDK_OPTS);
220350
220777
  const sdk = sdkFlag === "latest" ? "latest" : sdkVersion;
220351
- const name = flagStr(args, "name") ?? existing.name ?? basename7(root);
220778
+ const name = flagStr(args, "name") ?? existing.name ?? basename8(root);
220352
220779
  const config = {
220353
220780
  ...existing,
220354
220781
  name,
@@ -220362,7 +220789,7 @@ var appInit = async (args) => {
220362
220789
  buildNumber: existing.ios?.buildNumber ?? 1
220363
220790
  }
220364
220791
  };
220365
- writeJson(join20(root, "app.json"), config);
220792
+ writeJson(join21(root, "app.json"), config);
220366
220793
  log(`${c.green("written")} app.json`);
220367
220794
  ensureShellIgnored(root, "ios");
220368
220795
  const identity = {
@@ -220373,9 +220800,9 @@ var appInit = async (args) => {
220373
220800
  buildNumber: config.ios.buildNumber
220374
220801
  };
220375
220802
  const write = (rel, content) => {
220376
- const abs = join20(iosDir, rel);
220377
- mkdirSync13(dirname12(abs), { recursive: true });
220378
- writeFileSync15(abs, content);
220803
+ const abs = join21(iosDir, rel);
220804
+ mkdirSync14(dirname12(abs), { recursive: true });
220805
+ writeFileSync16(abs, content);
220379
220806
  log(`${c.green("created")} ios/${rel}`);
220380
220807
  };
220381
220808
  write("README.md", IOS_README);
@@ -220403,23 +220830,23 @@ var ensureSdkResourcesBundle = async (sdkVersion) => {
220403
220830
  const override = process.env.LECODES_SDK_RESOURCES;
220404
220831
  if (override) {
220405
220832
  note(`Using local resources bundle ${override} (LECODES_SDK_RESOURCES)`);
220406
- return resolve7(override);
220833
+ return resolve8(override);
220407
220834
  }
220408
- const dir = join20(homedir7(), ".lecodes", "sdk-resources", sdkVersion);
220409
- const bundle = join20(dir, "LeCodesSDKResources.bundle");
220410
- if (existsSync19(bundle))
220835
+ const dir = join21(homedir7(), ".lecodes", "sdk-resources", sdkVersion);
220836
+ const bundle = join21(dir, "LeCodesSDKResources.bundle");
220837
+ if (existsSync20(bundle))
220411
220838
  return bundle;
220412
220839
  const url = `https://github.com/${SDK_REPO}/releases/download/${sdkVersion}/LeCodesSDKResources.bundle.zip`;
220413
220840
  note(`Downloading LeCodesSDKResources.bundle (SDK ${sdkVersion})…`);
220414
220841
  const res = await fetch(url);
220415
220842
  if (!res.ok)
220416
220843
  throw new CliError(`Resources bundle download failed (HTTP ${res.status}) — ${url}`);
220417
- mkdirSync13(dir, { recursive: true });
220418
- const zip = join20(dir, "LeCodesSDKResources.bundle.zip");
220419
- writeFileSync15(zip, new Uint8Array(await res.arrayBuffer()));
220420
- execFileSync4("ditto", ["-x", "-k", zip, dir]);
220421
- rmSync6(zip);
220422
- if (!existsSync19(bundle))
220844
+ mkdirSync14(dir, { recursive: true });
220845
+ const zip = join21(dir, "LeCodesSDKResources.bundle.zip");
220846
+ writeFileSync16(zip, new Uint8Array(await res.arrayBuffer()));
220847
+ execFileSync6("ditto", ["-x", "-k", zip, dir]);
220848
+ rmSync7(zip);
220849
+ if (!existsSync20(bundle))
220423
220850
  throw new CliError(`No LeCodesSDKResources.bundle inside the release asset ${url}`);
220424
220851
  return bundle;
220425
220852
  };
@@ -220444,13 +220871,13 @@ var resolveUpdateUrl = (update, root) => {
220444
220871
  };
220445
220872
  var appSync = async (args) => {
220446
220873
  const root = projectRoot();
220447
- const config = readJson(join20(root, "app.json"));
220874
+ const config = readJson(join21(root, "app.json"));
220448
220875
  const platform = args._[1];
220449
220876
  if (platform !== undefined && platform !== "ios" && platform !== "android") {
220450
220877
  throw new CliError(`Unknown platform "${platform}" — use ios or android.`);
220451
220878
  }
220452
- const hasIos = !!config?.ios && existsSync19(join20(root, "ios"));
220453
- const hasAndroid = !!config?.android && existsSync19(join20(root, "android"));
220879
+ const hasIos = !!config?.ios && existsSync20(join21(root, "ios"));
220880
+ const hasAndroid = !!config?.android && existsSync20(join21(root, "android"));
220454
220881
  if (platform === "ios" && !hasIos)
220455
220882
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
220456
220883
  if (platform === "android" && !hasAndroid)
@@ -220465,7 +220892,7 @@ var appSync = async (args) => {
220465
220892
  await syncAndroid(root, config, iosArtifacts);
220466
220893
  };
220467
220894
  var syncIos = async (root, config) => {
220468
- const iosDir = join20(root, "ios");
220895
+ const iosDir = join21(root, "ios");
220469
220896
  if (!config.ios)
220470
220897
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
220471
220898
  const requestedVariant = config.ios.variant ?? "auto";
@@ -220475,14 +220902,14 @@ var syncIos = async (root, config) => {
220475
220902
  const linkedSdk = shellSdkVersion(iosDir);
220476
220903
  const sdkPin = process.env.LECODES_IOS_SDK ?? config.ios.sdk ?? linkedSdk ?? "latest";
220477
220904
  const sdkVersion = await resolveSdkVersion(sdkPin, linkedSdk, IOS_SDK_OPTS);
220478
- const lockPath = join20(root, "plugins.lock.json");
220905
+ const lockPath = join21(root, "plugins.lock.json");
220479
220906
  const oldLock = readJson(lockPath)?.plugins ?? {};
220480
220907
  const pluginSpecs = Object.entries(config.plugins ?? {});
220481
220908
  const runtimePlugins = [];
220482
220909
  const infoValues = {};
220483
220910
  const entitlementValues = {};
220484
220911
  const newLock = {};
220485
- const sourcesDir = join20(iosDir, "LeCodesRuntime", "Sources");
220912
+ const sourcesDir = join21(iosDir, "LeCodesRuntime", "Sources");
220486
220913
  for (const [id, spec] of pluginSpecs) {
220487
220914
  const plugin = resolvePlugin(id, spec, oldLock[id], "ios");
220488
220915
  const ios = plugin.manifest.ios;
@@ -220490,12 +220917,12 @@ var syncIos = async (root, config) => {
220490
220917
  const register = ios.register;
220491
220918
  if (!module || !register)
220492
220919
  throw new CliError(`Plugin "${id}": manifest ios.module / ios.register missing`);
220493
- const src = join20(plugin.dir, ios.sources ?? "ios");
220494
- if (!existsSync19(src))
220920
+ const src = join21(plugin.dir, ios.sources ?? "ios");
220921
+ if (!existsSync20(src))
220495
220922
  throw new CliError(`Plugin "${id}": sources dir ${src} missing`);
220496
- const dest = join20(sourcesDir, module);
220497
- rmSync6(dest, { recursive: true, force: true });
220498
- cpSync3(src, dest, { recursive: true });
220923
+ const dest = join21(sourcesDir, module);
220924
+ rmSync7(dest, { recursive: true, force: true });
220925
+ cpSync4(src, dest, { recursive: true });
220499
220926
  if (ios.minSdk && VERSION_RE.test(ios.minSdk) && cmpVersion(sdkVersion, ios.minSdk) < 0) {
220500
220927
  throw new CliError(`Plugin "${id}" needs lecodes-ios-sdk ${ios.minSdk} but the shell links ${sdkVersion} — run \`lecodes app update\` (or set app.json ios.sdk).`);
220501
220928
  }
@@ -220510,9 +220937,9 @@ var syncIos = async (root, config) => {
220510
220937
  log(`${c.green("plugin")} ${id}@${plugin.lock.version} (${plugin.lock.tag} → Sources/${module})`);
220511
220938
  }
220512
220939
  const keep = new Set(["LeCodesRuntime", ...runtimePlugins.map((p2) => p2.module)]);
220513
- for (const entry of existsSync19(sourcesDir) ? readdirSync8(sourcesDir) : []) {
220514
- if (!keep.has(entry) && statSync6(join20(sourcesDir, entry)).isDirectory()) {
220515
- rmSync6(join20(sourcesDir, entry), { recursive: true });
220940
+ for (const entry of existsSync20(sourcesDir) ? readdirSync9(sourcesDir) : []) {
220941
+ if (!keep.has(entry) && statSync7(join21(sourcesDir, entry)).isDirectory()) {
220942
+ rmSync7(join21(sourcesDir, entry), { recursive: true });
220516
220943
  log(`${c.yellow("removed")} ios/LeCodesRuntime/Sources/${entry} (plugin uninstalled)`);
220517
220944
  }
220518
220945
  }
@@ -220520,11 +220947,11 @@ var syncIos = async (root, config) => {
220520
220947
  if (!Array.isArray(remotePatterns) || remotePatterns.some((p2) => typeof p2 !== "string")) {
220521
220948
  throw new CliError('app.json "resources.remote" must be an array of glob strings (e.g. ["videos/**"]).');
220522
220949
  }
220523
- const resourcesDir = join20(iosDir, "App", "Resources");
220524
- rmSync6(resourcesDir, { recursive: true, force: true });
220525
- mkdirSync13(resourcesDir, { recursive: true });
220526
- const resources = await compileShell({ _: [], flags: { o: join20(resourcesDir, "app.js") } }, { remotePatterns });
220527
- const bundleJs = readFileSync16(join20(resourcesDir, "app.js"), "utf8");
220950
+ const resourcesDir = join21(iosDir, "App", "Resources");
220951
+ rmSync7(resourcesDir, { recursive: true, force: true });
220952
+ mkdirSync14(resourcesDir, { recursive: true });
220953
+ const resources = await compileShell({ _: [], flags: { o: join21(resourcesDir, "app.js") } }, { remotePatterns });
220954
+ const bundleJs = readFileSync17(join21(resourcesDir, "app.js"), "utf8");
220528
220955
  const { variant, detected } = resolveVariant(requestedVariant, bundleJs);
220529
220956
  const WHY = {
220530
220957
  core: "UI only",
@@ -220538,8 +220965,8 @@ var syncIos = async (root, config) => {
220538
220965
  const sdkProvided = new Set;
220539
220966
  if (variant === "3d" || variant === "full") {
220540
220967
  const sdkResources = await ensureSdkResourcesBundle(sdkVersion);
220541
- cpSync3(sdkResources, join20(resourcesDir, "LeCodesSDKResources.bundle"), { recursive: true });
220542
- for (const f3 of readdirSync8(sdkResources))
220968
+ cpSync4(sdkResources, join21(resourcesDir, "LeCodesSDKResources.bundle"), { recursive: true });
220969
+ for (const f3 of readdirSync9(sdkResources))
220543
220970
  if (f3 !== "Info.plist")
220544
220971
  sdkProvided.add(f3);
220545
220972
  log(`${c.green("resources")} LeCodesSDKResources.bundle (filament) → ios/App/Resources/`);
@@ -220566,7 +220993,7 @@ var syncIos = async (root, config) => {
220566
220993
  }
220567
220994
  if (candidates.length > 1)
220568
220995
  warn(`asset name collision: ${name} (${candidates.map((r2) => r2.path).join(", ")}) — embedding ${candidates[0].path}`);
220569
- cpSync3(candidates[0].absPath, join20(resourcesDir, name));
220996
+ cpSync4(candidates[0].absPath, join21(resourcesDir, name));
220570
220997
  embedded++;
220571
220998
  }
220572
220999
  if (embedded > 0)
@@ -220586,14 +221013,14 @@ var syncIos = async (root, config) => {
220586
221013
  }
220587
221014
  if (shellUuid)
220588
221015
  log(`${c.green("identity")} project ${shellUuid} baked into the shell (world identity)`);
220589
- writeFileSync15(join20(iosDir, "README.md"), IOS_README);
220590
- writeFileSync15(join20(iosDir, "LeCodesRuntime", "Package.swift"), runtimePackageTemplate(sdkVersion, variant, runtimePlugins));
220591
- writeFileSync15(join20(sourcesDir, "LeCodesRuntime", "LeCodesRuntime.swift"), runtimeSwiftTemplate(runtimePlugins, shellUuid));
221016
+ writeFileSync16(join21(iosDir, "README.md"), IOS_README);
221017
+ writeFileSync16(join21(iosDir, "LeCodesRuntime", "Package.swift"), runtimePackageTemplate(sdkVersion, variant, runtimePlugins));
221018
+ writeFileSync16(join21(sourcesDir, "LeCodesRuntime", "LeCodesRuntime.swift"), runtimeSwiftTemplate(runtimePlugins, shellUuid));
220592
221019
  const mergedKeys = {};
220593
221020
  for (const [key, values] of Object.entries(infoValues))
220594
221021
  mergedKeys[key] = [...new Set(values)].join(" ");
220595
221022
  const updateUrl = resolveUpdateUrl(config.update, root);
220596
- writeFileSync15(join20(iosDir, "App", "Info.plist"), infoPlistTemplate(mergedKeys, updateUrl));
221023
+ writeFileSync16(join21(iosDir, "App", "Info.plist"), infoPlistTemplate(mergedKeys, updateUrl));
220597
221024
  if (updateUrl)
220598
221025
  log(`${c.green("update")} OTA source ${updateUrl} — checked natively at launch, new code applies next launch`);
220599
221026
  else if (!config.update) {
@@ -220604,10 +221031,10 @@ var syncIos = async (root, config) => {
220604
221031
  if (manifest)
220605
221032
  note(`tip: add "update": true to app.json for over-the-air JS updates — ship with \`lecodes push --publish\`, no store release`);
220606
221033
  }
220607
- if (pluginSpecs.length > 0 || existsSync19(lockPath))
221034
+ if (pluginSpecs.length > 0 || existsSync20(lockPath))
220608
221035
  writeJson(lockPath, { plugins: newLock });
220609
- const pbxPath = join20(iosDir, "App.xcodeproj", "project.pbxproj");
220610
- let pbx = readFileSync16(pbxPath, "utf8").replace(/MARKETING_VERSION = [^;]+;/g, `MARKETING_VERSION = ${config.version ?? "1.0.0"};`).replace(/CURRENT_PROJECT_VERSION = [^;]+;/g, `CURRENT_PROJECT_VERSION = ${config.ios.buildNumber ?? 1};`);
221036
+ const pbxPath = join21(iosDir, "App.xcodeproj", "project.pbxproj");
221037
+ let pbx = readFileSync17(pbxPath, "utf8").replace(/MARKETING_VERSION = [^;]+;/g, `MARKETING_VERSION = ${config.version ?? "1.0.0"};`).replace(/CURRENT_PROJECT_VERSION = [^;]+;/g, `CURRENT_PROJECT_VERSION = ${config.ios.buildNumber ?? 1};`);
220611
221038
  const mergedEntitlements = {};
220612
221039
  for (const [key, values] of Object.entries(entitlementValues)) {
220613
221040
  if (values.every((v) => Array.isArray(v))) {
@@ -220628,7 +221055,7 @@ var syncIos = async (root, config) => {
220628
221055
  }
220629
221056
  const hasEntitlements = Object.keys(mergedEntitlements).length > 0;
220630
221057
  if (pbx.includes("CODE_SIGN_ENTITLEMENTS") || hasEntitlements) {
220631
- writeFileSync15(join20(iosDir, "App", "App.entitlements"), entitlementsTemplate(mergedEntitlements));
221058
+ writeFileSync16(join21(iosDir, "App", "App.entitlements"), entitlementsTemplate(mergedEntitlements));
220632
221059
  if (hasEntitlements)
220633
221060
  log(`${c.green("caps")} App/App.entitlements: ${Object.keys(mergedEntitlements).sort().join(", ")}`);
220634
221061
  if (!pbx.includes("CODE_SIGN_ENTITLEMENTS")) {
@@ -220644,7 +221071,7 @@ $2Info.plist,`);
220644
221071
  }
220645
221072
  }
220646
221073
  }
220647
- writeFileSync15(pbxPath, pbx);
221074
+ writeFileSync16(pbxPath, pbx);
220648
221075
  if (config.icon) {
220649
221076
  await writeIosAppIcon(iosDir, await loadIconMaster(root, config));
220650
221077
  }
@@ -220656,7 +221083,7 @@ $2Info.plist,`);
220656
221083
  var appPlugin = async (args) => {
220657
221084
  const sub = args._[1];
220658
221085
  const root = projectRoot();
220659
- const configPath = join20(root, "app.json");
221086
+ const configPath = join21(root, "app.json");
220660
221087
  const config = readJson(configPath);
220661
221088
  if (!config)
220662
221089
  throw new CliError("No app.json here — run `lecodes app init <ios|android>` first.");
@@ -220667,7 +221094,7 @@ var appPlugin = async (args) => {
220667
221094
  log("No plugins installed.");
220668
221095
  return;
220669
221096
  }
220670
- const lock = readJson(join20(root, "plugins.lock.json"))?.plugins ?? {};
221097
+ const lock = readJson(join21(root, "plugins.lock.json"))?.plugins ?? {};
220671
221098
  for (const [id2, spec] of entries) {
220672
221099
  const pin = lock[id2] ? c.dim(` ${lock[id2].repo} ${lock[id2].tag} (${lock[id2].commit.slice(0, 7)})`) : c.dim(" (not synced)");
220673
221100
  log(`${c.bold(id2)} ${spec}${pin}`);
@@ -220720,14 +221147,14 @@ var exportOptionsPlist = (method, teamId, destination) => `<?xml version="1.0" e
220720
221147
  `;
220721
221148
  var requireShell = () => {
220722
221149
  const root = projectRoot();
220723
- const config = readJson(join20(root, "app.json"));
220724
- if (!config?.ios || !existsSync19(join20(root, "ios"))) {
221150
+ const config = readJson(join21(root, "app.json"));
221151
+ if (!config?.ios || !existsSync20(join21(root, "ios"))) {
220725
221152
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
220726
221153
  }
220727
221154
  return { root, config };
220728
221155
  };
220729
221156
  var updateSdkPin = async (root, config, args, checkOnly, platform) => {
220730
- const [label, repo, minVersion, shellVersion] = platform === "ios" ? ["lecodes-ios-sdk", SDK_REPO, SDK_MIN_VERSION, () => shellSdkVersion(join20(root, "ios"))] : ["lecodes-android-sdk", ANDROID_SDK_REPO, ANDROID_SDK_MIN_VERSION, () => shellSdkVersionAndroid(join20(root, "android"))];
221157
+ const [label, repo, minVersion, shellVersion] = platform === "ios" ? ["lecodes-ios-sdk", SDK_REPO, SDK_MIN_VERSION, () => shellSdkVersion(join21(root, "ios"))] : ["lecodes-android-sdk", ANDROID_SDK_REPO, ANDROID_SDK_MIN_VERSION, () => shellSdkVersionAndroid(join21(root, "android"))];
220731
221158
  const section = config[platform];
220732
221159
  const wanted = flagStr(args, "sdk");
220733
221160
  let changed = false;
@@ -220758,19 +221185,19 @@ var updateSdkPin = async (root, config, args, checkOnly, platform) => {
220758
221185
  }
220759
221186
  if (!checkOnly && nextPin !== pin) {
220760
221187
  section.sdk = nextPin;
220761
- writeJson(join20(root, "app.json"), config);
221188
+ writeJson(join21(root, "app.json"), config);
220762
221189
  }
220763
221190
  return changed;
220764
221191
  };
220765
221192
  var appUpdate = async (args) => {
220766
221193
  const root = projectRoot();
220767
- const config = readJson(join20(root, "app.json"));
221194
+ const config = readJson(join21(root, "app.json"));
220768
221195
  const platform = args._[1];
220769
221196
  if (platform !== undefined && platform !== "ios" && platform !== "android") {
220770
221197
  throw new CliError(`Unknown platform "${platform}" — use ios or android.`);
220771
221198
  }
220772
- const hasIos = !!config?.ios && existsSync19(join20(root, "ios"));
220773
- const hasAndroid = !!config?.android && existsSync19(join20(root, "android"));
221199
+ const hasIos = !!config?.ios && existsSync20(join21(root, "ios"));
221200
+ const hasAndroid = !!config?.android && existsSync20(join21(root, "android"));
220774
221201
  if (platform === "ios" && !hasIos)
220775
221202
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
220776
221203
  if (platform === "android" && !hasAndroid)
@@ -220788,7 +221215,7 @@ var appUpdate = async (args) => {
220788
221215
  changed = await updateSdkPin(root, config, args, checkOnly, "ios") || changed;
220789
221216
  if (doAndroid)
220790
221217
  changed = await updateSdkPin(root, config, args, checkOnly, "android") || changed;
220791
- const lockPath = join20(root, "plugins.lock.json");
221218
+ const lockPath = join21(root, "plugins.lock.json");
220792
221219
  const lock = readJson(lockPath)?.plugins ?? {};
220793
221220
  const specs = Object.entries(config.plugins ?? {});
220794
221221
  if (specs.length > 0 && !flagBool(args, "no-plugins")) {
@@ -220817,7 +221244,7 @@ var appUpdate = async (args) => {
220817
221244
  log(`${c.green("plugin")} ${id} ${entry?.tag ?? "—"} → ${c.bold(tag)}`);
220818
221245
  changed = true;
220819
221246
  }
220820
- if (!checkOnly && existsSync19(lockPath))
221247
+ if (!checkOnly && existsSync20(lockPath))
220821
221248
  writeJson(lockPath, { plugins: kept });
220822
221249
  }
220823
221250
  log("");
@@ -220837,9 +221264,9 @@ var appUpdate = async (args) => {
220837
221264
  };
220838
221265
  var appDoctor = async (_args) => {
220839
221266
  const root = projectRoot();
220840
- const config = readJson(join20(root, "app.json"));
220841
- const hasIos = !!config?.ios && existsSync19(join20(root, "ios"));
220842
- const hasAndroid = !!config?.android && existsSync19(join20(root, "android"));
221267
+ const config = readJson(join21(root, "app.json"));
221268
+ const hasIos = !!config?.ios && existsSync20(join21(root, "ios"));
221269
+ const hasAndroid = !!config?.android && existsSync20(join21(root, "android"));
220843
221270
  if (!hasIos && !hasAndroid) {
220844
221271
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
220845
221272
  }
@@ -220850,14 +221277,14 @@ var appDoctor = async (_args) => {
220850
221277
  failed++;
220851
221278
  };
220852
221279
  if (config?.icon) {
220853
- check(existsSync19(join20(root, config.icon)), `Icon master ${config.icon}`, `app.json "icon" points at a missing file — sync will fail`);
221280
+ check(existsSync20(join21(root, config.icon)), `Icon master ${config.icon}`, `app.json "icon" points at a missing file — sync will fail`);
220854
221281
  }
220855
221282
  if (hasIos)
220856
221283
  await doctorIosChecks(root, config, check);
220857
221284
  if (hasAndroid) {
220858
221285
  await doctorAndroidChecks(root, config, check);
220859
221286
  if (!hasIos) {
220860
- const lock = readJson(join20(root, "plugins.lock.json"))?.plugins ?? {};
221287
+ const lock = readJson(join21(root, "plugins.lock.json"))?.plugins ?? {};
220861
221288
  const wanted = Object.keys(config.plugins ?? {});
220862
221289
  const inSync = wanted.every((id) => lock[id]) && Object.keys(lock).every((id) => wanted.includes(id));
220863
221290
  check(wanted.length === 0 || inSync, "Plugins synced", "run `lecodes app sync`");
@@ -220876,22 +221303,22 @@ var doctorIosChecks = async (root, config, check) => {
220876
221303
  } catch {}
220877
221304
  check(xcodeVersion !== "", `Xcode CLI (${xcodeVersion || "not found"})`, "install Xcode + command line tools");
220878
221305
  check(!!config.ios.teamId, "Development team set", "lecodes app init wrote app.json — fill ios.teamId", false);
220879
- const iconDir = join20(root, "ios", "App", "Assets.xcassets", "AppIcon.appiconset");
220880
- const hasIcon = existsSync19(iconDir) && readdirSync8(iconDir).some((f3) => f3.endsWith(".png"));
221306
+ const iconDir = join21(root, "ios", "App", "Assets.xcassets", "AppIcon.appiconset");
221307
+ const hasIcon = existsSync20(iconDir) && readdirSync9(iconDir).some((f3) => f3.endsWith(".png"));
220881
221308
  check(hasIcon, "App icon present", "drop a 1024×1024 PNG into ios/App/Assets.xcassets/AppIcon.appiconset/ (App Store requires it)", false);
220882
- const bundle = join20(root, "ios", "App", "Resources", "app.js");
220883
- check(existsSync19(bundle), "Bundled app.js", "run `lecodes app sync`");
221309
+ const bundle = join21(root, "ios", "App", "Resources", "app.js");
221310
+ check(existsSync20(bundle), "Bundled app.js", "run `lecodes app sync`");
220884
221311
  const requestedVariant = config.ios.variant ?? "auto";
220885
221312
  const validVariant = requestedVariant === "auto" || SDK_VARIANTS.includes(requestedVariant);
220886
221313
  check(validVariant, `ios.variant "${requestedVariant}"`, `use "auto" or one of ${SDK_VARIANTS.join(", ")}`);
220887
- if (existsSync19(bundle) && validVariant) {
220888
- const { variant } = resolveVariant(requestedVariant, readFileSync16(bundle, "utf8"));
220889
- const pkg = readFileSync16(join20(root, "ios", "LeCodesRuntime", "Package.swift"), "utf8");
221314
+ if (existsSync20(bundle) && validVariant) {
221315
+ const { variant } = resolveVariant(requestedVariant, readFileSync17(bundle, "utf8"));
221316
+ const pkg = readFileSync17(join21(root, "ios", "LeCodesRuntime", "Package.swift"), "utf8");
220890
221317
  const needsResources = variant === "3d" || variant === "full";
220891
- const hasResources = existsSync19(join20(root, "ios", "App", "Resources", "LeCodesSDKResources.bundle"));
221318
+ const hasResources = existsSync20(join21(root, "ios", "App", "Resources", "LeCodesSDKResources.bundle"));
220892
221319
  check(pkg.includes(`"LeCodesSDK-${variant}"`) && (!needsResources || hasResources), `SDK variant ${variant}${needsResources ? " (+ filament resources)" : ""}`, "run `lecodes app sync`");
220893
221320
  }
220894
- const linked = shellSdkVersion(join20(root, "ios"));
221321
+ const linked = shellSdkVersion(join21(root, "ios"));
220895
221322
  const pin = config.ios.sdk ?? "latest";
220896
221323
  check(!!linked, `SDK ${linked ?? "version unreadable"}`, "run `lecodes app sync`");
220897
221324
  if (linked && pin !== "latest") {
@@ -220903,27 +221330,27 @@ var doctorIosChecks = async (root, config, check) => {
220903
221330
  check(cmpVersion(linked, latest) >= 0, `SDK is the latest release (${latest})`, "run `lecodes app update`", false);
220904
221331
  } catch {}
220905
221332
  }
220906
- const lock = readJson(join20(root, "plugins.lock.json"))?.plugins ?? {};
221333
+ const lock = readJson(join21(root, "plugins.lock.json"))?.plugins ?? {};
220907
221334
  const wanted = Object.keys(config.plugins ?? {});
220908
221335
  const inSync = wanted.every((id) => lock[id]) && Object.keys(lock).every((id) => wanted.includes(id));
220909
221336
  check(wanted.length === 0 || inSync, "Plugins synced", "run `lecodes app sync`");
220910
- const pbxFile = join20(root, "ios", "App.xcodeproj", "project.pbxproj");
220911
- if (existsSync19(pbxFile) && readFileSync16(pbxFile, "utf8").includes("CODE_SIGN_ENTITLEMENTS")) {
220912
- check(existsSync19(join20(root, "ios", "App", "App.entitlements")), "App/App.entitlements present", "run `lecodes app sync`");
221337
+ const pbxFile = join21(root, "ios", "App.xcodeproj", "project.pbxproj");
221338
+ if (existsSync20(pbxFile) && readFileSync17(pbxFile, "utf8").includes("CODE_SIGN_ENTITLEMENTS")) {
221339
+ check(existsSync20(join21(root, "ios", "App", "App.entitlements")), "App/App.entitlements present", "run `lecodes app sync`");
220913
221340
  }
220914
221341
  if (wanted.includes("push")) {
220915
- const runtimeSwift = join20(root, "ios", "LeCodesRuntime", "Sources", "LeCodesRuntime", "LeCodesRuntime.swift");
220916
- const baked = existsSync19(runtimeSwift) && readFileSync16(runtimeSwift, "utf8").includes("bootProjectUuid");
221342
+ const runtimeSwift = join21(root, "ios", "LeCodesRuntime", "Sources", "LeCodesRuntime", "LeCodesRuntime.swift");
221343
+ const baked = existsSync20(runtimeSwift) && readFileSync17(runtimeSwift, "utf8").includes("bootProjectUuid");
220917
221344
  check(baked, "Push world identity baked", `without it Push.register() rejects "unavailable" — needs a pushed project (uuid), lecodes-ios-sdk ${SDK_IDENTITY_VERSION}+ and \`lecodes app sync\``, false);
220918
221345
  }
220919
221346
  if (config.update) {
220920
- const plist = readFileSync16(join20(root, "ios", "App", "Info.plist"), "utf8");
221347
+ const plist = readFileSync17(join21(root, "ios", "App", "Info.plist"), "utf8");
220921
221348
  check(plist.includes("<key>LeCodesUpdateURL</key>"), "OTA update URL in Info.plist", "run `lecodes app sync`", false);
220922
- const appDir = join20(root, "ios", "App");
220923
- const callsUpdater = readdirSync8(appDir).some((f3) => f3.endsWith(".swift") && readFileSync16(join20(appDir, f3), "utf8").includes("LeCodesUpdater.activeBundle"));
221349
+ const appDir = join21(root, "ios", "App");
221350
+ const callsUpdater = readdirSync9(appDir).some((f3) => f3.endsWith(".swift") && readFileSync17(join21(appDir, f3), "utf8").includes("LeCodesUpdater.activeBundle"));
220924
221351
  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);
220925
221352
  }
220926
- const asc = readJson(join20(homedir7(), ".lecodes", "asc.json"));
221353
+ const asc = readJson(join21(homedir7(), ".lecodes", "asc.json"));
220927
221354
  check(!!asc?.keyPath, "App Store Connect API key configured", "pass --key-path/--key-id/--issuer-id to `lecodes app upload` once to store it", false);
220928
221355
  };
220929
221356
  var ARCHIVE_PATH = "ios/build/App.xcarchive";
@@ -220959,8 +221386,8 @@ var appBuild = async (args) => {
220959
221386
  success(`Archive: ios/build/App.xcarchive`);
220960
221387
  if (flagBool(args, "no-export"))
220961
221388
  return;
220962
- const optionsPath = join20(root, "ios", "build", "ExportOptions.plist");
220963
- writeFileSync15(optionsPath, exportOptionsPlist(method, config.ios.teamId ?? ""));
221389
+ const optionsPath = join21(root, "ios", "build", "ExportOptions.plist");
221390
+ writeFileSync16(optionsPath, exportOptionsPlist(method, config.ios.teamId ?? ""));
220964
221391
  note(`Exporting (.ipa, method ${method})…`);
220965
221392
  runTool(root, "xcodebuild", [
220966
221393
  "-exportArchive",
@@ -220979,10 +221406,10 @@ var appUpload = async (args) => {
220979
221406
  throw new CliError("Play upload isn't wired yet — upload android/app/build/outputs/bundle/release/*.aab in the Play Console (`lecodes app build android` produces it).");
220980
221407
  }
220981
221408
  const { root, config } = requireShell();
220982
- if (!existsSync19(join20(root, ARCHIVE_PATH))) {
221409
+ if (!existsSync20(join21(root, ARCHIVE_PATH))) {
220983
221410
  throw new CliError("No archive at ios/build/App.xcarchive — run `lecodes app build ios` first.");
220984
221411
  }
220985
- const ascPath = join20(homedir7(), ".lecodes", "asc.json");
221412
+ const ascPath = join21(homedir7(), ".lecodes", "asc.json");
220986
221413
  const stored = readJson(ascPath) ?? {};
220987
221414
  const keyPath = flagStr(args, "key-path") ?? stored.keyPath;
220988
221415
  const keyId = flagStr(args, "key-id") ?? stored.keyId;
@@ -220991,15 +221418,15 @@ var appUpload = async (args) => {
220991
221418
  throw new CliError(`App Store Connect API key needed: --key-path <AuthKey.p8> --key-id <id> --issuer-id <id>
220992
221419
  (create one at appstoreconnect.apple.com → Users and Access → Integrations; stored in ~/.lecodes/asc.json after first use)`);
220993
221420
  }
220994
- if (!existsSync19(keyPath))
221421
+ if (!existsSync20(keyPath))
220995
221422
  throw new CliError(`API key file not found: ${keyPath}`);
220996
221423
  if (flagStr(args, "key-path")) {
220997
- mkdirSync13(dirname12(ascPath), { recursive: true });
221424
+ mkdirSync14(dirname12(ascPath), { recursive: true });
220998
221425
  writeJson(ascPath, { keyPath, keyId, issuerId });
220999
221426
  note("Stored the key reference in ~/.lecodes/asc.json");
221000
221427
  }
221001
- const optionsPath = join20(root, "ios", "build", "UploadOptions.plist");
221002
- writeFileSync15(optionsPath, exportOptionsPlist("app-store-connect", config.ios.teamId ?? "", "upload"));
221428
+ const optionsPath = join21(root, "ios", "build", "UploadOptions.plist");
221429
+ writeFileSync16(optionsPath, exportOptionsPlist("app-store-connect", config.ios.teamId ?? "", "upload"));
221003
221430
  note("Uploading to App Store Connect… (processing continues on Apple's side)");
221004
221431
  runTool(root, "xcodebuild", [
221005
221432
  "-exportArchive",
@@ -221010,14 +221437,14 @@ var appUpload = async (args) => {
221010
221437
  "-exportPath",
221011
221438
  "ios/build/upload",
221012
221439
  "-authenticationKeyPath",
221013
- resolve7(keyPath),
221440
+ resolve8(keyPath),
221014
221441
  "-authenticationKeyID",
221015
221442
  keyId,
221016
221443
  "-authenticationKeyIssuerID",
221017
221444
  issuerId,
221018
221445
  "-allowProvisioningUpdates"
221019
221446
  ]);
221020
- const configPath = join20(root, "app.json");
221447
+ const configPath = join21(root, "app.json");
221021
221448
  const fresh = readJson(configPath);
221022
221449
  fresh.ios.buildNumber = (fresh.ios.buildNumber ?? 1) + 1;
221023
221450
  writeJson(configPath, fresh);
@@ -221028,19 +221455,19 @@ var appOpen = async (args) => {
221028
221455
  const root = projectRoot();
221029
221456
  let platform = args._[1];
221030
221457
  if (!platform) {
221031
- const hasIos = existsSync19(join20(root, "ios"));
221032
- const hasAndroid = existsSync19(join20(root, "android"));
221458
+ const hasIos = existsSync20(join21(root, "ios"));
221459
+ const hasAndroid = existsSync20(join21(root, "android"));
221033
221460
  if (hasIos && hasAndroid)
221034
221461
  throw new CliError("Both shells exist — `lecodes app open ios` or `lecodes app open android`.");
221035
221462
  platform = hasAndroid ? "android" : "ios";
221036
221463
  }
221037
221464
  if (platform === "ios") {
221038
- const proj = join20(root, "ios", "App.xcodeproj");
221039
- if (!existsSync19(proj))
221465
+ const proj = join21(root, "ios", "App.xcodeproj");
221466
+ if (!existsSync20(proj))
221040
221467
  throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
221041
221468
  if (process.platform !== "darwin")
221042
221469
  throw new CliError("Opening Xcode requires macOS.");
221043
- execFileSync4("open", [proj], { stdio: "ignore" });
221470
+ execFileSync6("open", [proj], { stdio: "ignore" });
221044
221471
  success("Opened ios/App.xcodeproj in Xcode.");
221045
221472
  return;
221046
221473
  }
@@ -221073,8 +221500,8 @@ var app = async (args) => {
221073
221500
  init_api();
221074
221501
  init_config();
221075
221502
  init_manifest();
221076
- import { existsSync as existsSync21, mkdirSync as mkdirSync15, readdirSync as readdirSync10 } from "node:fs";
221077
- import { resolve as resolve8 } from "node:path";
221503
+ import { existsSync as existsSync22, mkdirSync as mkdirSync16, readdirSync as readdirSync11 } from "node:fs";
221504
+ import { resolve as resolve9 } from "node:path";
221078
221505
  init_types4();
221079
221506
  init_ignore();
221080
221507
  init_util();
@@ -221095,11 +221522,11 @@ var clone3 = async (args) => {
221095
221522
  const uuid = parseUuid(args._[0]);
221096
221523
  const project = await getProject(apiUrl, token, uuid);
221097
221524
  const dir = args._[1] ?? slug(project.name);
221098
- const root = resolve8(dir);
221099
- if (existsSync21(root) && readdirSync10(root).length > 0) {
221525
+ const root = resolve9(dir);
221526
+ if (existsSync22(root) && readdirSync11(root).length > 0) {
221100
221527
  throw new CliError(`Directory "${dir}" already exists and is not empty.`);
221101
221528
  }
221102
- mkdirSync15(root, { recursive: true });
221529
+ mkdirSync16(root, { recursive: true });
221103
221530
  info(c.dim(`Fetching project ${c.bold(project.name)}…`));
221104
221531
  const files = await materializeAssets(root, project, apiUrl, token, progress);
221105
221532
  endProgress();
@@ -221122,8 +221549,8 @@ init_config();
221122
221549
  init_manifest();
221123
221550
  init_types4();
221124
221551
  init_ignore();
221125
- import { existsSync as existsSync22, mkdirSync as mkdirSync16, readdirSync as readdirSync11, writeFileSync as writeFileSync17 } from "node:fs";
221126
- import { join as join22, resolve as resolve9 } from "node:path";
221552
+ import { existsSync as existsSync23, mkdirSync as mkdirSync17, readdirSync as readdirSync12, writeFileSync as writeFileSync18 } from "node:fs";
221553
+ import { join as join23, resolve as resolve10 } from "node:path";
221127
221554
 
221128
221555
  // src/commands/projectTemplates.ts
221129
221556
  var appClaudeMd = (name) => `# ${name} — LeCodes app
@@ -221263,21 +221690,21 @@ var create = async (args) => {
221263
221690
  if (!name)
221264
221691
  throw new CliError("Usage: lecodes create <name> [directory]");
221265
221692
  const dir = args._[1] ?? slug2(name);
221266
- const root = resolve9(dir);
221267
- if (existsSync22(root) && readdirSync11(root).length > 0) {
221693
+ const root = resolve10(dir);
221694
+ if (existsSync23(root) && readdirSync12(root).length > 0) {
221268
221695
  throw new CliError(`Directory "${dir}" already exists and is not empty.`);
221269
221696
  }
221270
- mkdirSync16(root, { recursive: true });
221697
+ mkdirSync17(root, { recursive: true });
221271
221698
  info(c.dim(`Creating project ${c.bold(name)}…`));
221272
221699
  const { uuid } = await createProject(apiUrl, token, name, flagStr(args, "team"));
221273
221700
  info(c.dim("Fetching type definitions…"));
221274
221701
  await materializeTypes(root, apiUrl);
221275
221702
  writeTsconfig(root);
221276
221703
  writeDefaultIgnore(root);
221277
- writeFileSync17(join22(root, "main.ts"), STARTER);
221278
- writeFileSync17(join22(root, "CLAUDE.md"), appClaudeMd(name));
221279
- mkdirSync16(join22(root, ".claude"), { recursive: true });
221280
- writeFileSync17(join22(root, ".claude", "settings.json"), CLAUDE_SETTINGS);
221704
+ writeFileSync18(join23(root, "main.ts"), STARTER);
221705
+ writeFileSync18(join23(root, "CLAUDE.md"), appClaudeMd(name));
221706
+ mkdirSync17(join23(root, ".claude"), { recursive: true });
221707
+ writeFileSync18(join23(root, ".claude", "settings.json"), CLAUDE_SETTINGS);
221281
221708
  const commits = await getCommits(apiUrl, token, uuid);
221282
221709
  writeManifest(root, { uuid, apiUrl, name, baseHeadId: commits.headId, files: {} });
221283
221710
  success(`Created ${c.bold(name)} in ${c.bold(dir)}/`);
@@ -221344,6 +221771,10 @@ var status = async () => {
221344
221771
  log(`downloaded ${installed.length > 0 ? installed.join(", ") : c.dim("none")} ${c.dim("(~/.lecodes/renderer)")}`);
221345
221772
  if (process.env.LECODES_DESKTOP_EXE)
221346
221773
  log(`override LECODES_DESKTOP_EXE=${process.env.LECODES_DESKTOP_EXE}`);
221774
+ if (process.platform === "darwin") {
221775
+ const installed2 = findMacHostExe();
221776
+ log(`installed ${installed2 ?? c.dim("none")} ${c.dim("(offline fallback only)")}`);
221777
+ }
221347
221778
  try {
221348
221779
  const { latest } = await fetchDesktopManifest();
221349
221780
  const hint = semverNewer(selected, latest) > 0 ? c.yellow("(newer — run `lecodes desktop update`)") : latest === selected ? c.dim("(up to date)") : c.dim("(older than the CLI's minimum — this desktop release isn't published yet)");
@@ -221371,16 +221802,16 @@ init_design();
221371
221802
  // src/commands/dev.ts
221372
221803
  init_manifest();
221373
221804
  init_ignore();
221374
- import { existsSync as existsSync24, mkdirSync as mkdirSync18, readFileSync as readFileSync21, watch, writeFileSync as writeFileSync19 } from "node:fs";
221805
+ import { existsSync as existsSync25, mkdirSync as mkdirSync19, readFileSync as readFileSync22, watch, writeFileSync as writeFileSync20 } from "node:fs";
221375
221806
  import { randomBytes as randomBytes3 } from "node:crypto";
221376
221807
  import { networkInterfaces } from "node:os";
221377
- import { basename as basename10, join as join26 } from "node:path";
221808
+ import { basename as basename11, join as join27 } from "node:path";
221378
221809
 
221379
221810
  // src/dev/devServer.ts
221380
221811
  init_localFiles();
221381
221812
  import { createServer as createServer2 } from "node:http";
221382
- import { createReadStream, statSync as statSync8 } from "node:fs";
221383
- import { join as join25 } from "node:path";
221813
+ import { createReadStream, statSync as statSync9 } from "node:fs";
221814
+ import { join as join26 } from "node:path";
221384
221815
  init_projectCompile();
221385
221816
  init_fonts();
221386
221817
  init_assetIcons();
@@ -221654,14 +222085,14 @@ var sendText = (res, status2, type, body) => {
221654
222085
  res.end(body);
221655
222086
  };
221656
222087
  var sendJson = (res, status2, body) => sendText(res, status2, "application/json", JSON.stringify(body));
221657
- var readBody = (req) => new Promise((resolve10) => {
222088
+ var readBody = (req) => new Promise((resolve11) => {
221658
222089
  let body = "";
221659
222090
  req.on("data", (c4) => {
221660
222091
  if (body.length < 256 * 1024)
221661
222092
  body += c4.toString("utf8");
221662
222093
  });
221663
- req.on("end", () => resolve10(body));
221664
- req.on("error", () => resolve10(body));
222094
+ req.on("end", () => resolve11(body));
222095
+ req.on("error", () => resolve11(body));
221665
222096
  });
221666
222097
  var startDevServer = async (options) => {
221667
222098
  const { root, manifest, name, origin, token, entryOverride } = options;
@@ -221766,9 +222197,9 @@ var startDevServer = async (options) => {
221766
222197
  const name2 = route.slice("/shaders/".length);
221767
222198
  if (!isShaderArtifactName(name2))
221768
222199
  return sendText(res, 400, "text/plain", "Bad path");
221769
- const abs = join25(shadersCacheDir(root), name2);
222200
+ const abs = join26(shadersCacheDir(root), name2);
221770
222201
  try {
221771
- if (!statSync8(abs).isFile())
222202
+ if (!statSync9(abs).isFile())
221772
222203
  throw new Error("not a file");
221773
222204
  } catch {
221774
222205
  return sendText(res, 404, "text/plain", "Not found");
@@ -221785,7 +222216,7 @@ var startDevServer = async (options) => {
221785
222216
  return sendText(res, 400, "text/plain", "Bad path");
221786
222217
  }
221787
222218
  const abs = resolveLocalPath(root, key);
221788
- if (!abs || !statSync8(abs).isFile())
222219
+ if (!abs || !statSync9(abs).isFile())
221789
222220
  return sendText(res, 404, "text/plain", "Not found");
221790
222221
  res.writeHead(200, { "content-type": contentType(key), "cache-control": "no-store" });
221791
222222
  createReadStream(abs).pipe(res);
@@ -221828,11 +222259,11 @@ var startDevServer = async (options) => {
221828
222259
  if (conn)
221829
222260
  clients.add(conn);
221830
222261
  });
221831
- await new Promise((resolve10, reject) => {
222262
+ await new Promise((resolve11, reject) => {
221832
222263
  server2.once("error", reject);
221833
222264
  server2.listen(options.port, options.bind ?? "0.0.0.0", () => {
221834
222265
  server2.removeListener("error", reject);
221835
- resolve10();
222266
+ resolve11();
221836
222267
  });
221837
222268
  });
221838
222269
  const address = server2.address();
@@ -221880,24 +222311,24 @@ var lanCandidates = () => {
221880
222311
  return out.sort((x2, y2) => x2.rank - y2.rank).map(({ address, iface }) => ({ address, iface }));
221881
222312
  };
221882
222313
  var loadDevToken = (root) => {
221883
- const path2 = join26(root, LECODES_DIR, "dev.json");
222314
+ const path2 = join27(root, LECODES_DIR, "dev.json");
221884
222315
  try {
221885
- const token2 = JSON.parse(readFileSync21(path2, "utf8")).token;
222316
+ const token2 = JSON.parse(readFileSync22(path2, "utf8")).token;
221886
222317
  if (token2 && /^[a-z0-9]{8,}$/.test(token2))
221887
222318
  return token2;
221888
222319
  } catch {}
221889
222320
  const token = randomBytes3(6).toString("hex");
221890
- mkdirSync18(join26(root, LECODES_DIR), { recursive: true });
221891
- writeFileSync19(path2, JSON.stringify({ token }, null, 2) + `
222321
+ mkdirSync19(join27(root, LECODES_DIR), { recursive: true });
222322
+ writeFileSync20(path2, JSON.stringify({ token }, null, 2) + `
221892
222323
  `);
221893
222324
  return token;
221894
222325
  };
221895
222326
  var printQr = async (url) => {
221896
222327
  try {
221897
222328
  const qrcode = (await Promise.resolve().then(() => __toESM(require_main(), 1))).default;
221898
- await new Promise((resolve10) => qrcode.generate(url, { small: true }, (out) => {
222329
+ await new Promise((resolve11) => qrcode.generate(url, { small: true }, (out) => {
221899
222330
  log(out);
221900
- resolve10();
222331
+ resolve11();
221901
222332
  }));
221902
222333
  } catch {
221903
222334
  note("(couldn't render a QR code — open the URL below on the device)");
@@ -221913,7 +222344,7 @@ var dev = async (args) => {
221913
222344
  root = process.cwd();
221914
222345
  manifest = null;
221915
222346
  }
221916
- const name = manifest?.name ?? basename10(root);
222347
+ const name = manifest?.name ?? basename11(root);
221917
222348
  if (typeof globalThis.Bun !== "undefined") {
221918
222349
  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).");
221919
222350
  }
@@ -222005,7 +222436,7 @@ ${result.error}`);
222005
222436
  if (alternatives.length > 0) {
222006
222437
  note(`Device can't reach it? This machine also has ${alternatives.map((a2) => `${a2.address} (${a2.iface})`).join(", ")} — retry with --host <ip>.`);
222007
222438
  }
222008
- if (existsSync24(join26(root, LECODES_DIR, "manifest.json")))
222439
+ if (existsSync25(join27(root, LECODES_DIR, "manifest.json")))
222009
222440
  note(`Token stored in ${LECODES_DIR}/dev.json (delete it to rotate the URL).`);
222010
222441
  log("");
222011
222442
  log(`Watching ${root} — save a file to hot-reload. Ctrl+C to stop.`);
@@ -222034,8 +222465,8 @@ init_localFiles();
222034
222465
 
222035
222466
  // src/serverDiff.ts
222036
222467
  init_localFiles();
222037
- import { readFileSync as readFileSync22 } from "node:fs";
222038
- import { basename as basename11 } from "node:path";
222468
+ import { readFileSync as readFileSync23 } from "node:fs";
222469
+ import { basename as basename12 } from "node:path";
222039
222470
  var isTextAsset = (t2) => t2 === "text" || t2 === "shader" || t2 === "scene";
222040
222471
  var computeServerDiff = (project, scan2) => {
222041
222472
  const pathById = buildPaths(project.assets);
@@ -222052,8 +222483,8 @@ var computeServerDiff = (project, scan2) => {
222052
222483
  const server2 = serverByPath.get(path2);
222053
222484
  const local = localByPath.get(path2);
222054
222485
  if (local && !server2) {
222055
- const text = isTextKind(classifyKind(basename11(path2)));
222056
- entries.push({ path: path2, status: "added", binary: !text, serverText: "", localText: text ? readFileSync22(local.absPath, "utf8") : undefined });
222486
+ const text = isTextKind(classifyKind(basename12(path2)));
222487
+ entries.push({ path: path2, status: "added", binary: !text, serverText: "", localText: text ? readFileSync23(local.absPath, "utf8") : undefined });
222057
222488
  continue;
222058
222489
  }
222059
222490
  if (server2 && !local) {
@@ -222064,12 +222495,12 @@ var computeServerDiff = (project, scan2) => {
222064
222495
  if (server2 && local) {
222065
222496
  if (isTextAsset(server2.type)) {
222066
222497
  const serverText = server2.text ?? "";
222067
- const localText = readFileSync22(local.absPath, "utf8");
222498
+ const localText = readFileSync23(local.absPath, "utf8");
222068
222499
  if (serverText !== localText)
222069
222500
  entries.push({ path: path2, status: "modified", binary: false, serverText, localText });
222070
222501
  } else {
222071
222502
  const serverSize = server2.file?.size;
222072
- const localSize = Buffer.byteLength(readFileSync22(local.absPath));
222503
+ const localSize = Buffer.byteLength(readFileSync23(local.absPath));
222073
222504
  if (serverSize !== undefined && serverSize !== localSize) {
222074
222505
  entries.push({ path: path2, status: "modified", binary: true });
222075
222506
  }
@@ -222227,8 +222658,8 @@ var statusLabel = (e2) => e2.status === "added" ? c.green(" added ") : e2.st
222227
222658
  init_manifest();
222228
222659
  init_types4();
222229
222660
  init_ignore();
222230
- import { existsSync as existsSync25, mkdirSync as mkdirSync19, writeFileSync as writeFileSync20 } from "node:fs";
222231
- import { basename as basename12, dirname as dirname14, join as join27, resolve as resolve10 } from "node:path";
222661
+ import { existsSync as existsSync26, mkdirSync as mkdirSync20, writeFileSync as writeFileSync21 } from "node:fs";
222662
+ import { basename as basename13, dirname as dirname14, join as join28, resolve as resolve11 } from "node:path";
222232
222663
  init_util();
222233
222664
  var STARTER2 = `// Welcome to LeCodes — the whole SDK is available as globals, no imports needed.
222234
222665
  // Run it live on a device with \`lecodes dev\`, or bundle it with \`lecodes compile -o app.js\`.
@@ -222336,18 +222767,18 @@ Full command list: \`lecodes --help\`.
222336
222767
  `;
222337
222768
  var init2 = async (args) => {
222338
222769
  const dirArg = (args._[0] ?? ".").replace(/[/\\]+$/, "") || ".";
222339
- const root = resolve10(dirArg);
222340
- const name = basename12(root);
222341
- mkdirSync19(root, { recursive: true });
222770
+ const root = resolve11(dirArg);
222771
+ const name = basename13(root);
222772
+ mkdirSync20(root, { recursive: true });
222342
222773
  let created = 0;
222343
222774
  const write = (rel, content) => {
222344
- const abs = join27(root, rel);
222345
- if (existsSync25(abs)) {
222775
+ const abs = join28(root, rel);
222776
+ if (existsSync26(abs)) {
222346
222777
  note(`kept ${rel}`);
222347
222778
  return;
222348
222779
  }
222349
- mkdirSync19(dirname14(abs), { recursive: true });
222350
- writeFileSync20(abs, content);
222780
+ mkdirSync20(dirname14(abs), { recursive: true });
222781
+ writeFileSync21(abs, content);
222351
222782
  log(`${c.green("created")} ${rel}`);
222352
222783
  created++;
222353
222784
  };
@@ -222394,8 +222825,8 @@ init_config();
222394
222825
  init_ignore();
222395
222826
  init_localFiles();
222396
222827
  init_manifest();
222397
- import { existsSync as existsSync26 } from "node:fs";
222398
- import { basename as basename14, join as join28, resolve as resolve11 } from "node:path";
222828
+ import { existsSync as existsSync27 } from "node:fs";
222829
+ import { basename as basename15, join as join29, resolve as resolve12 } from "node:path";
222399
222830
  init_types4();
222400
222831
  init_util();
222401
222832
 
@@ -222404,8 +222835,8 @@ init_api();
222404
222835
  init_config();
222405
222836
  init_manifest();
222406
222837
  init_localFiles();
222407
- import { basename as basename13 } from "node:path";
222408
- import { readFileSync as readFileSync23 } from "node:fs";
222838
+ import { basename as basename14 } from "node:path";
222839
+ import { readFileSync as readFileSync24 } from "node:fs";
222409
222840
  init_util();
222410
222841
  var push = async (args) => {
222411
222842
  const root = findProjectRoot(process.cwd());
@@ -222461,14 +222892,14 @@ var push = async (args) => {
222461
222892
  let uploaded = 0;
222462
222893
  for (const file of scan2.files) {
222463
222894
  const source = sourceFor(file.path);
222464
- const kind = source?.type ?? classifyKind(basename13(file.path));
222895
+ const kind = source?.type ?? classifyKind(basename14(file.path));
222465
222896
  const entry = { path: file.path, type: kind };
222466
222897
  if (source)
222467
222898
  entry.assetId = source.id;
222468
222899
  if (isTextKind(kind)) {
222469
- entry.text = readFileSync23(file.absPath, "utf8");
222900
+ entry.text = readFileSync24(file.absPath, "utf8");
222470
222901
  } else if (!source || source.sha !== file.sha) {
222471
- const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename13(file.path), readFileSync23(file.absPath));
222902
+ const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename14(file.path), readFileSync24(file.absPath));
222472
222903
  entry.blobFileId = blob.id;
222473
222904
  uploaded++;
222474
222905
  }
@@ -222575,10 +223006,10 @@ var link = async (args) => {
222575
223006
  const apiUrl = requireApiUrl(config);
222576
223007
  const token = requireToken(config);
222577
223008
  const dirArg = args._[0];
222578
- const root = dirArg ? resolve11(dirArg) : process.cwd();
222579
- if (!existsSync26(root))
223009
+ const root = dirArg ? resolve12(dirArg) : process.cwd();
223010
+ if (!existsSync27(root))
222580
223011
  throw new CliError(`No such directory: ${root}`);
222581
- if (existsSync26(join28(root, LECODES_DIR, "manifest.json")) && !flagBool(args, "force")) {
223012
+ if (existsSync27(join29(root, LECODES_DIR, "manifest.json")) && !flagBool(args, "force")) {
222582
223013
  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.");
222583
223014
  }
222584
223015
  let enclosing = null;
@@ -222605,7 +223036,7 @@ var link = async (args) => {
222605
223036
  }
222606
223037
  name = project.name;
222607
223038
  } else {
222608
- name = flagStr(args, "name") ?? basename14(root);
223039
+ name = flagStr(args, "name") ?? basename15(root);
222609
223040
  info(c.dim(`Creating project ${c.bold(name)}…`));
222610
223041
  uuid = (await createProject(apiUrl, token, name, flagStr(args, "team"))).uuid;
222611
223042
  files = {};
@@ -222642,10 +223073,10 @@ var link = async (args) => {
222642
223073
 
222643
223074
  // src/commands/lightmap.ts
222644
223075
  init_manifest();
222645
- import { existsSync as existsSync27, mkdirSync as mkdirSync20, mkdtempSync as mkdtempSync3, rmSync as rmSync8, statSync as statSync9, unlinkSync, writeFileSync as writeFileSync21 } from "node:fs";
222646
- import { spawnSync as spawnSync4 } from "node:child_process";
222647
- import { tmpdir as tmpdir3 } from "node:os";
222648
- import { join as join29, relative as relative4, resolve as resolve12 } from "node:path";
223076
+ import { existsSync as existsSync28, mkdirSync as mkdirSync21, mkdtempSync as mkdtempSync4, rmSync as rmSync9, statSync as statSync10, unlinkSync, writeFileSync as writeFileSync22 } from "node:fs";
223077
+ import { spawnSync as spawnSync6 } from "node:child_process";
223078
+ import { tmpdir as tmpdir4 } from "node:os";
223079
+ import { join as join30, relative as relative4, resolve as resolve13 } from "node:path";
222649
223080
  init_util();
222650
223081
  var num = (args, key, fallback, min, max) => {
222651
223082
  const raw = flagStr(args, key);
@@ -222658,20 +223089,20 @@ var num = (args, key, fallback, min, max) => {
222658
223089
  };
222659
223090
  var findToktx = () => {
222660
223091
  const env = process.env.KTX_TOKTX;
222661
- if (env && existsSync27(env))
223092
+ if (env && existsSync28(env))
222662
223093
  return env;
222663
- for (const base of [process.env.LOCALAPPDATA && join29(process.env.LOCALAPPDATA, "Programs"), process.env.ProgramFiles]) {
223094
+ for (const base of [process.env.LOCALAPPDATA && join30(process.env.LOCALAPPDATA, "Programs"), process.env.ProgramFiles]) {
222664
223095
  if (!base)
222665
223096
  continue;
222666
- const guess = join29(base, "KTX-Software", "bin", "toktx.exe");
222667
- if (existsSync27(guess))
223097
+ const guess = join30(base, "KTX-Software", "bin", "toktx.exe");
223098
+ if (existsSync28(guess))
222668
223099
  return guess;
222669
223100
  }
222670
223101
  return "toktx";
222671
223102
  };
222672
223103
  var checkToktx = (toktx) => {
222673
223104
  try {
222674
- const p2 = spawnSync4(toktx, ["--version"], { encoding: "utf8" });
223105
+ const p2 = spawnSync6(toktx, ["--version"], { encoding: "utf8" });
222675
223106
  if (p2.status === 0)
222676
223107
  return (p2.stdout ?? "").toString().trim() || "toktx";
222677
223108
  } catch {}
@@ -222687,7 +223118,7 @@ var requireToktx = () => {
222687
223118
  };
222688
223119
  var encodeKtx2 = (toktx, png2, ktx2) => {
222689
223120
  const args = ["--t2", "--encode", "uastc", "--uastc_quality", "2", "--zcmp", "18", "--genmipmap", "--assign_oetf", "srgb", ktx2, png2];
222690
- const p2 = spawnSync4(toktx, args, { encoding: "utf8" });
223121
+ const p2 = spawnSync6(toktx, args, { encoding: "utf8" });
222691
223122
  if (p2.status !== 0) {
222692
223123
  throw new CliError(`toktx failed (${p2.status ?? p2.error?.message ?? "?"}) while encoding ${ktx2}:
222693
223124
  ${(p2.stderr || p2.stdout || "").trim()}`);
@@ -222701,10 +223132,10 @@ var bake = async (args) => {
222701
223132
  root = process.cwd();
222702
223133
  }
222703
223134
  const toktx = requireToktx();
222704
- const outDir = resolve12(root, flagStr(args, "out") ?? "assets/lightmap");
223135
+ const outDir = resolve13(root, flagStr(args, "out") ?? "assets/lightmap");
222705
223136
  const entryFlag = flagStr(args, "entry");
222706
223137
  const entryFile = entryFlag ?? "main.ts";
222707
- if (!existsSync27(resolve12(root, entryFile))) {
223138
+ if (!existsSync28(resolve13(root, entryFile))) {
222708
223139
  throw new CliError(`No ${entryFile} in ${root} — pass --entry <file> (the file that builds the scene and calls Lightmap.load).`);
222709
223140
  }
222710
223141
  const cfg = {
@@ -222727,7 +223158,7 @@ var bake = async (args) => {
222727
223158
  const entryText = `globalThis.__lecodesLightmap = ${JSON.stringify(cfg)}
222728
223159
  import ${JSON.stringify(importPath)}
222729
223160
  `;
222730
- mkdirSync20(outDir, { recursive: true });
223161
+ mkdirSync21(outDir, { recursive: true });
222731
223162
  ensurePlaceholders(outDir);
222732
223163
  const desktopRenderer = resolveDesktopRenderer(root, args);
222733
223164
  const { js, resources } = await buildBundle(args, undefined, {
@@ -222736,8 +223167,8 @@ import ${JSON.stringify(importPath)}
222736
223167
  extraEntries: [{ path: entryPath, type: "text", text: entryText }],
222737
223168
  entryOverride: entryPath
222738
223169
  });
222739
- const scratch = mkdtempSync3(join29(tmpdir3(), "lecodes-lightmap-"));
222740
- const outPng = join29(scratch, "frame.png");
223170
+ const scratch = mkdtempSync4(join30(tmpdir4(), "lecodes-lightmap-"));
223171
+ const outPng = join30(scratch, "frame.png");
222741
223172
  note(`Baking ${cfg.size}² at ${(cfg.texel * 100).toFixed(1)} cm texels (${cfg.sunRays} sun + ${cfg.aoRays} AO rays) → ${relative4(root, outDir) || "."}…`);
222742
223173
  let output = "";
222743
223174
  try {
@@ -222759,7 +223190,7 @@ import ${JSON.stringify(importPath)}
222759
223190
  throw e2;
222760
223191
  output = msg;
222761
223192
  } finally {
222762
- rmSync8(scratch, { recursive: true, force: true });
223193
+ rmSync9(scratch, { recursive: true, force: true });
222763
223194
  }
222764
223195
  const lines = output.split(/\r?\n/).filter((l2) => l2.startsWith("[lightmap]"));
222765
223196
  for (const l2 of lines) {
@@ -222769,17 +223200,17 @@ import ${JSON.stringify(importPath)}
222769
223200
  logErr(text.startsWith("error") ? c.red(`✗ ${text}`) : c.dim(` ${text}`));
222770
223201
  }
222771
223202
  if (/\[lightmap\] done/.test(output)) {
222772
- const png2 = join29(outDir, "lightmap.png");
222773
- const ktx2 = join29(outDir, "lightmap.ktx2");
222774
- if (!existsSync27(png2))
223203
+ const png2 = join30(outDir, "lightmap.png");
223204
+ const ktx2 = join30(outDir, "lightmap.ktx2");
223205
+ if (!existsSync28(png2))
222775
223206
  throw new CliError(`The host reported a bake but ${png2} is missing.`);
222776
223207
  const t0 = Date.now();
222777
223208
  encodeKtx2(toktx, png2, ktx2);
222778
- const pngKb = Math.round(statSync9(png2).size / 1024), ktxKb = Math.round(statSync9(ktx2).size / 1024);
223209
+ const pngKb = Math.round(statSync10(png2).size / 1024), ktxKb = Math.round(statSync10(ktx2).size / 1024);
222779
223210
  unlinkSync(png2);
222780
223211
  logErr(c.dim(` encoded lightmap.ktx2 (UASTC + mips, ${ktxKb} KB from a ${pngKb} KB raster) in ${((Date.now() - t0) / 1000).toFixed(1)} s`));
222781
223212
  logErr(c.green(`✓ baked → ${relative4(root, outDir) || "."}/lightmap.ktx2 + lightmap.bake`));
222782
- logErr(c.dim(` apply it with Lightmap.load(scene, { data: asset('<rel>/${relative4(root, join29(outDir, "lightmap.bake")).replace(/\\/g, "/")}'), texture: asset('<rel>/${relative4(root, ktx2).replace(/\\/g, "/")}') }) — asset() paths are relative to the calling file`));
223213
+ logErr(c.dim(` apply it with Lightmap.load(scene, { data: asset('<rel>/${relative4(root, join30(outDir, "lightmap.bake")).replace(/\\/g, "/")}'), texture: asset('<rel>/${relative4(root, ktx2).replace(/\\/g, "/")}') }) — asset() paths are relative to the calling file`));
222783
223214
  return;
222784
223215
  }
222785
223216
  if (lines.some((l2) => /no bake module/.test(l2))) {
@@ -222792,13 +223223,13 @@ import ${JSON.stringify(importPath)}
222792
223223
  };
222793
223224
  var PLACEHOLDER_KTX2 = Buffer.from("q0tUWCAyMLsNChoKAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAaAAAACwAAACUAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAQAQAAAAAAABAAAAAAAAAAEAAAAAAAAAAsAAAAAAAAAAIAKACmAQIAAwMAABAAAAAAAAAAAAB/AAAAAAAAAAAA/////xEAAABLVFhvcmllbnRhdGlvbgByAAAAACcAAABLVFh3cml0ZXIAdG9rdHggdjQuNC4yIC8gbGlia3R4IHY0LjQuMgAAMwAAAEtUWHdyaXRlclNjUGFyYW1zAC0tZW5jb2RlIHVhc3RjIC0tdWFzdGNfcXVhbGl0eSAwAAD3////H3zvAQAAAAAAAAAA", "base64");
222794
223225
  var ensurePlaceholders = (outDir) => {
222795
- const bake2 = join29(outDir, "lightmap.bake");
222796
- const ktx2 = join29(outDir, "lightmap.ktx2");
222797
- if (!existsSync27(bake2))
222798
- writeFileSync21(bake2, JSON.stringify({ version: 1, placeholder: true, size: 1, texel: 0, instances: [] }) + `
223226
+ const bake2 = join30(outDir, "lightmap.bake");
223227
+ const ktx2 = join30(outDir, "lightmap.ktx2");
223228
+ if (!existsSync28(bake2))
223229
+ writeFileSync22(bake2, JSON.stringify({ version: 1, placeholder: true, size: 1, texel: 0, instances: [] }) + `
222799
223230
  `);
222800
- if (!existsSync27(ktx2))
222801
- writeFileSync21(ktx2, PLACEHOLDER_KTX2);
223231
+ if (!existsSync28(ktx2))
223232
+ writeFileSync22(ktx2, PLACEHOLDER_KTX2);
222802
223233
  };
222803
223234
  var lightmap = async (args) => {
222804
223235
  const sub = args._[0];
@@ -222824,7 +223255,7 @@ var login = async (args) => {
222824
223255
 
222825
223256
  // src/commands/shaders.ts
222826
223257
  init_manifest();
222827
- import { readFileSync as readFileSync24 } from "node:fs";
223258
+ import { readFileSync as readFileSync25 } from "node:fs";
222828
223259
  init_util();
222829
223260
  init_compile();
222830
223261
  var collectShaders = () => {
@@ -222856,7 +223287,7 @@ var status2 = async () => {
222856
223287
  if (matcId === null)
222857
223288
  state = c.dim("unknown (matc not downloaded)");
222858
223289
  else {
222859
- const text = readFileSync24(shader.absPath, "utf8");
223290
+ const text = readFileSync25(shader.absPath, "utf8");
222860
223291
  if (shaderIsCompiled(root, text, matcId))
222861
223292
  state = "compiled";
222862
223293
  else {
@@ -222865,7 +223296,7 @@ var status2 = async () => {
222865
223296
  }
222866
223297
  }
222867
223298
  log(` ${shader.path.padEnd(width)} ${state}`);
222868
- const schema = parseShaderSchema(readFileSync24(shader.absPath, "utf8"));
223299
+ const schema = parseShaderSchema(readFileSync25(shader.absPath, "utf8"));
222869
223300
  for (const p2 of schema.params) {
222870
223301
  const range = p2.min !== undefined ? ` [${p2.min}..${p2.max}]` : "";
222871
223302
  const def = p2.default !== null && p2.default !== undefined ? ` = ${JSON.stringify(p2.default)}` : "";
@@ -222908,7 +223339,7 @@ var compile3 = async (args) => {
222908
223339
  }
222909
223340
  const matcExe = await resolveMatcExe();
222910
223341
  const matcId = matcIdentity(matcExe);
222911
- const wasCompiled = new Map(shaders.map((s2) => [s2.path, shaderIsCompiled(root, readFileSync24(s2.absPath, "utf8"), matcId)]));
223342
+ const wasCompiled = new Map(shaders.map((s2) => [s2.path, shaderIsCompiled(root, readFileSync25(s2.absPath, "utf8"), matcId)]));
222912
223343
  const errors2 = [];
222913
223344
  for (const shader of shaders) {
222914
223345
  try {
@@ -222942,8 +223373,8 @@ var shaders = async (args) => {
222942
223373
  };
222943
223374
 
222944
223375
  // src/commands/render.ts
222945
- import { mkdirSync as mkdirSync21, readFileSync as readFileSync26, writeFileSync as writeFileSync22 } from "node:fs";
222946
- import { dirname as dirname15, resolve as resolve15 } from "node:path";
223376
+ import { mkdirSync as mkdirSync22, readFileSync as readFileSync27, writeFileSync as writeFileSync23 } from "node:fs";
223377
+ import { dirname as dirname15, resolve as resolve16 } from "node:path";
222947
223378
 
222948
223379
  // src/compile/headlessBundle.ts
222949
223380
  init_api();
@@ -223027,23 +223458,23 @@ var compileHeadlessBundle = async (args, opts = {}) => {
223027
223458
  // src/compile/screenEntry.ts
223028
223459
  init_designMeta();
223029
223460
  init_util();
223030
- import { existsSync as existsSync28, readFileSync as readFileSync25, statSync as statSync10 } from "node:fs";
223031
- import { isAbsolute, join as join30, relative as relative5, resolve as resolve13 } from "node:path";
223461
+ import { existsSync as existsSync29, readFileSync as readFileSync26, statSync as statSync11 } from "node:fs";
223462
+ import { isAbsolute, join as join31, relative as relative5, resolve as resolve14 } from "node:path";
223032
223463
  var SCREEN_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs"];
223033
223464
  var SCREEN_ENTRY_PATH = "/.screen-entry.ts";
223034
223465
  var DEFAULT_DESIGN_DIR = "design";
223035
223466
  var screenArg = (args) => args._[0];
223036
223467
  var hasSeparator = (file) => file.includes("/") || file.includes("\\");
223037
223468
  var findScreenFile = (root, file, dirFlag) => {
223038
- const candidates = [resolve13(process.cwd(), file)];
223469
+ const candidates = [resolve14(process.cwd(), file)];
223039
223470
  if (!hasSeparator(file)) {
223040
- candidates.push(join30(root, (dirFlag ?? DEFAULT_DESIGN_DIR).replace(/[/\\]+$/, ""), "screens", file));
223471
+ candidates.push(join31(root, (dirFlag ?? DEFAULT_DESIGN_DIR).replace(/[/\\]+$/, ""), "screens", file));
223041
223472
  }
223042
223473
  for (const base of candidates) {
223043
- if (existsSync28(base) && statSync10(base).isFile())
223474
+ if (existsSync29(base) && statSync11(base).isFile())
223044
223475
  return base;
223045
223476
  for (const ext2 of SCREEN_EXTS)
223046
- if (existsSync28(base + ext2))
223477
+ if (existsSync29(base + ext2))
223047
223478
  return base + ext2;
223048
223479
  }
223049
223480
  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/.`));
@@ -223052,8 +223483,8 @@ var designDirOf = (root, key) => {
223052
223483
  const m = /^\/(.*)screens\/([^/]+)\.[^./]+$/.exec(key);
223053
223484
  if (!m)
223054
223485
  return null;
223055
- const dir = join30(root, m[1]);
223056
- return existsSync28(join30(dir, "meta.json")) ? { dir, id: m[2] } : null;
223486
+ const dir = join31(root, m[1]);
223487
+ return existsSync29(join31(dir, "meta.json")) ? { dir, id: m[2] } : null;
223057
223488
  };
223058
223489
  var resolveScreenTarget = (root, file, opts = {}) => {
223059
223490
  const abs = findScreenFile(root, file, opts.dir);
@@ -223062,7 +223493,7 @@ var resolveScreenTarget = (root, file, opts = {}) => {
223062
223493
  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.`);
223063
223494
  }
223064
223495
  const key = toKey(rel);
223065
- if (!/^\s*export\s+default\b/m.test(readFileSync25(abs, "utf8"))) {
223496
+ if (!/^\s*export\s+default\b/m.test(readFileSync26(abs, "utf8"))) {
223066
223497
  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.`);
223067
223498
  }
223068
223499
  const invoke = opts.state !== undefined ? `screen(${JSON.stringify(opts.state)})` : "screen()";
@@ -223101,7 +223532,7 @@ init_manifest();
223101
223532
 
223102
223533
  // src/desktopScript.ts
223103
223534
  init_util();
223104
- import { resolve as resolve14 } from "node:path";
223535
+ import { resolve as resolve15 } from "node:path";
223105
223536
  var isPoint = (v) => typeof v === "object" && v !== null && typeof v.x === "number" && typeof v.y === "number";
223106
223537
  var FRAME_MS = 16;
223107
223538
  var ACTIONS = ["tap", "type", "fill", "scroll", "wait", "frames", "drag", "hold", "key", "keyDown", "keyUp", "look", "mouseTo", "wheel", "stick", "gamepad", "camera", "view"];
@@ -223302,7 +223733,7 @@ var translateScenario = (rawSteps, opts) => {
223302
223733
  if (s2.screenshot !== undefined) {
223303
223734
  if (typeof s2.screenshot !== "string")
223304
223735
  throw new CliError(`${at}: "screenshot" must be a path.`);
223305
- plan.screenshot = resolve14(opts.outDir, s2.screenshot);
223736
+ plan.screenshot = resolve15(opts.outDir, s2.screenshot);
223306
223737
  plan.lines.push(emit(`frames 2`));
223307
223738
  plan.lines.push(emit(`shot ${plan.screenshot}`));
223308
223739
  budgetMs += 3 * FRAME_MS;
@@ -223411,9 +223842,9 @@ var requireRenderer = async () => {
223411
223842
  };
223412
223843
  var fileScenarioIO = (dir) => ({
223413
223844
  write: (relPath, data) => {
223414
- const abs = resolve15(dir, relPath);
223415
- mkdirSync21(dirname15(abs), { recursive: true });
223416
- writeFileSync22(abs, data);
223845
+ const abs = resolve16(dir, relPath);
223846
+ mkdirSync22(dirname15(abs), { recursive: true });
223847
+ writeFileSync23(abs, data);
223417
223848
  }
223418
223849
  });
223419
223850
  var resolveFixtures = (value, baseDir) => {
@@ -223421,9 +223852,9 @@ var resolveFixtures = (value, baseDir) => {
223421
223852
  return;
223422
223853
  let map = value;
223423
223854
  if (typeof value === "string") {
223424
- const abs = resolve15(baseDir, value);
223855
+ const abs = resolve16(baseDir, value);
223425
223856
  try {
223426
- map = JSON.parse(readFileSync26(abs, "utf8"));
223857
+ map = JSON.parse(readFileSync27(abs, "utf8"));
223427
223858
  } catch (e2) {
223428
223859
  throw new CliError(`Can't read fixtures "${value}": ${e2 instanceof Error ? e2.message : String(e2)}`);
223429
223860
  }
@@ -223436,7 +223867,7 @@ var resolveFixtures = (value, baseDir) => {
223436
223867
  var readScenarioFile = (path2) => {
223437
223868
  let raw;
223438
223869
  try {
223439
- raw = JSON.parse(readFileSync26(resolve15(path2), "utf8"));
223870
+ raw = JSON.parse(readFileSync27(resolve16(path2), "utf8"));
223440
223871
  } catch (e2) {
223441
223872
  throw new CliError(`Can't read scenario "${path2}": ${e2 instanceof Error ? e2.message : String(e2)}`);
223442
223873
  }
@@ -223493,13 +223924,13 @@ var renderDesktop = async (args) => {
223493
223924
  extraEntries: screen2 ? [screen2.entry] : undefined,
223494
223925
  entryOverride: screen2?.entryPath
223495
223926
  });
223496
- mkdirSync21(dirname15(resolve15(pngFlag)), { recursive: true });
223927
+ mkdirSync22(dirname15(resolve16(pngFlag)), { recursive: true });
223497
223928
  if (scenario) {
223498
223929
  const outDir = flagStr(args, "out-dir") ?? ".";
223499
223930
  const plan = translateScenario(scenario.steps, { outDir, headroomFrames: frames });
223500
223931
  for (const st of plan.steps)
223501
223932
  if (st.screenshot)
223502
- mkdirSync21(dirname15(st.screenshot), { recursive: true });
223933
+ mkdirSync22(dirname15(st.screenshot), { recursive: true });
223503
223934
  const timeoutMs2 = Number(flagStr(args, "timeout")) || 60000 + plan.steps.length * 5000;
223504
223935
  note(`Running ${plan.steps.length} step${plan.steps.length === 1 ? "" : "s"} (native)…`);
223505
223936
  const { output } = await runDesktopRender(js, resources, {
@@ -223603,7 +224034,7 @@ var render = async (args) => {
223603
224034
  const settleStr = flagStr(args, "settle") ?? (scenario?.settle !== undefined ? String(scenario.settle) : undefined);
223604
224035
  const timeStr = flagStr(args, "time");
223605
224036
  const onConsole = flagBool(args, "logs") ? (level, text2) => logErr(`${c.dim(`[${level}]`)} ${text2}`) : undefined;
223606
- const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname15(resolve15(scriptFlag)) : ".");
224037
+ const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname15(resolve16(scriptFlag)) : ".");
223607
224038
  const flagFixtures = resolveFixtures(flagStr(args, "fixtures"), ".");
223608
224039
  const fixtures = envelopeFixtures || flagFixtures ? { ...envelopeFixtures, ...flagFixtures } : undefined;
223609
224040
  if (fixtures) {
@@ -223642,7 +224073,7 @@ var render = async (args) => {
223642
224073
  ${c.red(st.error ?? "failed")}`);
223643
224074
  }
223644
224075
  if (!result2.ok && result2.failureArtifacts.length > 0) {
223645
- logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p2) => resolve15(outDir, p2)).join(", ")}`));
224076
+ logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p2) => resolve16(outDir, p2)).join(", ")}`));
223646
224077
  }
223647
224078
  logErr(result2.ok ? c.green(`✓ ${result2.steps.length}/${steps.length} steps passed`) : c.red(`✗ failed at step ${result2.steps.length} of ${steps.length}`));
223648
224079
  return done((cb) => cb(), result2.ok ? 0 : 1);
@@ -223666,8 +224097,8 @@ var render = async (args) => {
223666
224097
  if (isNullSink(pngFlag)) {
223667
224098
  note(`Discarded PNG (${pngFlag})`);
223668
224099
  } else {
223669
- mkdirSync21(dirname15(resolve15(pngFlag)), { recursive: true });
223670
- writeFileSync22(resolve15(pngFlag), png2);
224100
+ mkdirSync22(dirname15(resolve16(pngFlag)), { recursive: true });
224101
+ writeFileSync23(resolve16(pngFlag), png2);
223671
224102
  note(`Wrote ${pngFlag} (${png2.length} bytes, ${outW}x${outH}${clip !== undefined ? ", clipped" : ""})`);
223672
224103
  }
223673
224104
  return done((cb) => cb());
@@ -223682,7 +224113,7 @@ var render = async (args) => {
223682
224113
  return done((cb) => cb());
223683
224114
  }
223684
224115
  if (out) {
223685
- writeFileSync22(resolve15(out), result.text);
224116
+ writeFileSync23(resolve16(out), result.text);
223686
224117
  note(`Wrote ${out} (${result.text.length} bytes)`);
223687
224118
  return done((cb) => cb());
223688
224119
  }
@@ -223697,8 +224128,8 @@ init_api();
223697
224128
  init_config();
223698
224129
  init_manifest();
223699
224130
  init_util();
223700
- import { appendFileSync as appendFileSync2, existsSync as existsSync29, readFileSync as readFileSync27, writeFileSync as writeFileSync23 } from "node:fs";
223701
- import { join as join31 } from "node:path";
224131
+ import { appendFileSync as appendFileSync2, existsSync as existsSync30, readFileSync as readFileSync28, writeFileSync as writeFileSync24 } from "node:fs";
224132
+ import { join as join32 } from "node:path";
223702
224133
  var ENV_VAR = "LECODES_PUSH_KEY";
223703
224134
  var context = () => {
223704
224135
  const root = findProjectRoot(process.cwd());
@@ -223842,11 +224273,11 @@ var pnKeysList = async (ctx) => {
223842
224273
  };
223843
224274
  var writeEnv = (root, key) => {
223844
224275
  const touched = [];
223845
- const envPath = join31(root, ".env");
224276
+ const envPath = join32(root, ".env");
223846
224277
  const line = `${ENV_VAR}=${key}`;
223847
- const existing = existsSync29(envPath) ? readFileSync27(envPath, "utf8") : "";
224278
+ const existing = existsSync30(envPath) ? readFileSync28(envPath, "utf8") : "";
223848
224279
  if (new RegExp(`^${ENV_VAR}=`, "m").test(existing)) {
223849
- writeFileSync23(envPath, existing.replace(new RegExp(`^${ENV_VAR}=.*$`, "m"), line));
224280
+ writeFileSync24(envPath, existing.replace(new RegExp(`^${ENV_VAR}=.*$`, "m"), line));
223850
224281
  } else {
223851
224282
  const separator = !existing || existing.endsWith(`
223852
224283
  `) ? "" : `
@@ -223855,8 +224286,8 @@ var writeEnv = (root, key) => {
223855
224286
  `);
223856
224287
  }
223857
224288
  touched.push(".env");
223858
- const ignorePath = join31(root, ".gitignore");
223859
- const ignore = existsSync29(ignorePath) ? readFileSync27(ignorePath, "utf8") : "";
224289
+ const ignorePath = join32(root, ".gitignore");
224290
+ const ignore = existsSync30(ignorePath) ? readFileSync28(ignorePath, "utf8") : "";
223860
224291
  if (!ignore.split(`
223861
224292
  `).some((l2) => l2.trim() === ".env")) {
223862
224293
  const separator = !ignore || ignore.endsWith(`
@@ -223955,8 +224386,8 @@ var pn = async (args) => {
223955
224386
  init_api();
223956
224387
  init_config();
223957
224388
  init_manifest();
223958
- import { existsSync as existsSync30, mkdirSync as mkdirSync22, rmSync as rmSync9, writeFileSync as writeFileSync24 } from "node:fs";
223959
- import { dirname as dirname16, join as join32 } from "node:path";
224389
+ import { existsSync as existsSync31, mkdirSync as mkdirSync23, rmSync as rmSync10, writeFileSync as writeFileSync25 } from "node:fs";
224390
+ import { dirname as dirname16, join as join33 } from "node:path";
223960
224391
  init_types4();
223961
224392
  init_localFiles();
223962
224393
  init_util();
@@ -223979,9 +224410,9 @@ var pull = async (args) => {
223979
224410
  const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a2) => a2.id === id)?.type !== "folder").map(([, path2]) => path2));
223980
224411
  for (const path2 of Object.keys(manifest.files)) {
223981
224412
  if (!remotePaths.has(path2)) {
223982
- const abs = join32(root, ...path2.replace(/^\//, "").split("/"));
223983
- if (existsSync30(abs))
223984
- rmSync9(abs);
224413
+ const abs = join33(root, ...path2.replace(/^\//, "").split("/"));
224414
+ if (existsSync31(abs))
224415
+ rmSync10(abs);
223985
224416
  }
223986
224417
  }
223987
224418
  const files = await materializeAssets(root, project, apiUrl, token, progress);
@@ -224018,18 +224449,18 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
224018
224449
  const files = { ...manifest.files };
224019
224450
  for (const asset of targets) {
224020
224451
  const key = pathById.get(asset.id);
224021
- const abs = join32(root, ...key.replace(/^\//, "").split("/"));
224022
- mkdirSync22(dirname16(abs), { recursive: true });
224452
+ const abs = join33(root, ...key.replace(/^\//, "").split("/"));
224453
+ mkdirSync23(dirname16(abs), { recursive: true });
224023
224454
  let sha;
224024
224455
  if (asset.type === "file") {
224025
224456
  if (!asset.file?.src)
224026
224457
  continue;
224027
224458
  const bytes = await downloadBlob(apiUrl, asset.file.src);
224028
- writeFileSync24(abs, bytes);
224459
+ writeFileSync25(abs, bytes);
224029
224460
  sha = sha256(bytes);
224030
224461
  } else {
224031
224462
  const text = asset.text ?? "";
224032
- writeFileSync24(abs, text);
224463
+ writeFileSync25(abs, text);
224033
224464
  sha = sha256(text);
224034
224465
  }
224035
224466
  files[key] = { id: asset.id, type: kindOf3(asset.type), sha };
@@ -224042,8 +224473,8 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
224042
224473
 
224043
224474
  // src/commands/scene.ts
224044
224475
  init_manifest();
224045
- import { existsSync as existsSync32, readFileSync as readFileSync29, readdirSync as readdirSync13, statSync as statSync11, writeFileSync as writeFileSync26 } from "node:fs";
224046
- import { basename as basename15, dirname as dirname17, join as join34, resolve as resolve16 } from "node:path";
224476
+ import { existsSync as existsSync33, readFileSync as readFileSync30, readdirSync as readdirSync14, statSync as statSync12, writeFileSync as writeFileSync27 } from "node:fs";
224477
+ import { basename as basename16, dirname as dirname17, join as join35, resolve as resolve17 } from "node:path";
224047
224478
 
224048
224479
  // src/compile/sceneCompile.ts
224049
224480
  init_localFiles();
@@ -224052,18 +224483,18 @@ init_distRoot();
224052
224483
  init_util();
224053
224484
  init_projectCompile();
224054
224485
  import { createHash as createHash8 } from "node:crypto";
224055
- import { existsSync as existsSync31, mkdirSync as mkdirSync23, readFileSync as readFileSync28, writeFileSync as writeFileSync25 } from "node:fs";
224056
- import { join as join33 } from "node:path";
224486
+ import { existsSync as existsSync32, mkdirSync as mkdirSync24, readFileSync as readFileSync29, writeFileSync as writeFileSync26 } from "node:fs";
224487
+ import { join as join34 } from "node:path";
224057
224488
  var TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i;
224058
224489
  var rel = (key) => key.replace(/^\//, "");
224059
224490
  var resolveHarness = (loadSceneHarness2) => {
224060
- const inRepo = join33(distRoot, "..", "projects", "src", "scene-editor");
224491
+ const inRepo = join34(distRoot, "..", "projects", "src", "scene-editor");
224061
224492
  try {
224062
- return loadSceneHarness2(existsSync31(inRepo) ? inRepo : undefined);
224493
+ return loadSceneHarness2(existsSync32(inRepo) ? inRepo : undefined);
224063
224494
  } catch {
224064
- const vendored = join33(distRoot, "runtime", "scene-harness.json");
224065
- if (existsSync31(vendored))
224066
- return JSON.parse(readFileSync28(vendored, "utf8"));
224495
+ const vendored = join34(distRoot, "runtime", "scene-harness.json");
224496
+ if (existsSync32(vendored))
224497
+ return JSON.parse(readFileSync29(vendored, "utf8"));
224067
224498
  throw new CliError("scene-editor harness sources not found — reinstall the CLI or set LECODES_SCENE_HARNESS_DIR.");
224068
224499
  }
224069
224500
  };
@@ -224075,9 +224506,9 @@ var compileSceneEditor = async (opts) => {
224075
224506
  for (const file of scan2.files) {
224076
224507
  const path2 = "./" + rel(file.path);
224077
224508
  if (TEXT_EXT.test(path2)) {
224078
- entries.push({ path: path2, type: "text", text: readFileSync28(file.absPath, "utf8") });
224509
+ entries.push({ path: path2, type: "text", text: readFileSync29(file.absPath, "utf8") });
224079
224510
  } else if (/\.mat$/i.test(path2)) {
224080
- entries.push({ path: path2, type: "shader", text: readFileSync28(file.absPath, "utf8"), absPath: file.absPath });
224511
+ entries.push({ path: path2, type: "shader", text: readFileSync29(file.absPath, "utf8"), absPath: file.absPath });
224081
224512
  } else {
224082
224513
  entries.push({ path: path2, type: "resource", fileSrc: "/project/" + rel(file.path) });
224083
224514
  }
@@ -224093,10 +224524,10 @@ var compileSceneEditor = async (opts) => {
224093
224524
  continue;
224094
224525
  const hash = createHash8("sha256").update(e2.text ?? "").digest("hex").slice(0, 16);
224095
224526
  const dir = shadersCacheDir(root);
224096
- mkdirSync23(dir, { recursive: true });
224527
+ mkdirSync24(dir, { recursive: true });
224097
224528
  const liteName = `${hash}_webgl-lite.filamat`;
224098
- if (!existsSync31(join33(dir, liteName)))
224099
- writeFileSync25(join33(dir, liteName), JSON.stringify(buildLiteMaterialArtifact2(e2.text ?? "")));
224529
+ if (!existsSync32(join34(dir, liteName)))
224530
+ writeFileSync26(join34(dir, liteName), JSON.stringify(buildLiteMaterialArtifact2(e2.text ?? "")));
224100
224531
  const src = `/project/.lecodes/shaders/${hash}_opengl.filamat`;
224101
224532
  e2.shaders = ["opengl", "webgl", "webgl-lite"].map((platform) => ({ platform, src }));
224102
224533
  }
@@ -224130,7 +224561,7 @@ var DEFAULT_PORT3 = 4488;
224130
224561
  var findAppRoot = (start) => {
224131
224562
  let dir = start;
224132
224563
  while (true) {
224133
- if (existsSync32(join34(dir, "app.json")))
224564
+ if (existsSync33(join35(dir, "app.json")))
224134
224565
  return dir;
224135
224566
  const parent = dirname17(dir);
224136
224567
  if (parent === dir)
@@ -224149,10 +224580,10 @@ var getContext2 = (args) => {
224149
224580
  manifest = null;
224150
224581
  const pathArg = args._[0];
224151
224582
  if (pathArg) {
224152
- const abs = resolve16(process.cwd(), pathArg);
224153
- if (existsSync32(abs) && statSync11(abs).isDirectory())
224583
+ const abs = resolve17(process.cwd(), pathArg);
224584
+ if (existsSync33(abs) && statSync12(abs).isDirectory())
224154
224585
  root = abs;
224155
- else if (abs.endsWith(".scene.ts") && existsSync32(abs))
224586
+ else if (abs.endsWith(".scene.ts") && existsSync33(abs))
224156
224587
  root = findAppRoot(dirname17(abs)) ?? dirname17(abs);
224157
224588
  else
224158
224589
  throw new CliError(`No such folder or .scene.ts: ${pathArg}`);
@@ -224160,15 +224591,15 @@ var getContext2 = (args) => {
224160
224591
  root = findAppRoot(root) ?? root;
224161
224592
  }
224162
224593
  }
224163
- return { root, manifest, name: manifest?.name ?? basename15(root) };
224594
+ return { root, manifest, name: manifest?.name ?? basename16(root) };
224164
224595
  };
224165
224596
  var writeMcpConfig2 = (root, port) => {
224166
- const path2 = join34(root, ".mcp.json");
224597
+ const path2 = join35(root, ".mcp.json");
224167
224598
  const entry = { type: "http", url: `http://localhost:${port}/mcp` };
224168
224599
  let config = {};
224169
- if (existsSync32(path2)) {
224600
+ if (existsSync33(path2)) {
224170
224601
  try {
224171
- config = JSON.parse(readFileSync29(path2, "utf8"));
224602
+ config = JSON.parse(readFileSync30(path2, "utf8"));
224172
224603
  } catch {
224173
224604
  warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-scene server by hand.");
224174
224605
  return;
@@ -224181,17 +224612,17 @@ var writeMcpConfig2 = (root, port) => {
224181
224612
  return;
224182
224613
  }
224183
224614
  config.mcpServers["lecodes-scene"] = entry;
224184
- writeFileSync26(path2, JSON.stringify(config, null, 2) + `
224615
+ writeFileSync27(path2, JSON.stringify(config, null, 2) + `
224185
224616
  `);
224186
224617
  note(`${existing ? "updated" : "created"} .mcp.json (lecodes-scene MCP → ${entry.url})`);
224187
224618
  };
224188
224619
  var countScenes = (dir) => {
224189
224620
  let n2 = 0;
224190
- for (const name of readdirSync13(dir)) {
224621
+ for (const name of readdirSync14(dir)) {
224191
224622
  if (name === "node_modules" || name.startsWith("."))
224192
224623
  continue;
224193
- const full = join34(dir, name);
224194
- if (statSync11(full).isDirectory())
224624
+ const full = join35(dir, name);
224625
+ if (statSync12(full).isDirectory())
224195
224626
  n2 += countScenes(full);
224196
224627
  else if (name.endsWith(".scene.ts"))
224197
224628
  n2++;
@@ -224236,9 +224667,9 @@ var scene = async (args) => {
224236
224667
  init_util();
224237
224668
 
224238
224669
  // src/cmgenTool.ts
224239
- import { existsSync as existsSync33, readdirSync as readdirSync14 } from "node:fs";
224670
+ import { existsSync as existsSync34, readdirSync as readdirSync15 } from "node:fs";
224240
224671
  import { homedir as homedir8 } from "node:os";
224241
- import { join as join35, resolve as resolve17 } from "node:path";
224672
+ import { join as join36, resolve as resolve18 } from "node:path";
224242
224673
  init_util();
224243
224674
  var exeName2 = () => process.platform === "win32" ? "cmgen.exe" : "cmgen";
224244
224675
  var platformKey3 = () => {
@@ -224248,10 +224679,10 @@ var platformKey3 = () => {
224248
224679
  return "linux-x64";
224249
224680
  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}.`);
224250
224681
  };
224251
- var cacheRoot3 = () => join35(homedir8(), ".lecodes", "cmgen");
224682
+ var cacheRoot3 = () => join36(homedir8(), ".lecodes", "cmgen");
224252
224683
  var findCmgenIn = (dir) => {
224253
- for (const entry of readdirSync14(dir, { withFileTypes: true })) {
224254
- const p2 = join35(dir, entry.name);
224684
+ for (const entry of readdirSync15(dir, { withFileTypes: true })) {
224685
+ const p2 = join36(dir, entry.name);
224255
224686
  if (entry.isFile() && entry.name === exeName2())
224256
224687
  return p2;
224257
224688
  if (entry.isDirectory()) {
@@ -224264,7 +224695,7 @@ var findCmgenIn = (dir) => {
224264
224695
  };
224265
224696
  var installedCmgenVersions = () => {
224266
224697
  try {
224267
- return readdirSync14(cacheRoot3(), { withFileTypes: true }).filter((e2) => e2.isDirectory() && e2.name.startsWith("cmgen-")).map((e2) => e2.name.slice("cmgen-".length)).filter((v) => findCmgenIn(join35(cacheRoot3(), `cmgen-${v}`)) !== null).sort(semverNewer);
224698
+ return readdirSync15(cacheRoot3(), { withFileTypes: true }).filter((e2) => e2.isDirectory() && e2.name.startsWith("cmgen-")).map((e2) => e2.name.slice("cmgen-".length)).filter((v) => findCmgenIn(join36(cacheRoot3(), `cmgen-${v}`)) !== null).sort(semverNewer);
224268
224699
  } catch {
224269
224700
  return [];
224270
224701
  }
@@ -224286,19 +224717,19 @@ var downloadCmgen = async (version, versions, dest) => {
224286
224717
  var resolveCmgenExeCached = () => {
224287
224718
  const override = process.env.LECODES_CMGEN;
224288
224719
  if (override) {
224289
- if (!existsSync33(override))
224720
+ if (!existsSync34(override))
224290
224721
  throw new CliError(`LECODES_CMGEN points at "${override}" which doesn't exist.`);
224291
- return resolve17(override);
224722
+ return resolve18(override);
224292
224723
  }
224293
224724
  const cached = installedCmgenVersions()[0];
224294
- return cached ? findCmgenIn(join35(cacheRoot3(), `cmgen-${cached}`)) : null;
224725
+ return cached ? findCmgenIn(join36(cacheRoot3(), `cmgen-${cached}`)) : null;
224295
224726
  };
224296
224727
  var resolveCmgenExe = async () => {
224297
224728
  const cached = resolveCmgenExeCached();
224298
224729
  if (cached)
224299
224730
  return cached;
224300
224731
  const { latest, versions } = await fetchCmgenManifest();
224301
- const dir = join35(cacheRoot3(), `cmgen-${latest}`);
224732
+ const dir = join36(cacheRoot3(), `cmgen-${latest}`);
224302
224733
  await downloadCmgen(latest, versions, dir);
224303
224734
  const exe = findCmgenIn(dir);
224304
224735
  if (!exe)
@@ -224356,8 +224787,8 @@ var status3 = async (_args) => {
224356
224787
  };
224357
224788
 
224358
224789
  // src/commands/test.ts
224359
- import { existsSync as existsSync34, mkdirSync as mkdirSync24, readFileSync as readFileSync30, readdirSync as readdirSync15 } from "node:fs";
224360
- import { dirname as dirname18, resolve as resolve18 } from "node:path";
224790
+ import { existsSync as existsSync35, mkdirSync as mkdirSync25, readFileSync as readFileSync31, readdirSync as readdirSync16 } from "node:fs";
224791
+ import { dirname as dirname18, resolve as resolve19 } from "node:path";
224361
224792
  init_manifest();
224362
224793
  init_util();
224363
224794
  var deriveDesignFlows = (meta, entryScreen) => {
@@ -224420,7 +224851,7 @@ var slug3 = (s2) => s2.replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "").
224420
224851
  var readFlowFile = (abs, displayName) => {
224421
224852
  let raw;
224422
224853
  try {
224423
- raw = JSON.parse(readFileSync30(abs, "utf8"));
224854
+ raw = JSON.parse(readFileSync31(abs, "utf8"));
224424
224855
  } catch (e2) {
224425
224856
  throw new CliError(`Can't read "${displayName}": ${e2 instanceof Error ? e2.message : String(e2)}`);
224426
224857
  }
@@ -224475,40 +224906,40 @@ var test = async (args) => {
224475
224906
  if (!Number.isFinite(nativeFixedDt) || nativeFixedDt < 0 || nativeFixedDt > 1000)
224476
224907
  throw new CliError(`--fixed-dt wants a number of ms (0 = wall clock, max 1000), got "${fixedDtFlag}".`);
224477
224908
  const projectViewport = desktopDefaultViewport(root);
224478
- const testsDir = resolve18(root, flagStr(args, "dir") ?? "tests");
224479
- const designMetaPath = resolve18(root, flagStr(args, "design-dir") ?? "design", "meta.json");
224909
+ const testsDir = resolve19(root, flagStr(args, "dir") ?? "tests");
224910
+ const designMetaPath = resolve19(root, flagStr(args, "design-dir") ?? "design", "meta.json");
224480
224911
  const asJson = flagBool(args, "json");
224481
224912
  const out = asJson ? logErr : log;
224482
224913
  const cases = [];
224483
224914
  if (args._.length > 0) {
224484
224915
  for (const given of args._) {
224485
- const candidates = [resolve18(given), resolve18(testsDir, given), resolve18(testsDir, `${given}.flow.json`)];
224486
- const abs = candidates.find((p2) => existsSync34(p2));
224916
+ const candidates = [resolve19(given), resolve19(testsDir, given), resolve19(testsDir, `${given}.flow.json`)];
224917
+ const abs = candidates.find((p2) => existsSync35(p2));
224487
224918
  if (!abs)
224488
224919
  throw new CliError(`No such test: "${given}" (looked in ${testsDir}).`);
224489
224920
  cases.push(readFlowFile(abs, given));
224490
224921
  }
224491
- } else if (existsSync34(testsDir)) {
224492
- for (const f3 of readdirSync15(testsDir).filter((f4) => f4.endsWith(".flow.json")).sort()) {
224493
- cases.push(readFlowFile(resolve18(testsDir, f3), f3));
224922
+ } else if (existsSync35(testsDir)) {
224923
+ for (const f3 of readdirSync16(testsDir).filter((f4) => f4.endsWith(".flow.json")).sort()) {
224924
+ cases.push(readFlowFile(resolve19(testsDir, f3), f3));
224494
224925
  }
224495
224926
  }
224496
224927
  const fixturesFlag = flagStr(args, "fixtures");
224497
- const defaultPack = resolve18(testsDir, "fixtures.json");
224498
- const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") : existsSync34(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
224928
+ const defaultPack = resolve19(testsDir, "fixtures.json");
224929
+ const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") : existsSync35(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
224499
224930
  const flowsOnly = flagBool(args, "flows");
224500
224931
  const wantFlows = !flagBool(args, "no-flows") && args._.length === 0;
224501
224932
  if (flowsOnly)
224502
224933
  cases.length = 0;
224503
224934
  let skipped = [];
224504
224935
  let entryScreen = null;
224505
- if (desktop2 && wantFlows && existsSync34(designMetaPath)) {
224936
+ if (desktop2 && wantFlows && existsSync35(designMetaPath)) {
224506
224937
  note("Design-derived flows skipped with --desktop (they tap by name; run `lecodes test` headless for them).");
224507
224938
  }
224508
- if (!desktop2 && (wantFlows || flowsOnly) && existsSync34(designMetaPath)) {
224939
+ if (!desktop2 && (wantFlows || flowsOnly) && existsSync35(designMetaPath)) {
224509
224940
  let meta;
224510
224941
  try {
224511
- meta = JSON.parse(readFileSync30(designMetaPath, "utf8"));
224942
+ meta = JSON.parse(readFileSync31(designMetaPath, "utf8"));
224512
224943
  } catch (e2) {
224513
224944
  throw new CliError(`Can't read ${designMetaPath}: ${e2 instanceof Error ? e2.message : String(e2)}`);
224514
224945
  }
@@ -224540,16 +224971,16 @@ var test = async (args) => {
224540
224971
  }, timeoutMs);
224541
224972
  killer.unref?.();
224542
224973
  const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
224543
- const artifactsRoot = resolve18(root, flagStr(args, "artifacts") ?? resolve18(testsDir, ".artifacts"));
224974
+ const artifactsRoot = resolve19(root, flagStr(args, "artifacts") ?? resolve19(testsDir, ".artifacts"));
224544
224975
  const results = [];
224545
224976
  const runNativeCase = async (kase) => {
224546
- const caseDir = resolve18(artifactsRoot, slug3(kase.name));
224977
+ const caseDir = resolve19(artifactsRoot, slug3(kase.name));
224547
224978
  const plan = translateScenario(kase.steps, { outDir: caseDir, headroomFrames: nativeFrames });
224548
224979
  for (const st of plan.steps)
224549
224980
  if (st.screenshot)
224550
- mkdirSync24(dirname18(st.screenshot), { recursive: true });
224551
- mkdirSync24(caseDir, { recursive: true });
224552
- const finalPng = resolve18(caseDir, "final.png");
224981
+ mkdirSync25(dirname18(st.screenshot), { recursive: true });
224982
+ mkdirSync25(caseDir, { recursive: true });
224983
+ const finalPng = resolve19(caseDir, "final.png");
224553
224984
  const { output } = await runDesktopRender(native.js, native.resources, {
224554
224985
  renderer: desktopRenderer,
224555
224986
  width: Number(flagStr(args, "width")) || kase.width || projectViewport?.width || 390,
@@ -224601,14 +225032,14 @@ var test = async (args) => {
224601
225032
  onConsole
224602
225033
  });
224603
225034
  try {
224604
- const io = fileScenarioIO(resolve18(artifactsRoot, slug3(kase.name)));
225035
+ const io = fileScenarioIO(resolve19(artifactsRoot, slug3(kase.name)));
224605
225036
  const outcome = await renderer.runScenario(session, steps, io);
224606
225037
  result = {
224607
225038
  name: kase.name,
224608
225039
  kind: kase.kind,
224609
225040
  ok: outcome.ok,
224610
225041
  steps: outcome.steps.map((s2) => ({ desc: s2.desc, ok: s2.ok, error: s2.error })),
224611
- artifacts: outcome.failureArtifacts.map((p2) => resolve18(artifactsRoot, slug3(kase.name), p2))
225042
+ artifacts: outcome.failureArtifacts.map((p2) => resolve19(artifactsRoot, slug3(kase.name), p2))
224612
225043
  };
224613
225044
  } finally {
224614
225045
  session.dispose();
@@ -224676,11 +225107,11 @@ init_config();
224676
225107
  init_manifest();
224677
225108
  init_types4();
224678
225109
  init_util();
224679
- import { resolve as resolve19 } from "node:path";
225110
+ import { resolve as resolve20 } from "node:path";
224680
225111
  var label = (bundle) => `${Object.keys(bundle).length} file(s)`;
224681
225112
  var types4 = async (args) => {
224682
225113
  const dirArg = args._[0];
224683
- const root = dirArg ? resolve19(dirArg) : (() => {
225114
+ const root = dirArg ? resolve20(dirArg) : (() => {
224684
225115
  try {
224685
225116
  return findProjectRoot(process.cwd());
224686
225117
  } catch {
@@ -224739,23 +225170,23 @@ var types4 = async (args) => {
224739
225170
  };
224740
225171
 
224741
225172
  // src/index.ts
224742
- import { existsSync as existsSync36, readFileSync as readFileSync32, realpathSync as realpathSync2 } from "node:fs";
225173
+ import { existsSync as existsSync38, readFileSync as readFileSync34, realpathSync as realpathSync2 } from "node:fs";
224743
225174
  import { createRequire as createRequire4 } from "node:module";
224744
- import { delimiter as delimiter2, dirname as dirname20, join as join37 } from "node:path";
225175
+ import { delimiter as delimiter2, dirname as dirname21, join as join39 } from "node:path";
224745
225176
 
224746
225177
  // src/commands/install.ts
224747
225178
  init_util();
224748
- import { execFileSync as execFileSync5 } from "node:child_process";
224749
- import { appendFileSync as appendFileSync3, chmodSync as chmodSync2, cpSync as cpSync4, existsSync as existsSync35, mkdirSync as mkdirSync25, readFileSync as readFileSync31, realpathSync, rmSync as rmSync10, symlinkSync } from "node:fs";
225179
+ import { execFileSync as execFileSync7 } from "node:child_process";
225180
+ import { appendFileSync as appendFileSync3, chmodSync as chmodSync2, cpSync as cpSync5, existsSync as existsSync36, mkdirSync as mkdirSync26, readFileSync as readFileSync32, realpathSync, rmSync as rmSync11, symlinkSync } from "node:fs";
224750
225181
  import { homedir as homedir9 } from "node:os";
224751
- import { basename as basename16, delimiter, dirname as dirname19, join as join36, resolve as resolve20, sep as sep2 } from "node:path";
225182
+ import { basename as basename17, delimiter, dirname as dirname19, join as join37, resolve as resolve21, sep as sep2 } from "node:path";
224752
225183
  var isWin = process.platform === "win32";
224753
225184
  var HOME = homedir9();
224754
225185
  var safeRealpath = (p2) => {
224755
225186
  try {
224756
225187
  return realpathSync(p2);
224757
225188
  } catch {
224758
- return resolve20(p2);
225189
+ return resolve21(p2);
224759
225190
  }
224760
225191
  };
224761
225192
  var onPath = (dir) => {
@@ -224772,24 +225203,24 @@ var onPath = (dir) => {
224772
225203
  };
224773
225204
  var install = async (args) => {
224774
225205
  const bundle = dirname19(process.execPath);
224775
- const exeName3 = basename16(process.execPath);
225206
+ const exeName3 = basename17(process.execPath);
224776
225207
  const chiselName = isWin ? "chisel.exe" : "chisel";
224777
- if (!existsSync35(join36(bundle, "runtime")) || !existsSync35(join36(bundle, chiselName))) {
225208
+ if (!existsSync36(join37(bundle, "runtime")) || !existsSync36(join37(bundle, chiselName))) {
224778
225209
  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.");
224779
225210
  }
224780
- const companions = [exeName3, chiselName, "runtime", "README.txt"].filter((f3) => existsSync35(join36(bundle, f3)));
225211
+ const companions = [exeName3, chiselName, "runtime", "README.txt"].filter((f3) => existsSync36(join37(bundle, f3)));
224781
225212
  if (isWin)
224782
225213
  return installWindows(args, bundle, companions);
224783
225214
  return installUnix(args, bundle, companions);
224784
225215
  };
224785
225216
  var copyBundle = (bundle, target, companions) => {
224786
- mkdirSync25(target, { recursive: true });
225217
+ mkdirSync26(target, { recursive: true });
224787
225218
  if (safeRealpath(target) === safeRealpath(bundle))
224788
225219
  return;
224789
225220
  for (const f3 of companions) {
224790
- const dest = join36(target, f3);
224791
- rmSync10(dest, { recursive: true, force: true });
224792
- cpSync4(join36(bundle, f3), dest, { recursive: true });
225221
+ const dest = join37(target, f3);
225222
+ rmSync11(dest, { recursive: true, force: true });
225223
+ cpSync5(join37(bundle, f3), dest, { recursive: true });
224793
225224
  }
224794
225225
  };
224795
225226
  var permError = (e2, dir, system) => {
@@ -224801,41 +225232,41 @@ var permError = (e2, dir, system) => {
224801
225232
  };
224802
225233
  var installUnix = (args, bundle, companions) => {
224803
225234
  const system = flagBool(args, "system");
224804
- const libDir = resolve20(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join36(HOME, ".local", "lib", "lecodes-cli")));
224805
- const binDir = resolve20(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join36(HOME, ".local", "bin")));
224806
- const link2 = join36(binDir, "lecodes");
225235
+ const libDir = resolve21(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join37(HOME, ".local", "lib", "lecodes-cli")));
225236
+ const binDir = resolve21(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join37(HOME, ".local", "bin")));
225237
+ const link2 = join37(binDir, "lecodes");
224807
225238
  try {
224808
225239
  copyBundle(bundle, libDir, companions);
224809
- chmodSync2(join36(libDir, "lecodes"), 493);
224810
- chmodSync2(join36(libDir, "chisel"), 493);
225240
+ chmodSync2(join37(libDir, "lecodes"), 493);
225241
+ chmodSync2(join37(libDir, "chisel"), 493);
224811
225242
  } catch (e2) {
224812
225243
  throw permError(e2, libDir, system);
224813
225244
  }
224814
225245
  try {
224815
- mkdirSync25(binDir, { recursive: true });
224816
- rmSync10(link2, { force: true });
224817
- symlinkSync(join36(libDir, "lecodes"), link2);
225246
+ mkdirSync26(binDir, { recursive: true });
225247
+ rmSync11(link2, { force: true });
225248
+ symlinkSync(join37(libDir, "lecodes"), link2);
224818
225249
  } catch (e2) {
224819
225250
  throw permError(e2, binDir, system);
224820
225251
  }
224821
225252
  success(`Installed to ${c.bold(libDir)}`);
224822
- log(` ${c.dim(link2 + " → " + join36(libDir, "lecodes"))}`);
225253
+ log(` ${c.dim(link2 + " → " + join37(libDir, "lecodes"))}`);
224823
225254
  finishPathUnix(args, binDir);
224824
225255
  };
224825
225256
  var shellRc = () => {
224826
- const shell = basename16(process.env.SHELL ?? "");
225257
+ const shell = basename17(process.env.SHELL ?? "");
224827
225258
  if (shell === "zsh")
224828
- return { file: join36(HOME, ".zshrc"), name: "~/.zshrc" };
225259
+ return { file: join37(HOME, ".zshrc"), name: "~/.zshrc" };
224829
225260
  if (shell === "bash")
224830
- return { file: join36(HOME, ".bashrc"), name: "~/.bashrc" };
224831
- return { file: join36(HOME, ".profile"), name: "~/.profile" };
225261
+ return { file: join37(HOME, ".bashrc"), name: "~/.bashrc" };
225262
+ return { file: join37(HOME, ".profile"), name: "~/.profile" };
224832
225263
  };
224833
225264
  var PATH_MARKER = "# Added by `lecodes install`";
224834
225265
  var appendPathToShellRc = (binDir) => {
224835
225266
  const { file, name } = shellRc();
224836
225267
  const line = `export PATH="${binDir}:$PATH"`;
224837
225268
  try {
224838
- const existing = existsSync35(file) ? readFileSync31(file, "utf8") : "";
225269
+ const existing = existsSync36(file) ? readFileSync32(file, "utf8") : "";
224839
225270
  if (existing.includes(line))
224840
225271
  return name;
224841
225272
  appendFileSync3(file, `${existing && !existing.endsWith(`
@@ -224873,7 +225304,7 @@ var finishPathUnix = (args, binDir) => {
224873
225304
  log(` • or install system-wide (no PATH edit): ${c.bold("sudo lecodes install --system")}`);
224874
225305
  };
224875
225306
  var installWindows = (args, bundle, companions) => {
224876
- const target = resolve20(flagStr(args, "dir") ?? join36(process.env.LOCALAPPDATA ?? join36(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
225307
+ const target = resolve21(flagStr(args, "dir") ?? join37(process.env.LOCALAPPDATA ?? join37(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
224877
225308
  try {
224878
225309
  copyBundle(bundle, target, companions);
224879
225310
  } catch (e2) {
@@ -224901,7 +225332,7 @@ var installWindows = (args, bundle, companions) => {
224901
225332
  var addToUserPathWindows = (dir) => {
224902
225333
  const script = "$d = $env:LECODES_INSTALL_DIR;" + "$p = [Environment]::GetEnvironmentVariable('Path','User'); if (-not $p) { $p = '' };" + "$parts = @($p.Split(';') | Where-Object { $_ -ne '' });" + "if ($parts -notcontains $d) { [Environment]::SetEnvironmentVariable('Path', (@($parts + $d) -join ';'), 'User') }";
224903
225334
  try {
224904
- execFileSync5("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], { stdio: "ignore", env: { ...process.env, LECODES_INSTALL_DIR: dir } });
225335
+ execFileSync7("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], { stdio: "ignore", env: { ...process.env, LECODES_INSTALL_DIR: dir } });
224905
225336
  return true;
224906
225337
  } catch {
224907
225338
  return false;
@@ -224910,10 +225341,85 @@ var addToUserPathWindows = (dir) => {
224910
225341
 
224911
225342
  // src/index.ts
224912
225343
  init_util();
225344
+
225345
+ // src/projectEnv.ts
225346
+ import { existsSync as existsSync37, readFileSync as readFileSync33 } from "node:fs";
225347
+ import { homedir as homedir10 } from "node:os";
225348
+ import { dirname as dirname20, join as join38, resolve as resolve22 } from "node:path";
225349
+ var KEY_RE = /^(LECODES|CREATOR)_[A-Z0-9_]+$/;
225350
+ var parseDotEnv = (text) => {
225351
+ const out = {};
225352
+ for (const raw of text.split(/\r?\n/)) {
225353
+ const line = raw.trim();
225354
+ if (!line || line.startsWith("#"))
225355
+ continue;
225356
+ const m = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
225357
+ if (!m)
225358
+ continue;
225359
+ const key = m[1];
225360
+ let value = m[2];
225361
+ if (value.startsWith('"')) {
225362
+ const end = value.indexOf('"', 1);
225363
+ value = (end < 0 ? value.slice(1) : value.slice(1, end)).replace(/\\n/g, `
225364
+ `).replace(/\\t/g, "\t").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
225365
+ } else if (value.startsWith("'")) {
225366
+ const end = value.indexOf("'", 1);
225367
+ value = end < 0 ? value.slice(1) : value.slice(1, end);
225368
+ } else {
225369
+ const hash = value.search(/\s#/);
225370
+ value = (hash >= 0 ? value.slice(0, hash) : value).trim();
225371
+ }
225372
+ out[key] = value;
225373
+ }
225374
+ return out;
225375
+ };
225376
+ var applyDotEnv = (parsed, dir, env) => {
225377
+ const out = {};
225378
+ for (const [key, raw] of Object.entries(parsed)) {
225379
+ if (!KEY_RE.test(key) || env[key] !== undefined)
225380
+ continue;
225381
+ let value = raw;
225382
+ if (value.startsWith("./") || value.startsWith("../"))
225383
+ value = resolve22(dir, value);
225384
+ else if (value === "~" || value.startsWith("~/"))
225385
+ value = join38(homedir10(), value.slice(1));
225386
+ out[key] = value;
225387
+ }
225388
+ return out;
225389
+ };
225390
+ var findEnvRoot = (start) => {
225391
+ let dir = resolve22(start);
225392
+ while (true) {
225393
+ if (existsSync37(join38(dir, "app.json")) || existsSync37(join38(dir, ".lecodes")))
225394
+ return dir;
225395
+ const parent = dirname20(dir);
225396
+ if (parent === dir)
225397
+ return null;
225398
+ dir = parent;
225399
+ }
225400
+ };
225401
+ var loadProjectEnv = (cwd = process.cwd()) => {
225402
+ try {
225403
+ const root = findEnvRoot(cwd);
225404
+ if (!root)
225405
+ return [];
225406
+ const file = join38(root, ".env");
225407
+ if (!existsSync37(file))
225408
+ return [];
225409
+ const added = applyDotEnv(parseDotEnv(readFileSync33(file, "utf8")), root, process.env);
225410
+ for (const [k, v] of Object.entries(added))
225411
+ process.env[k] = v;
225412
+ return Object.keys(added);
225413
+ } catch {
225414
+ return [];
225415
+ }
225416
+ };
225417
+
225418
+ // src/index.ts
224913
225419
  var STANDALONE = (() => {
224914
- const dir = dirname20(process.execPath);
225420
+ const dir = dirname21(process.execPath);
224915
225421
  const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
224916
- return existsSync36(join37(dir, "runtime")) && existsSync36(join37(dir, chisel));
225422
+ return existsSync38(join39(dir, "runtime")) && existsSync38(join39(dir, chisel));
224917
225423
  })();
224918
225424
  var INSTALLED = STANDALONE && (() => {
224919
225425
  const exe = process.platform === "win32" ? "lecodes.exe" : "lecodes";
@@ -224928,8 +225434,8 @@ var INSTALLED = STANDALONE && (() => {
224928
225434
  if (!d)
224929
225435
  return false;
224930
225436
  try {
224931
- const cand = join37(d, exe);
224932
- return existsSync36(cand) && realpathSync2(cand) === self2;
225437
+ const cand = join39(d, exe);
225438
+ return existsSync38(cand) && realpathSync2(cand) === self2;
224933
225439
  } catch {
224934
225440
  return false;
224935
225441
  }
@@ -225133,10 +225639,20 @@ ${c.bold("Commands:")}
225133
225639
  it) + app.ico from app.json "icon". Duplicate asset
225134
225640
  basenames are a build error (they silently alias in
225135
225641
  the bundle). Also spelled 'lecodes app build desktop'.
225642
+ On macOS: a signed .app instead (the downloaded LeCodes
225643
+ Desktop.app + the project inside it); app.json
225644
+ desktop.macos = bundleId / buildNumber / category /
225645
+ signing.identity / notarize.profile (a notarytool
225646
+ keychain profile — notarized + stapled, so it opens
225647
+ on any Mac without "Open Anyway").
225136
225648
  -o <dir> output folder (default dist/<name>)
225137
225649
  --slug <name> folder/zip name override (Latin-only)
225138
225650
  --renderer <gl|vulkan> host + shader variant
225139
225651
  --probe headless-render the staged folder to PNG
225652
+ (macOS: a brief windowed run + snapshot)
225653
+ --zip also pack the result for sending
225654
+ --sign <identity|adhoc> macOS codesign identity
225655
+ --notarize <profile> / --no-notarize macOS
225140
225656
  --zip <out>.zip, UTF-8/forward-slash entries
225141
225657
  update move to the newest published release
225142
225658
  (cached in ~/.lecodes/renderer)
@@ -225251,7 +225767,10 @@ ${c.bold("Commands:")}
225251
225767
  pn keys [new|revoke] Push keys for YOUR server: list, mint (owner only, shown once
225252
225768
  — 'new --env' banks it in .env), revoke <id>
225253
225769
 
225254
- Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKEN).`;
225770
+ Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKEN).
225771
+ A .env next to app.json is read at startup for LECODES_* / CREATOR_* keys (the shell's own
225772
+ environment wins; ./ paths resolve against the project; the file is never pushed) — e.g.
225773
+ LECODES_DESKTOP_EXE, LECODES_MATC, LECODES_NOTARY_PROFILE, LECODES_MACOS_SIGN_ID, LECODES_TOKEN.`;
225255
225774
  var commands = { init: init2, login, create, clone: clone3, link, status: status3, diff: diff2, pull, push, compile: compile2, preview: previewLegacy, render, test, types: types4, design, scene, dev, app, pn, desktop, shaders, assets, lightmap };
225256
225775
  if (STANDALONE)
225257
225776
  commands.install = install;
@@ -225265,15 +225784,15 @@ var OPTIONAL_PEERS = [
225265
225784
  var peerVersion = (name, probe) => {
225266
225785
  try {
225267
225786
  const require2 = createRequire4(import.meta.url);
225268
- let dir = dirname20(require2.resolve(probe));
225787
+ let dir = dirname21(require2.resolve(probe));
225269
225788
  for (let depth = 0;depth < 16; depth++) {
225270
- const manifest = join37(dir, "package.json");
225271
- if (existsSync36(manifest)) {
225272
- const parsed = JSON.parse(readFileSync32(manifest, "utf8"));
225789
+ const manifest = join39(dir, "package.json");
225790
+ if (existsSync38(manifest)) {
225791
+ const parsed = JSON.parse(readFileSync34(manifest, "utf8"));
225273
225792
  if (parsed.name === name)
225274
225793
  return parsed.version ?? "unknown";
225275
225794
  }
225276
- const parent = dirname20(dir);
225795
+ const parent = dirname21(dir);
225277
225796
  if (parent === dir)
225278
225797
  break;
225279
225798
  dir = parent;
@@ -225298,6 +225817,7 @@ var printVersion = () => {
225298
225817
  var main = async () => {
225299
225818
  const argv = process.argv.slice(2);
225300
225819
  const command = argv[0];
225820
+ loadProjectEnv();
225301
225821
  if (!command || command === "help" || command === "--help" || command === "-h") {
225302
225822
  log(HELP);
225303
225823
  return;