lecodes-cli 2.2.11 → 2.2.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -64,9 +64,9 @@ var package_default;
64
64
  var init_package = __esm(() => {
65
65
  package_default = {
66
66
  name: "lecodes-cli",
67
- version: "2.2.11",
67
+ version: "2.2.14",
68
68
  dependencies: {
69
- "@letary/chisel": "^0.14.0",
69
+ "@letary/chisel": "^0.15.0",
70
70
  jimp: "^1.6.1",
71
71
  "lecodes-sdk": "workspace:*"
72
72
  },
@@ -88,7 +88,7 @@ var init_package = __esm(() => {
88
88
  },
89
89
  peerDependencies: {
90
90
  "lecodes-design": "^0.11.0",
91
- "lecodes-headless": "^2.2.11",
91
+ "lecodes-headless": "^2.2.14",
92
92
  "lecodes-3d-editor": "^0.9.0",
93
93
  "lecodes-assets": "^0.2.1",
94
94
  "heic-decode": "^2.1.0",
@@ -158,10 +158,10 @@ var init_package = __esm(() => {
158
158
  });
159
159
 
160
160
  // ../../sdk/src/version.ts
161
- var SDK_VERSION = "2.2.11";
161
+ var SDK_VERSION = "2.2.14";
162
162
 
163
163
  // src/cli/args.ts
164
- var isNegativeNumber = (s) => /^-(\d|\.\d)/.test(s), isValue = (s) => !s.startsWith("-") || isNegativeNumber(s), parseArgv = (argv) => {
164
+ var isNegativeNumber = (s) => /^-(\d|\.\d)/.test(s), isValue = (s) => !s.startsWith("-") || isNegativeNumber(s), parseArgv = (argv, booleans = new Set) => {
165
165
  const out = { positional: [], flags: {} };
166
166
  for (let i = 0;i < argv.length; i++) {
167
167
  const arg = argv[i];
@@ -183,7 +183,13 @@ var isNegativeNumber = (s) => /^-(\d|\.\d)/.test(s), isValue = (s) => !s.startsW
183
183
  }
184
184
  const key = arg.slice(long ? 2 : 1);
185
185
  const next = argv[i + 1];
186
- if (next !== undefined && isValue(next)) {
186
+ if (booleans.has(key)) {
187
+ if (next === "true" || next === "false") {
188
+ out.flags[key] = next;
189
+ i++;
190
+ } else
191
+ out.flags[key] = true;
192
+ } else if (next !== undefined && isValue(next)) {
187
193
  out.flags[key] = next;
188
194
  i++;
189
195
  } else
@@ -243,6 +249,15 @@ var str = (help, opts) => ({ type: "string", help, ...opts }), num = (help, opts
243
249
  out[name] = present;
244
250
  }
245
251
  return out;
252
+ }, booleanFlagNames = (spec) => {
253
+ const names = new Set;
254
+ for (const [name, s] of Object.entries(spec))
255
+ if (s.type === "boolean") {
256
+ names.add(name);
257
+ if (s.alias)
258
+ names.add(s.alias);
259
+ }
260
+ return names;
246
261
  }, unknownFlags = (spec, raw) => {
247
262
  const known = new Set;
248
263
  for (const [name, s] of Object.entries(spec)) {
@@ -498,8 +513,8 @@ var all = (groups) => groups.flatMap((g) => g.commands), closest = (name, cmds)
498
513
  process.exitCode = 1;
499
514
  return;
500
515
  }
501
- const parsed = parseArgv(rest);
502
516
  const spec = node.flags ?? {};
517
+ const parsed = parseArgv(rest, booleanFlagNames(spec));
503
518
  if (!node.passthrough) {
504
519
  const unknown = unknownFlags(spec, parsed.flags);
505
520
  if (unknown.length)
@@ -525,6 +540,7 @@ var all = (groups) => groups.flatMap((g) => g.commands), closest = (name, cmds)
525
540
  process.exitCode = 1;
526
541
  };
527
542
  var init_run = __esm(() => {
543
+ init_command();
528
544
  init_args();
529
545
  init_command();
530
546
  init_errors();
@@ -590,7 +606,7 @@ var errorMessage = (body, status) => {
590
606
  return b.message;
591
607
  }
592
608
  return `Request failed with status ${status}`;
593
- }, apiRequest = async (apiUrl, path, opts = {}) => {
609
+ }, ApiError, apiRequest = async (apiUrl, path, opts = {}) => {
594
610
  const headers = {};
595
611
  const isForm = opts.body instanceof FormData;
596
612
  if (opts.token)
@@ -610,13 +626,64 @@ var errorMessage = (body, status) => {
610
626
  const contentType = resp.headers.get("content-type") ?? "";
611
627
  const parsed = contentType.startsWith("application/json") ? await resp.json() : contentType.startsWith("text/") ? await resp.text() : null;
612
628
  if (resp.status >= 400)
613
- throw new CliError(errorMessage(parsed, resp.status), resp.status);
629
+ throw new ApiError(errorMessage(parsed, resp.status), resp.status, parsed);
614
630
  return parsed;
615
631
  }, getAccount = (apiUrl, token) => apiRequest(apiUrl, "/account", { token }), getProject = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}`, { token }), createProject = (apiUrl, token, name, teamUuid) => apiRequest(apiUrl, "/projects", { token, body: { name, teamUuid } }), getCommits = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}/commits`, { token }), getTypes = (apiUrl) => apiRequest(apiUrl, "/types"), uploadBlob = (apiUrl, token, uuid, name, bytes) => {
616
632
  const form = new FormData;
617
633
  form.append("file", new Blob([bytes]), name);
618
634
  return apiRequest(apiUrl, `/projects/${uuid}/blobs`, { token, body: form });
619
- }, sync = (apiUrl, token, uuid, payload) => apiRequest(apiUrl, `/projects/${uuid}/sync`, { token, body: payload }), publishProject = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}/publish`, { token, body: {} }), enableDesignShare = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}/design/share`, { token, body: {} }), disableDesignShare = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}/design/share`, { token, method: "DELETE" }), setDesignCommentsOpen = (apiUrl, token, uuid, open) => apiRequest(apiUrl, `/projects/${uuid}/design/comments/open`, { token, body: { open } }), listDesignComments = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}/design/comments`, { token }), createDesignComment = (apiUrl, token, uuid, input) => apiRequest(apiUrl, `/projects/${uuid}/design/comments`, { token, body: input }), replyDesignComment = (apiUrl, token, uuid, threadId, body, origin) => apiRequest(apiUrl, `/projects/${uuid}/design/comments/${threadId}/messages`, { token, body: { body, origin } }), resolveDesignComment = (apiUrl, token, uuid, threadId, resolved) => apiRequest(apiUrl, `/projects/${uuid}/design/comments/${threadId}`, { token, method: "PATCH", body: { resolved } }), sendPush = (apiUrl, token, uuid, payload) => apiRequest(apiUrl, `/projects/${uuid}/push/send`, { token, body: payload }), listPushDevices = (apiUrl, token, uuid, user) => apiRequest(apiUrl, `/projects/${uuid}/push/registrations${user ? `?user=${encodeURIComponent(user)}` : ""}`, { token }), forgetPushDevice = (apiUrl, token, uuid, address) => apiRequest(apiUrl, `/projects/${uuid}/push/registrations/${encodeURIComponent(address)}`, { token, method: "DELETE" }), listPushKeys = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}/push/keys`, { token }), createPushKey = (apiUrl, token, uuid, name) => apiRequest(apiUrl, `/projects/${uuid}/push/keys`, { token, body: { name } }), revokePushKey = (apiUrl, token, uuid, id) => apiRequest(apiUrl, `/projects/${uuid}/push/keys/${id}`, { token, method: "DELETE" }), downloadBlob = async (apiUrl, src) => {
635
+ }, sync = (apiUrl, token, uuid, payload) => apiRequest(apiUrl, `/projects/${uuid}/sync`, { token, body: payload }), SchemaChangeError, opText = (op) => {
636
+ if (typeof op === "string")
637
+ return op;
638
+ if (op && typeof op === "object" && typeof op.text === "string")
639
+ return op.text;
640
+ return JSON.stringify(op);
641
+ }, schemaChangeOf = (e) => {
642
+ if (!(e instanceof ApiError) || e.status !== 409 || !e.body || typeof e.body !== "object")
643
+ return e;
644
+ const body = e.body;
645
+ const destructive = Array.isArray(body.destructive) ? body.destructive.map(opText) : null;
646
+ const incompatible = typeof body.incompatible === "string" ? body.incompatible : body.incompatible === true ? "" : null;
647
+ if (!destructive && incompatible === null)
648
+ return e;
649
+ return new SchemaChangeError(typeof body.error === "string" ? body.error : "The schema change needs a confirmation", destructive ?? [], incompatible);
650
+ }, releaseFailure = (e, opts) => {
651
+ const schema = schemaChangeOf(e);
652
+ if (schema !== e || !(e instanceof ApiError))
653
+ return schema;
654
+ if (opts.build && e.status === 400)
655
+ return new CliError(`Build failed:
656
+ ${e.message}`, 400);
657
+ if (e.status === 502 || e.status === 503) {
658
+ const detail = e.message.trimStart().startsWith("<") ? "" : `: ${e.message.trim()}`;
659
+ const service = opts.build ? "The platform's build service is unavailable" : "The platform could not reach the app backend";
660
+ return new CliError(`${service} (${e.status})${detail}. Nothing changed — try again in a minute.`, e.status);
661
+ }
662
+ return e;
663
+ }, publishProject = async (apiUrl, token, uuid, opts = {}) => {
664
+ try {
665
+ const body = { ...opts.allowDestructive ? { allowDestructive: true } : {}, ...opts.force ? { force: true } : {} };
666
+ return await apiRequest(apiUrl, `/projects/${uuid}/publish`, { token, body });
667
+ } catch (e) {
668
+ throw releaseFailure(e, { build: true });
669
+ }
670
+ }, listReleases = async (apiUrl, token, uuid) => (await apiRequest(apiUrl, `/projects/${uuid}/publish`, { token })).releases, useRelease = async (apiUrl, token, uuid, version, opts = {}) => {
671
+ try {
672
+ return await apiRequest(apiUrl, `/projects/${uuid}/releases/${encodeURIComponent(version)}/use`, { token, body: opts.allowDestructive ? { allowDestructive: true } : {} });
673
+ } catch (e) {
674
+ throw releaseFailure(e, { build: false });
675
+ }
676
+ }, uploadReleaseBundle = async (apiUrl, token, uuid, bundle) => {
677
+ const form = new FormData;
678
+ form.append("app", new Blob([bundle.code], { type: "text/javascript" }), "app.js");
679
+ if (bundle.map !== null)
680
+ form.append("map", new Blob([bundle.map], { type: "application/json" }), "app.js.map");
681
+ try {
682
+ return await apiRequest(apiUrl, `/projects/${uuid}/releases/upload`, { token, body: form });
683
+ } catch (e) {
684
+ throw releaseFailure(e, { build: false });
685
+ }
686
+ }, enableDesignShare = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}/design/share`, { token, body: {} }), disableDesignShare = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}/design/share`, { token, method: "DELETE" }), setDesignCommentsOpen = (apiUrl, token, uuid, open) => apiRequest(apiUrl, `/projects/${uuid}/design/comments/open`, { token, body: { open } }), listDesignComments = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}/design/comments`, { token }), createDesignComment = (apiUrl, token, uuid, input) => apiRequest(apiUrl, `/projects/${uuid}/design/comments`, { token, body: input }), replyDesignComment = (apiUrl, token, uuid, threadId, body, origin) => apiRequest(apiUrl, `/projects/${uuid}/design/comments/${threadId}/messages`, { token, body: { body, origin } }), resolveDesignComment = (apiUrl, token, uuid, threadId, resolved) => apiRequest(apiUrl, `/projects/${uuid}/design/comments/${threadId}`, { token, method: "PATCH", body: { resolved } }), sendPush = (apiUrl, token, uuid, payload) => apiRequest(apiUrl, `/projects/${uuid}/push/send`, { token, body: payload }), listPushDevices = (apiUrl, token, uuid, user) => apiRequest(apiUrl, `/projects/${uuid}/push/registrations${user ? `?user=${encodeURIComponent(user)}` : ""}`, { token }), forgetPushDevice = (apiUrl, token, uuid, address) => apiRequest(apiUrl, `/projects/${uuid}/push/registrations/${encodeURIComponent(address)}`, { token, method: "DELETE" }), listPushKeys = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}/push/keys`, { token }), createPushKey = (apiUrl, token, uuid, name) => apiRequest(apiUrl, `/projects/${uuid}/push/keys`, { token, body: { name } }), revokePushKey = (apiUrl, token, uuid, id) => apiRequest(apiUrl, `/projects/${uuid}/push/keys/${id}`, { token, method: "DELETE" }), downloadBlob = async (apiUrl, src) => {
620
687
  const resp = await fetch(`${apiUrl}${src}`);
621
688
  if (!resp.ok)
622
689
  throw new CliError(`Failed to download ${src} (status ${resp.status})`);
@@ -624,6 +691,22 @@ var errorMessage = (body, status) => {
624
691
  };
625
692
  var init_api = __esm(() => {
626
693
  init_cli();
694
+ ApiError = class ApiError extends CliError {
695
+ body;
696
+ constructor(message, status, body) {
697
+ super(message, status);
698
+ this.body = body;
699
+ }
700
+ };
701
+ SchemaChangeError = class SchemaChangeError extends CliError {
702
+ destructive;
703
+ incompatible;
704
+ constructor(message, destructive, incompatible) {
705
+ super(message, 409);
706
+ this.destructive = destructive;
707
+ this.incompatible = incompatible;
708
+ }
709
+ };
627
710
  });
628
711
 
629
712
  // ../../sdk/src/compile/assetName.ts
@@ -170224,7 +170307,7 @@ var normalizePath2 = (path) => {
170224
170307
  continue;
170225
170308
  const exported = new Map(checker.getExportsOfModule(modSym).map((s) => [s.name, s]));
170226
170309
  for (const ex of exports) {
170227
- if (ex.kind === "other")
170310
+ if (ex.kind === "other" || ex.kind === "push")
170228
170311
  continue;
170229
170312
  const id = `${serverModuleId(path)}#${ex.name}`;
170230
170313
  let sym = exported.get(ex.name);
@@ -170545,6 +170628,8 @@ var SERVER_FILE, isServerPath = (path) => SERVER_FILE.test(path), serverModuleId
170545
170628
  const s = init.trimStart();
170546
170629
  if (/^channel\s*[<(]/.test(s))
170547
170630
  return "channel";
170631
+ if (/^pushChannel\s*[<(]/.test(s))
170632
+ return "push";
170548
170633
  if (/^async\s+function\b/.test(s))
170549
170634
  return "fn";
170550
170635
  if (/^async\b/.test(s))
@@ -170588,7 +170673,7 @@ var SERVER_FILE, isServerPath = (path) => SERVER_FILE.test(path), serverModuleId
170588
170673
  }
170589
170674
  }
170590
170675
  return found.sort((a, b) => a.at - b.at).map(({ name, kind }) => ({ name, kind }));
170591
- }, serverStubSource = (path, exports, serverUrl) => {
170676
+ }, serverStubSource = (path, exports, serverUrl, push = exports.some((e) => e.kind === "push")) => {
170592
170677
  const mod = serverModuleId(path);
170593
170678
  const url = JSON.stringify(serverUrl);
170594
170679
  const lines = exports.map(({ name, kind }) => {
@@ -170597,8 +170682,12 @@ var SERVER_FILE, isServerPath = (path) => SERVER_FILE.test(path), serverModuleId
170597
170682
  return `export const ${name} = __rpc(${url}, ${id})`;
170598
170683
  if (kind === "channel")
170599
170684
  return `export const ${name} = __channel(${url}, ${id})`;
170685
+ if (kind === "push")
170686
+ return `export const ${name} = __pushChannel(${url}, ${id})`;
170600
170687
  return `export const ${name} = __serverOnly(${id})`;
170601
170688
  });
170689
+ if (push)
170690
+ lines.unshift("__pushEnable()");
170602
170691
  return `// client stub of ${mod} — generated by the lecodes compiler
170603
170692
  ${lines.join(`
170604
170693
  `)}
@@ -170606,9 +170695,9 @@ ${lines.join(`
170606
170695
  }, ClientPublishError, clientPublishes = (entries) => {
170607
170696
  const channels = new Map;
170608
170697
  for (const [path, exports] of Object.entries(scanServerModules(entries))) {
170609
- const names = exports.filter((e) => e.kind === "channel").map((e) => e.name);
170610
- if (names.length)
170611
- channels.set(path.replace(/\.[tj]s$/, ""), new Set(names));
170698
+ const names = new Map(exports.filter((e) => e.kind === "channel" || e.kind === "push").map((e) => [e.name, e.kind === "push" ? "send" : "publish"]));
170699
+ if (names.size)
170700
+ channels.set(path.replace(/\.[tj]s$/, ""), names);
170612
170701
  }
170613
170702
  if (channels.size === 0)
170614
170703
  return [];
@@ -170627,17 +170716,18 @@ ${lines.join(`
170627
170716
  continue;
170628
170717
  for (const part of m[1].split(",")) {
170629
170718
  const [imported, local] = part.split(/\s+as\s+/).map((s) => s.trim());
170630
- if (imported && names.has(imported))
170631
- locals.push(local ?? imported);
170719
+ const verb = imported ? names.get(imported) : undefined;
170720
+ if (verb)
170721
+ locals.push([local ?? imported, verb]);
170632
170722
  }
170633
170723
  }
170634
170724
  if (!locals.length)
170635
170725
  continue;
170636
170726
  const code = blankCommentsAndStrings(src);
170637
- for (const local of locals) {
170638
- for (const m of code.matchAll(new RegExp(`(?<![\\w$.])${local.replace(/\$/g, "\\$")}\\s*\\.\\s*publish\\s*\\(`, "g"))) {
170727
+ for (const [local, verb] of locals) {
170728
+ for (const m of code.matchAll(new RegExp(`(?<![\\w$.])${local.replace(/\$/g, "\\$")}\\s*\\.\\s*${verb}\\s*\\(`, "g"))) {
170639
170729
  out.push(`${serverModuleId(e.path)}:${code.slice(0, m.index).split(`
170640
- `).length}: ${local}.publish(…)`);
170730
+ `).length}: ${local}.${verb}(…)`);
170641
170731
  }
170642
170732
  }
170643
170733
  }
@@ -170677,7 +170767,11 @@ ${lines.join(`
170677
170767
  out[normalizePath3(e.path)] = scanServerExports(e.text ?? "", serverModuleId(e.path));
170678
170768
  }
170679
170769
  return out;
170680
- }, stubServerEntries = (entries, serverUrl) => entries.map((e) => e.type === "text" && isServerPath(e.path) ? { ...e, text: serverStubSource(e.path, scanServerExports(e.text ?? "", serverModuleId(e.path)), serverUrl) } : e), compileServerBundle = async (opts) => {
170770
+ }, stubServerEntries = (entries, serverUrl) => {
170771
+ const modules = scanServerModules(entries);
170772
+ const push = Object.values(modules).some((exports) => exports.some((e) => e.kind === "push"));
170773
+ return entries.map((e) => e.type === "text" && isServerPath(e.path) ? { ...e, text: serverStubSource(e.path, modules[normalizePath3(e.path)] ?? [], serverUrl, push) } : e);
170774
+ }, compileServerBundle = async (opts) => {
170681
170775
  const modules = scanServerModules(opts.entries);
170682
170776
  if (Object.keys(modules).length === 0)
170683
170777
  return null;
@@ -170714,7 +170808,7 @@ var init_serverSplit = __esm(() => {
170714
170808
  ClientPublishError = class ClientPublishError extends Error {
170715
170809
  problems;
170716
170810
  constructor(problems) {
170717
- super(`a channel goes from the server to the app — the app subscribes; to send, call a server function that publishes:
170811
+ super(`a channel goes from the server to the app — the app listens; to send, call a server function that publishes (a push channel: that sends):
170718
170812
  ${problems.join(`
170719
170813
  `)}`);
170720
170814
  this.name = "ClientPublishError";
@@ -171163,7 +171257,7 @@ var init_shaderSchema = __esm(() => {
171163
171257
  });
171164
171258
 
171165
171259
  // ../../sdk/src/compile/header.ts
171166
- var buildHeader = (name, code, opts = {}) => {
171260
+ var ENGINE_SIGNALS, buildHeader = (name, code, opts = {}) => {
171167
171261
  let header = `// sdk: ${SDK_VERSION}
171168
171262
  `;
171169
171263
  header += `// name: ${name.slice(0, 20)}
@@ -171181,6 +171275,10 @@ var buildHeader = (name, code, opts = {}) => {
171181
171275
  `;
171182
171276
  if (code.includes("_creator2d.createScene"))
171183
171277
  header += `// 2d: enable
171278
+ `;
171279
+ for (const [name, re] of ENGINE_SIGNALS)
171280
+ if (re.test(code))
171281
+ header += `// ${name}: enable
171184
171282
  `;
171185
171283
  if (usesAR(code, opts.plugins))
171186
171284
  header += `// ar: enable
@@ -171214,7 +171312,14 @@ var buildHeader = (name, code, opts = {}) => {
171214
171312
  `;
171215
171313
  return header;
171216
171314
  }, usesAR = (code, plugins) => !!plugins?.includes("ar") && code.includes("_creator.createScene");
171217
- var init_header = () => {};
171315
+ var init_header = __esm(() => {
171316
+ ENGINE_SIGNALS = [
171317
+ ["physics", /_creator\.(?:physicsCreateBody|physicsConfigure|physicsRaycast|characterCreate|vehicleCreate|ragdollCreate)\(/],
171318
+ ["net", /\b_creatorNet\b/],
171319
+ ["nav", /_creatorNav\.\w+\(/],
171320
+ ["audio", /_creatorAudio\.(?:loadClip|play)\(/]
171321
+ ];
171322
+ });
171218
171323
 
171219
171324
  // ../../sdk/src/compile/sourcemap.ts
171220
171325
  var offsetSourceMap = (mapJson, lines) => {
@@ -171228,295 +171333,8 @@ var offsetSourceMap = (mapJson, lines) => {
171228
171333
  return `//# lecodesMap=data:application/json;charset=utf-8;base64,${b64}`;
171229
171334
  };
171230
171335
 
171231
- // ../../sdk/src/compile/compileProject.ts
171232
- var localAssetBase = (path) => path.split(/[\\/]/).pop() ?? path, localAssetNames = (entries) => {
171233
- const seen = new Map;
171234
- for (const e of entries)
171235
- if (e.type === "resource") {
171236
- const b = localAssetBase(e.path);
171237
- seen.set(b, (seen.get(b) ?? 0) + 1);
171238
- }
171239
- const names = new Map;
171240
- for (const e of entries)
171241
- if (e.type === "resource") {
171242
- const b = localAssetBase(e.path);
171243
- names.set(e.path, (seen.get(b) ?? 0) > 1 ? e.path.replace(/^[./]+/, "").replace(/[\\/]/g, "__") : b);
171244
- }
171245
- return names;
171246
- }, entriesToFiles = (entries, opts) => {
171247
- const files = {};
171248
- const localNames = localAssetNames(entries);
171249
- const localAssetId = (nameExpr) => `_creatorFetch.local(${nameExpr})`;
171250
- for (const e of entries) {
171251
- if (e.type === "resource") {
171252
- if (opts.localAssets && !e.remote) {
171253
- const name = localNames.get(e.path) ?? localAssetBase(e.path);
171254
- files[e.path] = `export default ("id:" + ${localAssetId(JSON.stringify(name))})`;
171255
- } else {
171256
- files[e.path] = `export default "${opts.publicUrl}${e.fileSrc ?? ""}"`;
171257
- }
171258
- } else if (e.type === "shader") {
171259
- const shaders = e.shaders ?? [];
171260
- if (e.shadersLocal && shaders.length > 0) {
171261
- const name = shaders[0].src.split(/[\/]/).pop() ?? "";
171262
- const [prefix, suffix = ".filamat"] = name.split("_opengl");
171263
- files[e.path] = `export default ("id:" + ${localAssetId(`${JSON.stringify(prefix + "_")} + _creator.backend + ${JSON.stringify(suffix)}`)})`;
171264
- } else if (shaders.length > 0) {
171265
- files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.backend}")}\``;
171266
- } else {
171267
- files[e.path] = `export default ""`;
171268
- }
171269
- } else if (e.path.toLowerCase().endsWith(".svg")) {
171270
- files[e.path] = `export default SvgSource(\`${e.text ?? ""}\`)`;
171271
- } else if (e.path.toLowerCase().endsWith(".json")) {
171272
- files[e.path] = `export default ${e.text?.trim() || "null"}`;
171273
- } else {
171274
- files[e.path] = e.text ?? "";
171275
- }
171276
- }
171277
- return files;
171278
- }, pluginsOfAppJson = (entries) => {
171279
- const app = entries.find((e) => e.path.replace(/^[./\\]+/, "") === "app.json");
171280
- if (!app?.text)
171281
- return [];
171282
- try {
171283
- const plugins = JSON.parse(app.text).plugins;
171284
- return plugins && typeof plugins === "object" && !Array.isArray(plugins) ? Object.keys(plugins) : [];
171285
- } catch {
171286
- return [];
171287
- }
171288
- }, pluginsOfBundle = (mapJson) => {
171289
- const ids = new Set;
171290
- try {
171291
- for (const source of JSON.parse(mapJson).sources ?? []) {
171292
- const m = /\/plugins\/gen\/([^/]+)\/sdk\//.exec(source);
171293
- if (m)
171294
- ids.add(m[1]);
171295
- }
171296
- } catch {}
171297
- return [...ids].sort();
171298
- }, compileProjectWithMap = async (opts) => {
171299
- const published = clientPublishes(opts.entries);
171300
- if (published.length)
171301
- throw new ClientPublishError(published);
171302
- const entries = stubServerEntries(opts.entries, opts.serverUrl ?? "");
171303
- const entry = opts.entryOverride ?? detectEntry(entries, scanModulesChisel).entry;
171304
- if (!entry) {
171305
- throw new Error("Couldn't find an entrypoint (no .ts/.js project file — name yours main.ts). Pass entryOverride.");
171306
- }
171307
- const files = entriesToFiles(entries, opts);
171308
- const { code, map } = await bundleProjectWithMap(entry, files, opts.format ?? "esm", opts.minify, opts.editor, opts.libraries, opts.sourcemap ?? true);
171309
- const plugins = opts.plugins ?? pluginsOfAppJson(entries);
171310
- if (map && opts.onWarning) {
171311
- const missing = pluginsOfBundle(map).filter((id) => !plugins.includes(id));
171312
- if (missing.length) {
171313
- opts.onWarning(`The app uses ${missing.map((id) => `"${id}"`).join(", ")} and app.json does not list ${missing.length === 1 ? "it" : "them"}: a plugin that is not listed is not there — in a shell built from this project and in the browser alike. Add to app.json: "plugins": { ${missing.map((id) => `"${id}": "*"`).join(", ")} }`);
171314
- }
171315
- }
171316
- if (opts.header === false)
171317
- return { code, map, entry };
171318
- const header = buildHeader(opts.name, code, { icon: opts.icon, fonts: opts.fontsHeader, icons: opts.iconsHeader, plugins });
171319
- const headerLines = (header.match(/\n/g) ?? []).length;
171320
- return { code: header + code, map: map ? offsetSourceMap(map, headerLines) : null, entry };
171321
- }, compileProject = async (opts) => {
171322
- const { code, map } = await compileProjectWithMap(opts);
171323
- return map ? `${code}
171324
- ${inlineSourceMapComment(map)}` : code;
171325
- };
171326
- var init_compileProject = __esm(() => {
171327
- init_bundler();
171328
- init_detectEntry();
171329
- init_header();
171330
- init_serverSplit();
171331
- });
171332
-
171333
- // ../../sdk/src/compile/sceneEditor.ts
171334
- var SCENE_HARNESS_DIR = "./.scene-editor", SCENE_ENTRY_PATH = "./.scene-entry.ts", resolveRelative = (fromPath, spec) => {
171335
- const segs = (p) => p.split("/").filter((s) => s !== "" && s !== ".");
171336
- const out = segs(fromPath).slice(0, -1);
171337
- for (const seg of segs(spec)) {
171338
- if (seg === "..")
171339
- out.pop();
171340
- else
171341
- out.push(seg);
171342
- }
171343
- return "./" + out.join("/");
171344
- }, sceneImportsOf = (sceneText, scenePath, projectPaths) => {
171345
- const known = new Set(projectPaths);
171346
- const out = [];
171347
- const re = /^\s*import\s+[A-Za-z_$][\w$]*\s+from\s+["']([^"']+\.scene(?:\.ts)?)["']/gm;
171348
- for (const m of sceneText.matchAll(re)) {
171349
- const spec = m[1];
171350
- if (!spec.startsWith("."))
171351
- continue;
171352
- let p = resolveRelative(scenePath, spec);
171353
- if (!p.endsWith(".ts"))
171354
- p += ".ts";
171355
- if (known.has(p) && !out.includes(p))
171356
- out.push(p);
171357
- }
171358
- return out;
171359
- }, sceneEditorEntries = (scenePath, harness, projectPaths = [], sceneImports = []) => {
171360
- const entries = Object.entries(harness).map(([name, text]) => ({
171361
- path: `${SCENE_HARNESS_DIR}/${name}`,
171362
- type: "text",
171363
- text
171364
- }));
171365
- entries.push({
171366
- path: `${SCENE_HARNESS_DIR}/.edit-mode.ts`,
171367
- type: "text",
171368
- text: `;(globalThis as any).__lecodesSceneEdit = true
171369
- export {}
171370
- `
171371
- });
171372
- const editorImports = projectPaths.filter((p) => p.endsWith(".editor.ts")).map((p) => `import "${p}"
171373
- `).join("");
171374
- const materialPaths = projectPaths.filter((p) => p.endsWith(".material.ts"));
171375
- const materialImports = materialPaths.map((p, i) => `import __mat${i} from "${p}"
171376
- `).join("");
171377
- const materialRegister = materialPaths.length === 0 ? "" : `;(globalThis as any).__lecodesMaterials = { ${materialPaths.map((p, i) => `${JSON.stringify(p)}: __mat${i}`).join(", ")} }
171378
- `;
171379
- const sceneImportLines = sceneImports.map((p, i) => `import __scn${i} from "${p}"
171380
- `).join("");
171381
- const sceneRegister = sceneImports.length === 0 ? "" : `;(globalThis as any).__lecodesSceneFiles = { ${sceneImports.map((p, i) => `${JSON.stringify(p)}: __scn${i}`).join(", ")} }
171382
- `;
171383
- entries.push({
171384
- path: SCENE_ENTRY_PATH,
171385
- type: "text",
171386
- text: `import "${SCENE_HARNESS_DIR}/.edit-mode.ts"
171387
- ` + materialImports + sceneImportLines + `import "${scenePath}"
171388
- ` + editorImports + `import "${SCENE_HARNESS_DIR}/main.ts"
171389
- ` + materialRegister + sceneRegister
171390
- });
171391
- return entries;
171392
- };
171393
-
171394
- // ../../sdk/src/compile/fontRegistry.ts
171395
- var DEFAULT_FONT_REGISTRY_BASE = "https://cdn.le.codes/fonts/v1", fontRegistryBase = () => typeof process !== "undefined" && process.env?.LECODES_FONT_REGISTRY?.trim() || DEFAULT_FONT_REGISTRY_BASE, faceFileName = (id, weight, italic, ext) => `${id}-${weight}${italic ? "-italic" : ""}.${ext}`, LATIN, CYR, OFL = "OFL-1.1", f = (family, category, scripts, weights, italics, defaultWeights) => ({ family, category, scripts, weights, italics, defaultWeights, license: OFL }), FONT_REGISTRY;
171396
- var init_fontRegistry = __esm(() => {
171397
- LATIN = ["latin"];
171398
- CYR = ["latin", "cyrillic"];
171399
- FONT_REGISTRY = {
171400
- inter: f("Inter", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700, 800, 900], [], [400, 500, 700]),
171401
- manrope: f("Manrope", "sans-serif", CYR, [200, 300, 400, 500, 600, 700, 800], [], [400, 500, 700]),
171402
- "golos-text": f("Golos Text", "sans-serif", CYR, [400, 500, 600, 700, 800, 900], [], [400, 500, 700]),
171403
- onest: f("Onest", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700, 800, 900], [], [400, 500, 700]),
171404
- rubik: f("Rubik", "sans-serif", CYR, [300, 400, 500, 600, 700, 800, 900], [300, 400, 500, 600, 700, 800, 900], [400, 500, 700]),
171405
- nunito: f("Nunito", "sans-serif", CYR, [200, 300, 400, 500, 600, 700, 800, 900], [400, 700], [400, 600, 700]),
171406
- montserrat: f("Montserrat", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700, 800, 900], [400, 700], [400, 500, 700]),
171407
- "ibm-plex-sans": f("IBM Plex Sans", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700], [400, 700], [400, 500, 700]),
171408
- "pt-sans": f("PT Sans", "sans-serif", CYR, [400, 700], [400, 700], [400, 700]),
171409
- jost: f("Jost", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700, 800, 900], [400, 700], [400, 500, 700]),
171410
- "exo-2": f("Exo 2", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700, 800, 900], [400, 700], [400, 500, 700]),
171411
- unbounded: f("Unbounded", "display", CYR, [200, 300, 400, 500, 600, 700, 800, 900], [], [400, 700]),
171412
- "russo-one": f("Russo One", "display", CYR, [400], [], [400]),
171413
- oswald: f("Oswald", "display", CYR, [200, 300, 400, 500, 600, 700], [], [400, 700]),
171414
- comfortaa: f("Comfortaa", "display", CYR, [300, 400, 500, 600, 700], [], [400, 700]),
171415
- "space-grotesk": f("Space Grotesk", "display", LATIN, [300, 400, 500, 600, 700], [], [400, 700]),
171416
- "playfair-display": f("Playfair Display", "serif", CYR, [400, 500, 600, 700, 800, 900], [400, 700], [400, 700]),
171417
- lora: f("Lora", "serif", CYR, [400, 500, 600, 700], [400, 700], [400, 700]),
171418
- "pt-serif": f("PT Serif", "serif", CYR, [400, 700], [400, 700], [400, 700]),
171419
- cormorant: f("Cormorant", "serif", CYR, [300, 400, 500, 600, 700], [400, 700], [400, 700]),
171420
- literata: f("Literata", "serif", CYR, [200, 300, 400, 500, 600, 700, 800, 900], [400, 700], [400, 700]),
171421
- "jetbrains-mono": f("JetBrains Mono", "monospace", CYR, [100, 200, 300, 400, 500, 600, 700, 800], [400, 700], [400, 700]),
171422
- "fira-code": f("Fira Code", "monospace", CYR, [300, 400, 500, 600, 700], [], [400, 700]),
171423
- "ibm-plex-mono": f("IBM Plex Mono", "monospace", CYR, [100, 200, 300, 400, 500, 600, 700], [400, 700], [400, 700]),
171424
- caveat: f("Caveat", "handwriting", CYR, [400, 500, 600, 700], [], [400, 700]),
171425
- pacifico: f("Pacifico", "handwriting", CYR, [400], [], [400])
171426
- };
171427
- });
171428
-
171429
- // ../../sdk/src/compile/sfnt.ts
171430
- var TAG_TTF = 65536, TAG_OTTO = 1330926671, TAG_TTCF = 1953784678, TAG_WOFF = 2001684038, TAG_WOF2 = 2001684018, parseSfnt = (bytes) => {
171431
- const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
171432
- if (bytes.byteLength < 12)
171433
- throw new Error("not a font file (truncated)");
171434
- const version = dv.getUint32(0);
171435
- if (version === TAG_TTCF)
171436
- throw new Error("font collections (.ttc) aren't supported — export a single face");
171437
- if (version === TAG_WOFF || version === TAG_WOF2)
171438
- throw new Error("WOFF/WOFF2 project fonts aren't supported — use the .ttf/.otf");
171439
- if (version !== TAG_TTF && version !== TAG_OTTO)
171440
- throw new Error("not a TTF/OTF font file");
171441
- const numTables = dv.getUint16(4);
171442
- const tables = new Map;
171443
- for (let i = 0;i < numTables; i++) {
171444
- const rec = 12 + i * 16;
171445
- if (rec + 16 > bytes.byteLength)
171446
- throw new Error("not a font file (truncated table directory)");
171447
- const tag = String.fromCharCode(bytes[rec], bytes[rec + 1], bytes[rec + 2], bytes[rec + 3]);
171448
- tables.set(tag, { offset: dv.getUint32(rec + 8), length: dv.getUint32(rec + 12) });
171449
- }
171450
- const family = readFamily(dv, bytes, tables.get("name"));
171451
- if (!family)
171452
- throw new Error("no family name in the font's `name` table");
171453
- let weight = 400;
171454
- let italic = false;
171455
- const os2 = tables.get("OS/2");
171456
- if (os2 && os2.offset + 64 <= bytes.byteLength) {
171457
- weight = dv.getUint16(os2.offset + 4);
171458
- italic = (dv.getUint16(os2.offset + 62) & 1) === 1;
171459
- } else {
171460
- const head = tables.get("head");
171461
- if (head && head.offset + 46 <= bytes.byteLength) {
171462
- const macStyle = dv.getUint16(head.offset + 44);
171463
- if (macStyle & 1)
171464
- weight = 700;
171465
- italic = (macStyle & 2) === 2;
171466
- }
171467
- }
171468
- if (weight < 1 || weight > 1000)
171469
- weight = 400;
171470
- return { family, weight, italic };
171471
- }, readFamily = (dv, bytes, name) => {
171472
- if (!name || name.offset + 6 > bytes.byteLength)
171473
- return "";
171474
- const base = name.offset;
171475
- const count = dv.getUint16(base + 2);
171476
- const stringOffset = base + dv.getUint16(base + 4);
171477
- let best = "";
171478
- let bestScore = -1;
171479
- for (let i = 0;i < count; i++) {
171480
- const rec = base + 6 + i * 12;
171481
- if (rec + 12 > bytes.byteLength)
171482
- break;
171483
- const platformID = dv.getUint16(rec);
171484
- const languageID = dv.getUint16(rec + 4);
171485
- const nameID = dv.getUint16(rec + 6);
171486
- const length = dv.getUint16(rec + 8);
171487
- const offset = stringOffset + dv.getUint16(rec + 10);
171488
- if (nameID !== 16 && nameID !== 1)
171489
- continue;
171490
- if (offset + length > bytes.byteLength)
171491
- continue;
171492
- let score = nameID === 16 ? 100 : 0;
171493
- if (platformID === 3)
171494
- score += languageID === 1033 ? 30 : 20;
171495
- else if (platformID === 0)
171496
- score += 10;
171497
- if (score <= bestScore)
171498
- continue;
171499
- const text = platformID === 1 ? decodeLatin1(bytes, offset, length) : decodeUtf16be(bytes, offset, length);
171500
- if (!text.trim())
171501
- continue;
171502
- best = text.trim();
171503
- bestScore = score;
171504
- }
171505
- return best;
171506
- }, decodeUtf16be = (bytes, offset, length) => {
171507
- let out = "";
171508
- for (let i = 0;i + 1 < length; i += 2)
171509
- out += String.fromCharCode(bytes[offset + i] << 8 | bytes[offset + i + 1]);
171510
- return out;
171511
- }, decodeLatin1 = (bytes, offset, length) => {
171512
- let out = "";
171513
- for (let i = 0;i < length; i++)
171514
- out += String.fromCharCode(bytes[offset + i]);
171515
- return out;
171516
- };
171517
-
171518
- // ../../sdk/src/compile/fontMacro.ts
171519
- var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c) => c >= "A" && c <= "Z" || c >= "a" && c <= "z" || c === "_" || c === "$", isIdentPart = (c) => isIdentStart(c) || c >= "0" && c <= "9", skipString2 = (src, start) => {
171336
+ // ../../sdk/src/compile/assetIconMacro.ts
171337
+ var ICON_ID, isIdentStart = (c) => c >= "A" && c <= "Z" || c >= "a" && c <= "z" || c === "_" || c === "$", isIdentPart = (c) => isIdentStart(c) || c >= "0" && c <= "9", skipString2 = (src, start) => {
171520
171338
  const quote = src[start];
171521
171339
  const n = src.length;
171522
171340
  let i = start + 1;
@@ -171626,12 +171444,7 @@ var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c) => c >= "A" && c <= "Z
171626
171444
  if (inner.includes(q) && !inner.includes("\\" + q))
171627
171445
  return null;
171628
171446
  return inner.replace(/\\(.)/g, "$1");
171629
- }, prevNonSpace = (src, i) => {
171630
- let k = i - 1;
171631
- while (k >= 0 && /\s/.test(src[k]))
171632
- k--;
171633
- return k >= 0 ? src[k] : "";
171634
- }, scanFontCalls = (source) => {
171447
+ }, scanAssetIconCalls = (source) => {
171635
171448
  const calls = [];
171636
171449
  const n = source.length;
171637
171450
  let i = 0;
@@ -171659,7 +171472,7 @@ var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c) => c >= "A" && c <= "Z
171659
171472
  let j = i + 1;
171660
171473
  while (j < n && isIdentPart(source[j]))
171661
171474
  j++;
171662
- if (source.slice(i, j) === "font" && prevNonSpace(source, i) !== ".") {
171475
+ if (source.slice(i, j) === "assetIcon" && prevNonSpace(source, i) !== ".") {
171663
171476
  const call = parseCall(source, i, j);
171664
171477
  if (call) {
171665
171478
  calls.push(call);
@@ -171673,6 +171486,11 @@ var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c) => c >= "A" && c <= "Z
171673
171486
  i++;
171674
171487
  }
171675
171488
  return calls;
171489
+ }, prevNonSpace = (src, i) => {
171490
+ let k = i - 1;
171491
+ while (k >= 0 && /\s/.test(src[k]))
171492
+ k--;
171493
+ return k >= 0 ? src[k] : "";
171676
171494
  }, parseCall = (src, start, identEnd) => {
171677
171495
  const n = src.length;
171678
171496
  let i = identEnd;
@@ -171683,9 +171501,621 @@ var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c) => c >= "A" && c <= "Z
171683
171501
  const close = matchParen(src, i);
171684
171502
  if (close === -1)
171685
171503
  return null;
171686
- const call = { start, end: close + 1, arg: "", kind: "registry" };
171504
+ const call = { start, end: close + 1, id: "", pack: "", name: "" };
171687
171505
  const args = splitTopLevel(src.slice(i + 1, close), ",");
171688
- const arg = stringLiteralValue(args[0] ?? "");
171506
+ const id = stringLiteralValue(args[0] ?? "");
171507
+ if (id === null) {
171508
+ call.error = `assetIcon(...) needs a string-literal id like "lucide:bell" (got \`${(args[0] ?? "").trim() || "()"}\`).`;
171509
+ return call;
171510
+ }
171511
+ const m = ICON_ID.exec(id);
171512
+ if (!m) {
171513
+ call.error = `assetIcon("${id}"): id must be "pack:name", lowercase [a-z0-9-] (e.g. "lucide:bell").`;
171514
+ return call;
171515
+ }
171516
+ call.id = id;
171517
+ call.pack = m[1];
171518
+ call.name = m[2];
171519
+ const optsText = args[1]?.trim();
171520
+ if (optsText) {
171521
+ const color = extractColor(optsText);
171522
+ if (color.error) {
171523
+ call.error = color.error;
171524
+ return call;
171525
+ }
171526
+ if (color.bake !== undefined)
171527
+ call.bake = color.bake;
171528
+ else if (color.tint !== undefined)
171529
+ call.tint = color.tint;
171530
+ }
171531
+ return call;
171532
+ }, extractColor = (optsText) => {
171533
+ if (!optsText.startsWith("{") || !optsText.endsWith("}")) {
171534
+ return { error: `assetIcon(id, opts): opts must be an object literal like { color: "#5b8cff" }.` };
171535
+ }
171536
+ for (const prop of splitTopLevel(optsText.slice(1, -1), ",")) {
171537
+ const kv = splitTopLevel(prop, ":");
171538
+ if (kv.length < 2)
171539
+ continue;
171540
+ const key = kv[0].trim().replace(/^["'`]|["'`]$/g, "");
171541
+ if (key !== "color")
171542
+ continue;
171543
+ const raw = kv.slice(1).join(":").trim();
171544
+ if (!raw)
171545
+ continue;
171546
+ const literal = stringLiteralValue(raw);
171547
+ return literal !== null ? { bake: literal } : { tint: raw };
171548
+ }
171549
+ return {};
171550
+ }, rewriteAssetIcons = (source, calls, svgById) => {
171551
+ let out = source;
171552
+ for (const call of [...calls].sort((a, b) => b.start - a.start)) {
171553
+ const svg = svgById.get(call.id);
171554
+ if (svg === undefined)
171555
+ continue;
171556
+ out = out.slice(0, call.start) + iconExpr(svg, call) + out.slice(call.end);
171557
+ }
171558
+ return out;
171559
+ }, iconExpr = (svg, call) => {
171560
+ const body = call.bake ? svg.split("currentColor").join(call.bake) : svg;
171561
+ const src = `SvgSource(\`${escapeTemplate(body)}\`)`;
171562
+ return call.tint ? `Object.assign(${src}, { tintColor: (${call.tint}) })` : src;
171563
+ }, escapeTemplate = (s) => s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${"), applyAssetIconMacro = async (entries, resolveSvgs) => {
171564
+ const scanned = entries.filter((e) => e.type === "text" && typeof e.text === "string" && e.text.includes("assetIcon")).map((e) => ({ entry: e, calls: scanAssetIconCalls(e.text) })).filter((s) => s.calls.length > 0);
171565
+ if (scanned.length === 0)
171566
+ return [];
171567
+ const errors = scanned.flatMap((s) => s.calls.filter((c) => c.error).map((c) => c.error));
171568
+ if (errors.length)
171569
+ throw new Error([...new Set(errors)].join(`
171570
+ `));
171571
+ const ids = [...new Set(scanned.flatMap((s) => s.calls.map((c) => c.id)))];
171572
+ const svgById = await resolveSvgs(ids);
171573
+ const missing = ids.filter((id) => !svgById.has(id));
171574
+ if (missing.length)
171575
+ throw new Error(`assetIcon: could not resolve icon(s): ${missing.join(", ")}.`);
171576
+ for (const { entry, calls } of scanned)
171577
+ entry.text = rewriteAssetIcons(entry.text, calls, svgById);
171578
+ return ids;
171579
+ }, buildIconsHeaderValue = (ids) => [...new Set(ids.map((id) => id.split(":")[0]))].sort().join(";"), editDistance = (a, b, cap) => {
171580
+ if (Math.abs(a.length - b.length) > cap)
171581
+ return cap + 1;
171582
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
171583
+ for (let i = 1;i <= a.length; i++) {
171584
+ const cur = [i];
171585
+ let rowMin = i;
171586
+ for (let j = 1;j <= b.length; j++) {
171587
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
171588
+ if (cur[j] < rowMin)
171589
+ rowMin = cur[j];
171590
+ }
171591
+ if (rowMin > cap)
171592
+ return cap + 1;
171593
+ prev = cur;
171594
+ }
171595
+ return prev[b.length];
171596
+ }, suggestIconNames = (want, icons) => {
171597
+ const wantParts = new Set(want.split("-").filter(Boolean));
171598
+ const scored = [];
171599
+ for (const [name, info] of Object.entries(icons)) {
171600
+ const lenDiff = Math.abs(name.length - want.length) / 100;
171601
+ let rank;
171602
+ if (name.includes(want) || want.includes(name))
171603
+ rank = lenDiff;
171604
+ else {
171605
+ const d = editDistance(want, name, 2);
171606
+ if (d <= 2)
171607
+ rank = 1 + d;
171608
+ else if (info.kw?.some((k) => wantParts.has(k)))
171609
+ rank = 4 + lenDiff;
171610
+ else
171611
+ continue;
171612
+ }
171613
+ scored.push({ name, rank });
171614
+ }
171615
+ return scored.sort((a, b) => a.rank - b.rank || (a.name < b.name ? -1 : 1)).slice(0, 5).map((s) => s.name);
171616
+ }, iconNotFoundError = async (pack, name, read, registry) => {
171617
+ const metaRaw = await read(`${pack}/meta.json`);
171618
+ if (metaRaw !== null) {
171619
+ let icons = {};
171620
+ try {
171621
+ icons = JSON.parse(metaRaw).icons ?? {};
171622
+ } catch {}
171623
+ const similar = suggestIconNames(name, icons);
171624
+ const hint = similar.length ? ` Did you mean ${similar.map((s) => `"${pack}:${s}"`).join(", ")}?` : ` No similar name in the "${pack}" pack either.`;
171625
+ return new Error(`assetIcon: icon "${pack}:${name}" not found.${hint}`);
171626
+ }
171627
+ const licRaw = await read("licenses.json");
171628
+ if (licRaw !== null) {
171629
+ try {
171630
+ const packs = Object.keys(JSON.parse(licRaw)).sort();
171631
+ return new Error(`assetIcon: unknown icon pack "${pack}" (in "${pack}:${name}"). Available packs: ${packs.join(", ")}.`);
171632
+ } catch {}
171633
+ }
171634
+ return Object.assign(new Error(`assetIcon: can't reach the icon registry${registry ? ` (${registry})` : ""} to resolve "${pack}:${name}". ` + `Check your connection, or set LECODES_ICON_REGISTRY.`), { transient: true });
171635
+ }, VENDORED_ICON_PATH, collectVendoredIcons = (entries) => {
171636
+ const map = new Map;
171637
+ for (const e of entries) {
171638
+ if (e.type !== "text" || typeof e.text !== "string")
171639
+ continue;
171640
+ const m = VENDORED_ICON_PATH.exec(e.path);
171641
+ if (m)
171642
+ map.set(`${m[1]}:${m[2]}`, e.text);
171643
+ }
171644
+ return map;
171645
+ };
171646
+ var init_assetIconMacro = __esm(() => {
171647
+ ICON_ID = /^([a-z0-9]+(?:-[a-z0-9]+)*):([a-z0-9]+(?:-[a-z0-9]+)*)$/;
171648
+ VENDORED_ICON_PATH = /(?:^|\/)assets\/icons\/([a-z0-9]+(?:-[a-z0-9]+)*)\/([a-z0-9]+(?:-[a-z0-9]+)*)\.svg$/;
171649
+ });
171650
+
171651
+ // ../../sdk/src/compile/compileProject.ts
171652
+ var localAssetBase = (path) => path.split(/[\\/]/).pop() ?? path, localAssetNames = (entries) => {
171653
+ const seen = new Map;
171654
+ for (const e of entries)
171655
+ if (e.type === "resource") {
171656
+ const b = localAssetBase(e.path);
171657
+ seen.set(b, (seen.get(b) ?? 0) + 1);
171658
+ }
171659
+ const names = new Map;
171660
+ for (const e of entries)
171661
+ if (e.type === "resource") {
171662
+ const b = localAssetBase(e.path);
171663
+ names.set(e.path, (seen.get(b) ?? 0) > 1 ? e.path.replace(/^[./]+/, "").replace(/[\\/]/g, "__") : b);
171664
+ }
171665
+ return names;
171666
+ }, entriesToFiles = (entries, opts) => {
171667
+ const files = {};
171668
+ const localNames = localAssetNames(entries);
171669
+ const localAssetId = (nameExpr) => `_creatorFetch.local(${nameExpr})`;
171670
+ for (const e of entries) {
171671
+ if (e.type === "resource") {
171672
+ if (opts.localAssets && !e.remote) {
171673
+ const name = localNames.get(e.path) ?? localAssetBase(e.path);
171674
+ files[e.path] = `export default ("id:" + ${localAssetId(JSON.stringify(name))})`;
171675
+ } else {
171676
+ files[e.path] = `export default "${opts.publicUrl}${e.fileSrc ?? ""}"`;
171677
+ }
171678
+ } else if (e.type === "shader") {
171679
+ const shaders = e.shaders ?? [];
171680
+ if (e.shadersLocal && shaders.length > 0) {
171681
+ const name = shaders[0].src.split(/[\/]/).pop() ?? "";
171682
+ const [prefix, suffix = ".filamat"] = name.split("_opengl");
171683
+ files[e.path] = `export default ("id:" + ${localAssetId(`${JSON.stringify(prefix + "_")} + _creator.backend + ${JSON.stringify(suffix)}`)})`;
171684
+ } else if (shaders.length > 0) {
171685
+ files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.backend}")}\``;
171686
+ } else {
171687
+ files[e.path] = `export default ""`;
171688
+ }
171689
+ } else if (e.path.toLowerCase().endsWith(".svg")) {
171690
+ files[e.path] = `export default SvgSource(\`${escapeTemplate(e.text ?? "")}\`)`;
171691
+ } else if (e.path.toLowerCase().endsWith(".json")) {
171692
+ files[e.path] = `export default ${e.text?.trim() || "null"}`;
171693
+ } else if (!CODE_EXT.test(e.path)) {
171694
+ files[e.path] = `export default ${JSON.stringify(e.text ?? "")}`;
171695
+ } else {
171696
+ files[e.path] = e.text ?? "";
171697
+ }
171698
+ }
171699
+ return files;
171700
+ }, pluginSpecsOf = (plugins) => {
171701
+ if (Array.isArray(plugins))
171702
+ return Object.fromEntries(plugins.filter((p) => typeof p === "string").map((id) => [id, "*"]));
171703
+ if (plugins && typeof plugins === "object") {
171704
+ return Object.fromEntries(Object.entries(plugins).map(([id, v]) => [id, typeof v === "string" ? v : "*"]));
171705
+ }
171706
+ return {};
171707
+ }, pluginIdsOf = (plugins) => Object.keys(pluginSpecsOf(plugins)), pluginsOfAppJson = (entries) => {
171708
+ const app = entries.find((e) => e.path.replace(/^[./\\]+/, "") === "app.json");
171709
+ if (!app?.text)
171710
+ return [];
171711
+ try {
171712
+ return pluginIdsOf(JSON.parse(app.text).plugins);
171713
+ } catch {
171714
+ return [];
171715
+ }
171716
+ }, pluginsOfBundle = (mapJson) => {
171717
+ const ids = new Set;
171718
+ try {
171719
+ for (const source of JSON.parse(mapJson).sources ?? []) {
171720
+ const m = /\/plugins\/gen\/([^/]+)\/sdk\//.exec(source);
171721
+ if (m)
171722
+ ids.add(m[1]);
171723
+ }
171724
+ } catch {}
171725
+ return [...ids].sort();
171726
+ }, compileProjectWithMap = async (opts) => {
171727
+ const published = clientPublishes(opts.entries);
171728
+ if (published.length)
171729
+ throw new ClientPublishError(published);
171730
+ const entries = stubServerEntries(opts.entries, opts.serverUrl ?? "");
171731
+ const entry = opts.entryOverride ?? detectEntry(entries, scanModulesChisel).entry;
171732
+ if (!entry) {
171733
+ throw new Error("Couldn't find an entrypoint (no .ts/.js project file — name yours main.ts). Pass entryOverride.");
171734
+ }
171735
+ const files = entriesToFiles(entries, opts);
171736
+ const { code, map } = await bundleProjectWithMap(entry, files, opts.format ?? "esm", opts.minify, opts.editor, opts.libraries, opts.sourcemap ?? true);
171737
+ const plugins = opts.plugins ?? pluginsOfAppJson(entries);
171738
+ if (map && opts.onWarning) {
171739
+ const missing = pluginsOfBundle(map).filter((id) => !plugins.includes(id));
171740
+ if (missing.length) {
171741
+ opts.onWarning(`The app uses ${missing.map((id) => `"${id}"`).join(", ")} and app.json does not list ${missing.length === 1 ? "it" : "them"}: a plugin that is not listed is not there — in a shell built from this project and in the browser alike. Add to app.json: "plugins": { ${missing.map((id) => `"${id}": "*"`).join(", ")} }`);
171742
+ }
171743
+ }
171744
+ if (opts.header === false)
171745
+ return { code, map, entry };
171746
+ const header = buildHeader(opts.name, code, { icon: opts.icon, fonts: opts.fontsHeader, icons: opts.iconsHeader, plugins });
171747
+ const headerLines = (header.match(/\n/g) ?? []).length;
171748
+ return { code: header + code, map: map ? offsetSourceMap(map, headerLines) : null, entry };
171749
+ }, compileProject = async (opts) => {
171750
+ const { code, map } = await compileProjectWithMap(opts);
171751
+ return map ? `${code}
171752
+ ${inlineSourceMapComment(map)}` : code;
171753
+ };
171754
+ var init_compileProject = __esm(() => {
171755
+ init_bundler();
171756
+ init_detectEntry();
171757
+ init_header();
171758
+ init_serverSplit();
171759
+ init_assetMacro();
171760
+ init_assetIconMacro();
171761
+ });
171762
+
171763
+ // ../../sdk/src/compile/sceneEditor.ts
171764
+ var SCENE_HARNESS_DIR = "./.scene-editor", SCENE_ENTRY_PATH = "./.scene-entry.ts", resolveRelative = (fromPath, spec) => {
171765
+ const segs = (p) => p.split("/").filter((s) => s !== "" && s !== ".");
171766
+ const out = segs(fromPath).slice(0, -1);
171767
+ for (const seg of segs(spec)) {
171768
+ if (seg === "..")
171769
+ out.pop();
171770
+ else
171771
+ out.push(seg);
171772
+ }
171773
+ return "./" + out.join("/");
171774
+ }, sceneImportsOf = (sceneText, scenePath, projectPaths) => {
171775
+ const known = new Set(projectPaths);
171776
+ const out = [];
171777
+ const re = /^\s*import\s+[A-Za-z_$][\w$]*\s+from\s+["']([^"']+\.scene(?:\.ts)?)["']/gm;
171778
+ for (const m of sceneText.matchAll(re)) {
171779
+ const spec = m[1];
171780
+ if (!spec.startsWith("."))
171781
+ continue;
171782
+ let p = resolveRelative(scenePath, spec);
171783
+ if (!p.endsWith(".ts"))
171784
+ p += ".ts";
171785
+ if (known.has(p) && !out.includes(p))
171786
+ out.push(p);
171787
+ }
171788
+ return out;
171789
+ }, sceneEditorEntries = (scenePath, harness, projectPaths = [], sceneImports = []) => {
171790
+ const entries = Object.entries(harness).map(([name, text]) => ({
171791
+ path: `${SCENE_HARNESS_DIR}/${name}`,
171792
+ type: "text",
171793
+ text
171794
+ }));
171795
+ entries.push({
171796
+ path: `${SCENE_HARNESS_DIR}/.edit-mode.ts`,
171797
+ type: "text",
171798
+ text: `;(globalThis as any).__lecodesSceneEdit = true
171799
+ export {}
171800
+ `
171801
+ });
171802
+ const editorImports = projectPaths.filter((p) => p.endsWith(".editor.ts")).map((p) => `import "${p}"
171803
+ `).join("");
171804
+ const materialPaths = projectPaths.filter((p) => p.endsWith(".material.ts"));
171805
+ const materialImports = materialPaths.map((p, i) => `import __mat${i} from "${p}"
171806
+ `).join("");
171807
+ const materialRegister = materialPaths.length === 0 ? "" : `;(globalThis as any).__lecodesMaterials = { ${materialPaths.map((p, i) => `${JSON.stringify(p)}: __mat${i}`).join(", ")} }
171808
+ `;
171809
+ const sceneImportLines = sceneImports.map((p, i) => `import __scn${i} from "${p}"
171810
+ `).join("");
171811
+ const sceneRegister = sceneImports.length === 0 ? "" : `;(globalThis as any).__lecodesSceneFiles = { ${sceneImports.map((p, i) => `${JSON.stringify(p)}: __scn${i}`).join(", ")} }
171812
+ `;
171813
+ entries.push({
171814
+ path: SCENE_ENTRY_PATH,
171815
+ type: "text",
171816
+ text: `import "${SCENE_HARNESS_DIR}/.edit-mode.ts"
171817
+ ` + materialImports + sceneImportLines + `import "${scenePath}"
171818
+ ` + editorImports + `import "${SCENE_HARNESS_DIR}/main.ts"
171819
+ ` + materialRegister + sceneRegister
171820
+ });
171821
+ return entries;
171822
+ };
171823
+
171824
+ // ../../sdk/src/compile/fontRegistry.ts
171825
+ var DEFAULT_FONT_REGISTRY_BASE = "https://cdn.le.codes/fonts/v1", fontRegistryBase = () => typeof process !== "undefined" && process.env?.LECODES_FONT_REGISTRY?.trim() || DEFAULT_FONT_REGISTRY_BASE, faceFileName = (id, weight, italic, ext) => `${id}-${weight}${italic ? "-italic" : ""}.${ext}`, LATIN, CYR, OFL = "OFL-1.1", f = (family, category, scripts, weights, italics, defaultWeights) => ({ family, category, scripts, weights, italics, defaultWeights, license: OFL }), FONT_REGISTRY;
171826
+ var init_fontRegistry = __esm(() => {
171827
+ LATIN = ["latin"];
171828
+ CYR = ["latin", "cyrillic"];
171829
+ FONT_REGISTRY = {
171830
+ inter: f("Inter", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700, 800, 900], [], [400, 500, 700]),
171831
+ manrope: f("Manrope", "sans-serif", CYR, [200, 300, 400, 500, 600, 700, 800], [], [400, 500, 700]),
171832
+ "golos-text": f("Golos Text", "sans-serif", CYR, [400, 500, 600, 700, 800, 900], [], [400, 500, 700]),
171833
+ onest: f("Onest", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700, 800, 900], [], [400, 500, 700]),
171834
+ rubik: f("Rubik", "sans-serif", CYR, [300, 400, 500, 600, 700, 800, 900], [300, 400, 500, 600, 700, 800, 900], [400, 500, 700]),
171835
+ nunito: f("Nunito", "sans-serif", CYR, [200, 300, 400, 500, 600, 700, 800, 900], [400, 700], [400, 600, 700]),
171836
+ montserrat: f("Montserrat", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700, 800, 900], [400, 700], [400, 500, 700]),
171837
+ "ibm-plex-sans": f("IBM Plex Sans", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700], [400, 700], [400, 500, 700]),
171838
+ "pt-sans": f("PT Sans", "sans-serif", CYR, [400, 700], [400, 700], [400, 700]),
171839
+ jost: f("Jost", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700, 800, 900], [400, 700], [400, 500, 700]),
171840
+ "exo-2": f("Exo 2", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700, 800, 900], [400, 700], [400, 500, 700]),
171841
+ unbounded: f("Unbounded", "display", CYR, [200, 300, 400, 500, 600, 700, 800, 900], [], [400, 700]),
171842
+ "russo-one": f("Russo One", "display", CYR, [400], [], [400]),
171843
+ oswald: f("Oswald", "display", CYR, [200, 300, 400, 500, 600, 700], [], [400, 700]),
171844
+ comfortaa: f("Comfortaa", "display", CYR, [300, 400, 500, 600, 700], [], [400, 700]),
171845
+ "space-grotesk": f("Space Grotesk", "display", LATIN, [300, 400, 500, 600, 700], [], [400, 700]),
171846
+ "playfair-display": f("Playfair Display", "serif", CYR, [400, 500, 600, 700, 800, 900], [400, 700], [400, 700]),
171847
+ lora: f("Lora", "serif", CYR, [400, 500, 600, 700], [400, 700], [400, 700]),
171848
+ "pt-serif": f("PT Serif", "serif", CYR, [400, 700], [400, 700], [400, 700]),
171849
+ cormorant: f("Cormorant", "serif", CYR, [300, 400, 500, 600, 700], [400, 700], [400, 700]),
171850
+ literata: f("Literata", "serif", CYR, [200, 300, 400, 500, 600, 700, 800, 900], [400, 700], [400, 700]),
171851
+ "jetbrains-mono": f("JetBrains Mono", "monospace", CYR, [100, 200, 300, 400, 500, 600, 700, 800], [400, 700], [400, 700]),
171852
+ "fira-code": f("Fira Code", "monospace", CYR, [300, 400, 500, 600, 700], [], [400, 700]),
171853
+ "ibm-plex-mono": f("IBM Plex Mono", "monospace", CYR, [100, 200, 300, 400, 500, 600, 700], [400, 700], [400, 700]),
171854
+ caveat: f("Caveat", "handwriting", CYR, [400, 500, 600, 700], [], [400, 700]),
171855
+ pacifico: f("Pacifico", "handwriting", CYR, [400], [], [400])
171856
+ };
171857
+ });
171858
+
171859
+ // ../../sdk/src/compile/sfnt.ts
171860
+ var TAG_TTF = 65536, TAG_OTTO = 1330926671, TAG_TTCF = 1953784678, TAG_WOFF = 2001684038, TAG_WOF2 = 2001684018, parseSfnt = (bytes) => {
171861
+ const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
171862
+ if (bytes.byteLength < 12)
171863
+ throw new Error("not a font file (truncated)");
171864
+ const version = dv.getUint32(0);
171865
+ if (version === TAG_TTCF)
171866
+ throw new Error("font collections (.ttc) aren't supported — export a single face");
171867
+ if (version === TAG_WOFF || version === TAG_WOF2)
171868
+ throw new Error("WOFF/WOFF2 project fonts aren't supported — use the .ttf/.otf");
171869
+ if (version !== TAG_TTF && version !== TAG_OTTO)
171870
+ throw new Error("not a TTF/OTF font file");
171871
+ const numTables = dv.getUint16(4);
171872
+ const tables = new Map;
171873
+ for (let i = 0;i < numTables; i++) {
171874
+ const rec = 12 + i * 16;
171875
+ if (rec + 16 > bytes.byteLength)
171876
+ throw new Error("not a font file (truncated table directory)");
171877
+ const tag = String.fromCharCode(bytes[rec], bytes[rec + 1], bytes[rec + 2], bytes[rec + 3]);
171878
+ tables.set(tag, { offset: dv.getUint32(rec + 8), length: dv.getUint32(rec + 12) });
171879
+ }
171880
+ const family = readFamily(dv, bytes, tables.get("name"));
171881
+ if (!family)
171882
+ throw new Error("no family name in the font's `name` table");
171883
+ let weight = 400;
171884
+ let italic = false;
171885
+ const os2 = tables.get("OS/2");
171886
+ if (os2 && os2.offset + 64 <= bytes.byteLength) {
171887
+ weight = dv.getUint16(os2.offset + 4);
171888
+ italic = (dv.getUint16(os2.offset + 62) & 1) === 1;
171889
+ } else {
171890
+ const head = tables.get("head");
171891
+ if (head && head.offset + 46 <= bytes.byteLength) {
171892
+ const macStyle = dv.getUint16(head.offset + 44);
171893
+ if (macStyle & 1)
171894
+ weight = 700;
171895
+ italic = (macStyle & 2) === 2;
171896
+ }
171897
+ }
171898
+ if (weight < 1 || weight > 1000)
171899
+ weight = 400;
171900
+ return { family, weight, italic };
171901
+ }, readFamily = (dv, bytes, name) => {
171902
+ if (!name || name.offset + 6 > bytes.byteLength)
171903
+ return "";
171904
+ const base = name.offset;
171905
+ const count = dv.getUint16(base + 2);
171906
+ const stringOffset = base + dv.getUint16(base + 4);
171907
+ let best = "";
171908
+ let bestScore = -1;
171909
+ for (let i = 0;i < count; i++) {
171910
+ const rec = base + 6 + i * 12;
171911
+ if (rec + 12 > bytes.byteLength)
171912
+ break;
171913
+ const platformID = dv.getUint16(rec);
171914
+ const languageID = dv.getUint16(rec + 4);
171915
+ const nameID = dv.getUint16(rec + 6);
171916
+ const length = dv.getUint16(rec + 8);
171917
+ const offset = stringOffset + dv.getUint16(rec + 10);
171918
+ if (nameID !== 16 && nameID !== 1)
171919
+ continue;
171920
+ if (offset + length > bytes.byteLength)
171921
+ continue;
171922
+ let score = nameID === 16 ? 100 : 0;
171923
+ if (platformID === 3)
171924
+ score += languageID === 1033 ? 30 : 20;
171925
+ else if (platformID === 0)
171926
+ score += 10;
171927
+ if (score <= bestScore)
171928
+ continue;
171929
+ const text = platformID === 1 ? decodeLatin1(bytes, offset, length) : decodeUtf16be(bytes, offset, length);
171930
+ if (!text.trim())
171931
+ continue;
171932
+ best = text.trim();
171933
+ bestScore = score;
171934
+ }
171935
+ return best;
171936
+ }, decodeUtf16be = (bytes, offset, length) => {
171937
+ let out = "";
171938
+ for (let i = 0;i + 1 < length; i += 2)
171939
+ out += String.fromCharCode(bytes[offset + i] << 8 | bytes[offset + i + 1]);
171940
+ return out;
171941
+ }, decodeLatin1 = (bytes, offset, length) => {
171942
+ let out = "";
171943
+ for (let i = 0;i < length; i++)
171944
+ out += String.fromCharCode(bytes[offset + i]);
171945
+ return out;
171946
+ };
171947
+
171948
+ // ../../sdk/src/compile/fontMacro.ts
171949
+ var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart2 = (c) => c >= "A" && c <= "Z" || c >= "a" && c <= "z" || c === "_" || c === "$", isIdentPart2 = (c) => isIdentStart2(c) || c >= "0" && c <= "9", skipString3 = (src, start) => {
171950
+ const quote = src[start];
171951
+ const n = src.length;
171952
+ let i = start + 1;
171953
+ while (i < n) {
171954
+ const c = src[i];
171955
+ if (c === "\\") {
171956
+ i += 2;
171957
+ continue;
171958
+ }
171959
+ if (quote === "`" && c === "$" && src[i + 1] === "{") {
171960
+ i = skipBraces2(src, i + 1);
171961
+ continue;
171962
+ }
171963
+ if (c === quote)
171964
+ return i + 1;
171965
+ i++;
171966
+ }
171967
+ return n;
171968
+ }, skipBraces2 = (src, open) => {
171969
+ const n = src.length;
171970
+ let depth = 0;
171971
+ let i = open;
171972
+ while (i < n) {
171973
+ const c = src[i];
171974
+ if (c === '"' || c === "'" || c === "`") {
171975
+ i = skipString3(src, i);
171976
+ continue;
171977
+ }
171978
+ if (c === "{")
171979
+ depth++;
171980
+ else if (c === "}") {
171981
+ depth--;
171982
+ if (depth === 0)
171983
+ return i + 1;
171984
+ }
171985
+ i++;
171986
+ }
171987
+ return n;
171988
+ }, matchParen2 = (src, open) => {
171989
+ const n = src.length;
171990
+ let depth = 0;
171991
+ let i = open;
171992
+ while (i < n) {
171993
+ const c = src[i];
171994
+ if (c === '"' || c === "'" || c === "`") {
171995
+ i = skipString3(src, i);
171996
+ continue;
171997
+ }
171998
+ if (c === "/" && src[i + 1] === "/") {
171999
+ i += 2;
172000
+ while (i < n && src[i] !== `
172001
+ `)
172002
+ i++;
172003
+ continue;
172004
+ }
172005
+ if (c === "/" && src[i + 1] === "*") {
172006
+ i += 2;
172007
+ while (i < n && !(src[i] === "*" && src[i + 1] === "/"))
172008
+ i++;
172009
+ i += 2;
172010
+ continue;
172011
+ }
172012
+ if (c === "(")
172013
+ depth++;
172014
+ else if (c === ")") {
172015
+ depth--;
172016
+ if (depth === 0)
172017
+ return i;
172018
+ }
172019
+ i++;
172020
+ }
172021
+ return -1;
172022
+ }, splitTopLevel2 = (text, delim) => {
172023
+ const parts = [];
172024
+ const n = text.length;
172025
+ let depth = 0;
172026
+ let last = 0;
172027
+ let i = 0;
172028
+ while (i < n) {
172029
+ const c = text[i];
172030
+ if (c === '"' || c === "'" || c === "`") {
172031
+ i = skipString3(text, i);
172032
+ continue;
172033
+ }
172034
+ if (c === "(" || c === "[" || c === "{")
172035
+ depth++;
172036
+ else if (c === ")" || c === "]" || c === "}")
172037
+ depth = Math.max(0, depth - 1);
172038
+ else if (depth === 0 && c === delim) {
172039
+ parts.push(text.slice(last, i));
172040
+ last = i + 1;
172041
+ }
172042
+ i++;
172043
+ }
172044
+ parts.push(text.slice(last));
172045
+ return parts;
172046
+ }, stringLiteralValue2 = (text) => {
172047
+ const t = text.trim();
172048
+ if (t.length < 2)
172049
+ return null;
172050
+ const q = t[0];
172051
+ if (q !== '"' && q !== "'" && q !== "`" || t[t.length - 1] !== q)
172052
+ return null;
172053
+ const inner = t.slice(1, -1);
172054
+ if (q === "`" && inner.includes("${"))
172055
+ return null;
172056
+ if (inner.includes(q) && !inner.includes("\\" + q))
172057
+ return null;
172058
+ return inner.replace(/\\(.)/g, "$1");
172059
+ }, prevNonSpace2 = (src, i) => {
172060
+ let k = i - 1;
172061
+ while (k >= 0 && /\s/.test(src[k]))
172062
+ k--;
172063
+ return k >= 0 ? src[k] : "";
172064
+ }, scanFontCalls = (source) => {
172065
+ const calls = [];
172066
+ const n = source.length;
172067
+ let i = 0;
172068
+ while (i < n) {
172069
+ const c = source[i];
172070
+ if (c === '"' || c === "'" || c === "`") {
172071
+ i = skipString3(source, i);
172072
+ continue;
172073
+ }
172074
+ if (c === "/" && source[i + 1] === "/") {
172075
+ i += 2;
172076
+ while (i < n && source[i] !== `
172077
+ `)
172078
+ i++;
172079
+ continue;
172080
+ }
172081
+ if (c === "/" && source[i + 1] === "*") {
172082
+ i += 2;
172083
+ while (i < n && !(source[i] === "*" && source[i + 1] === "/"))
172084
+ i++;
172085
+ i += 2;
172086
+ continue;
172087
+ }
172088
+ if (isIdentStart2(c)) {
172089
+ let j = i + 1;
172090
+ while (j < n && isIdentPart2(source[j]))
172091
+ j++;
172092
+ if (source.slice(i, j) === "font" && prevNonSpace2(source, i) !== ".") {
172093
+ const call = parseCall2(source, i, j);
172094
+ if (call) {
172095
+ calls.push(call);
172096
+ i = call.end;
172097
+ continue;
172098
+ }
172099
+ }
172100
+ i = j;
172101
+ continue;
172102
+ }
172103
+ i++;
172104
+ }
172105
+ return calls;
172106
+ }, parseCall2 = (src, start, identEnd) => {
172107
+ const n = src.length;
172108
+ let i = identEnd;
172109
+ while (i < n && /\s/.test(src[i]))
172110
+ i++;
172111
+ if (src[i] !== "(")
172112
+ return null;
172113
+ const close = matchParen2(src, i);
172114
+ if (close === -1)
172115
+ return null;
172116
+ const call = { start, end: close + 1, arg: "", kind: "registry" };
172117
+ const args = splitTopLevel2(src.slice(i + 1, close), ",");
172118
+ const arg = stringLiteralValue2(args[0] ?? "");
171689
172119
  if (arg === null) {
171690
172120
  call.error = `font(...) needs a string-literal argument — a registry id ("manrope") or a project path ("./fonts/Brand.ttf"). Got \`${(args[0] ?? "").trim() || "()"}\`.`;
171691
172121
  return call;
@@ -171726,10 +172156,10 @@ var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c) => c >= "A" && c <= "Z
171726
172156
  if (!optsText.startsWith("{") || !optsText.endsWith("}")) {
171727
172157
  return `font("${call.arg}", opts): opts must be an object literal like { weights: [400, 700] }.`;
171728
172158
  }
171729
- for (const prop of splitTopLevel(optsText.slice(1, -1), ",")) {
172159
+ for (const prop of splitTopLevel2(optsText.slice(1, -1), ",")) {
171730
172160
  if (!prop.trim())
171731
172161
  continue;
171732
- const kv = splitTopLevel(prop, ":");
172162
+ const kv = splitTopLevel2(prop, ":");
171733
172163
  if (kv.length < 2)
171734
172164
  return `font("${call.arg}"): couldn't parse option \`${prop.trim()}\`.`;
171735
172165
  const key = kv[0].trim().replace(/^["'`]|["'`]$/g, "");
@@ -171924,321 +172354,6 @@ var init_fontMacro = __esm(() => {
171924
172354
  DECLARES_FONT = /\b(?:function|const|let|var|class)\s+font\b|\bimport\s+(?:\{[^}]*\bfont\b[^}]*\}|font)\s+from\b/;
171925
172355
  });
171926
172356
 
171927
- // ../../sdk/src/compile/assetIconMacro.ts
171928
- var ICON_ID, isIdentStart2 = (c) => c >= "A" && c <= "Z" || c >= "a" && c <= "z" || c === "_" || c === "$", isIdentPart2 = (c) => isIdentStart2(c) || c >= "0" && c <= "9", skipString3 = (src, start) => {
171929
- const quote = src[start];
171930
- const n = src.length;
171931
- let i = start + 1;
171932
- while (i < n) {
171933
- const c = src[i];
171934
- if (c === "\\") {
171935
- i += 2;
171936
- continue;
171937
- }
171938
- if (quote === "`" && c === "$" && src[i + 1] === "{") {
171939
- i = skipBraces2(src, i + 1);
171940
- continue;
171941
- }
171942
- if (c === quote)
171943
- return i + 1;
171944
- i++;
171945
- }
171946
- return n;
171947
- }, skipBraces2 = (src, open) => {
171948
- const n = src.length;
171949
- let depth = 0;
171950
- let i = open;
171951
- while (i < n) {
171952
- const c = src[i];
171953
- if (c === '"' || c === "'" || c === "`") {
171954
- i = skipString3(src, i);
171955
- continue;
171956
- }
171957
- if (c === "{")
171958
- depth++;
171959
- else if (c === "}") {
171960
- depth--;
171961
- if (depth === 0)
171962
- return i + 1;
171963
- }
171964
- i++;
171965
- }
171966
- return n;
171967
- }, matchParen2 = (src, open) => {
171968
- const n = src.length;
171969
- let depth = 0;
171970
- let i = open;
171971
- while (i < n) {
171972
- const c = src[i];
171973
- if (c === '"' || c === "'" || c === "`") {
171974
- i = skipString3(src, i);
171975
- continue;
171976
- }
171977
- if (c === "/" && src[i + 1] === "/") {
171978
- i += 2;
171979
- while (i < n && src[i] !== `
171980
- `)
171981
- i++;
171982
- continue;
171983
- }
171984
- if (c === "/" && src[i + 1] === "*") {
171985
- i += 2;
171986
- while (i < n && !(src[i] === "*" && src[i + 1] === "/"))
171987
- i++;
171988
- i += 2;
171989
- continue;
171990
- }
171991
- if (c === "(")
171992
- depth++;
171993
- else if (c === ")") {
171994
- depth--;
171995
- if (depth === 0)
171996
- return i;
171997
- }
171998
- i++;
171999
- }
172000
- return -1;
172001
- }, splitTopLevel2 = (text, delim) => {
172002
- const parts = [];
172003
- const n = text.length;
172004
- let depth = 0;
172005
- let last = 0;
172006
- let i = 0;
172007
- while (i < n) {
172008
- const c = text[i];
172009
- if (c === '"' || c === "'" || c === "`") {
172010
- i = skipString3(text, i);
172011
- continue;
172012
- }
172013
- if (c === "(" || c === "[" || c === "{")
172014
- depth++;
172015
- else if (c === ")" || c === "]" || c === "}")
172016
- depth = Math.max(0, depth - 1);
172017
- else if (depth === 0 && c === delim) {
172018
- parts.push(text.slice(last, i));
172019
- last = i + 1;
172020
- }
172021
- i++;
172022
- }
172023
- parts.push(text.slice(last));
172024
- return parts;
172025
- }, stringLiteralValue2 = (text) => {
172026
- const t = text.trim();
172027
- if (t.length < 2)
172028
- return null;
172029
- const q = t[0];
172030
- if (q !== '"' && q !== "'" && q !== "`" || t[t.length - 1] !== q)
172031
- return null;
172032
- const inner = t.slice(1, -1);
172033
- if (q === "`" && inner.includes("${"))
172034
- return null;
172035
- if (inner.includes(q) && !inner.includes("\\" + q))
172036
- return null;
172037
- return inner.replace(/\\(.)/g, "$1");
172038
- }, scanAssetIconCalls = (source) => {
172039
- const calls = [];
172040
- const n = source.length;
172041
- let i = 0;
172042
- while (i < n) {
172043
- const c = source[i];
172044
- if (c === '"' || c === "'" || c === "`") {
172045
- i = skipString3(source, i);
172046
- continue;
172047
- }
172048
- if (c === "/" && source[i + 1] === "/") {
172049
- i += 2;
172050
- while (i < n && source[i] !== `
172051
- `)
172052
- i++;
172053
- continue;
172054
- }
172055
- if (c === "/" && source[i + 1] === "*") {
172056
- i += 2;
172057
- while (i < n && !(source[i] === "*" && source[i + 1] === "/"))
172058
- i++;
172059
- i += 2;
172060
- continue;
172061
- }
172062
- if (isIdentStart2(c)) {
172063
- let j = i + 1;
172064
- while (j < n && isIdentPart2(source[j]))
172065
- j++;
172066
- if (source.slice(i, j) === "assetIcon" && prevNonSpace2(source, i) !== ".") {
172067
- const call = parseCall2(source, i, j);
172068
- if (call) {
172069
- calls.push(call);
172070
- i = call.end;
172071
- continue;
172072
- }
172073
- }
172074
- i = j;
172075
- continue;
172076
- }
172077
- i++;
172078
- }
172079
- return calls;
172080
- }, prevNonSpace2 = (src, i) => {
172081
- let k = i - 1;
172082
- while (k >= 0 && /\s/.test(src[k]))
172083
- k--;
172084
- return k >= 0 ? src[k] : "";
172085
- }, parseCall2 = (src, start, identEnd) => {
172086
- const n = src.length;
172087
- let i = identEnd;
172088
- while (i < n && /\s/.test(src[i]))
172089
- i++;
172090
- if (src[i] !== "(")
172091
- return null;
172092
- const close = matchParen2(src, i);
172093
- if (close === -1)
172094
- return null;
172095
- const call = { start, end: close + 1, id: "", pack: "", name: "" };
172096
- const args = splitTopLevel2(src.slice(i + 1, close), ",");
172097
- const id = stringLiteralValue2(args[0] ?? "");
172098
- if (id === null) {
172099
- call.error = `assetIcon(...) needs a string-literal id like "lucide:bell" (got \`${(args[0] ?? "").trim() || "()"}\`).`;
172100
- return call;
172101
- }
172102
- const m = ICON_ID.exec(id);
172103
- if (!m) {
172104
- call.error = `assetIcon("${id}"): id must be "pack:name", lowercase [a-z0-9-] (e.g. "lucide:bell").`;
172105
- return call;
172106
- }
172107
- call.id = id;
172108
- call.pack = m[1];
172109
- call.name = m[2];
172110
- const optsText = args[1]?.trim();
172111
- if (optsText) {
172112
- const color = extractColor(optsText);
172113
- if (color.error) {
172114
- call.error = color.error;
172115
- return call;
172116
- }
172117
- if (color.bake !== undefined)
172118
- call.bake = color.bake;
172119
- else if (color.tint !== undefined)
172120
- call.tint = color.tint;
172121
- }
172122
- return call;
172123
- }, extractColor = (optsText) => {
172124
- if (!optsText.startsWith("{") || !optsText.endsWith("}")) {
172125
- return { error: `assetIcon(id, opts): opts must be an object literal like { color: "#5b8cff" }.` };
172126
- }
172127
- for (const prop of splitTopLevel2(optsText.slice(1, -1), ",")) {
172128
- const kv = splitTopLevel2(prop, ":");
172129
- if (kv.length < 2)
172130
- continue;
172131
- const key = kv[0].trim().replace(/^["'`]|["'`]$/g, "");
172132
- if (key !== "color")
172133
- continue;
172134
- const raw = kv.slice(1).join(":").trim();
172135
- if (!raw)
172136
- continue;
172137
- const literal = stringLiteralValue2(raw);
172138
- return literal !== null ? { bake: literal } : { tint: raw };
172139
- }
172140
- return {};
172141
- }, rewriteAssetIcons = (source, calls, svgById) => {
172142
- let out = source;
172143
- for (const call of [...calls].sort((a, b) => b.start - a.start)) {
172144
- const svg = svgById.get(call.id);
172145
- if (svg === undefined)
172146
- continue;
172147
- out = out.slice(0, call.start) + iconExpr(svg, call) + out.slice(call.end);
172148
- }
172149
- return out;
172150
- }, iconExpr = (svg, call) => {
172151
- const body = call.bake ? svg.split("currentColor").join(call.bake) : svg;
172152
- const src = `SvgSource(\`${escapeTemplate(body)}\`)`;
172153
- return call.tint ? `Object.assign(${src}, { tintColor: (${call.tint}) })` : src;
172154
- }, escapeTemplate = (s) => s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${"), applyAssetIconMacro = async (entries, resolveSvgs) => {
172155
- const scanned = entries.filter((e) => e.type === "text" && typeof e.text === "string" && e.text.includes("assetIcon")).map((e) => ({ entry: e, calls: scanAssetIconCalls(e.text) })).filter((s) => s.calls.length > 0);
172156
- if (scanned.length === 0)
172157
- return [];
172158
- const errors = scanned.flatMap((s) => s.calls.filter((c) => c.error).map((c) => c.error));
172159
- if (errors.length)
172160
- throw new Error([...new Set(errors)].join(`
172161
- `));
172162
- const ids = [...new Set(scanned.flatMap((s) => s.calls.map((c) => c.id)))];
172163
- const svgById = await resolveSvgs(ids);
172164
- const missing = ids.filter((id) => !svgById.has(id));
172165
- if (missing.length)
172166
- throw new Error(`assetIcon: could not resolve icon(s): ${missing.join(", ")}.`);
172167
- for (const { entry, calls } of scanned)
172168
- entry.text = rewriteAssetIcons(entry.text, calls, svgById);
172169
- return ids;
172170
- }, buildIconsHeaderValue = (ids) => [...new Set(ids.map((id) => id.split(":")[0]))].sort().join(";"), editDistance = (a, b, cap) => {
172171
- if (Math.abs(a.length - b.length) > cap)
172172
- return cap + 1;
172173
- let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
172174
- for (let i = 1;i <= a.length; i++) {
172175
- const cur = [i];
172176
- let rowMin = i;
172177
- for (let j = 1;j <= b.length; j++) {
172178
- cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
172179
- if (cur[j] < rowMin)
172180
- rowMin = cur[j];
172181
- }
172182
- if (rowMin > cap)
172183
- return cap + 1;
172184
- prev = cur;
172185
- }
172186
- return prev[b.length];
172187
- }, suggestIconNames = (want, icons) => {
172188
- const wantParts = new Set(want.split("-").filter(Boolean));
172189
- const scored = [];
172190
- for (const [name, info] of Object.entries(icons)) {
172191
- const lenDiff = Math.abs(name.length - want.length) / 100;
172192
- let rank;
172193
- if (name.includes(want) || want.includes(name))
172194
- rank = lenDiff;
172195
- else {
172196
- const d = editDistance(want, name, 2);
172197
- if (d <= 2)
172198
- rank = 1 + d;
172199
- else if (info.kw?.some((k) => wantParts.has(k)))
172200
- rank = 4 + lenDiff;
172201
- else
172202
- continue;
172203
- }
172204
- scored.push({ name, rank });
172205
- }
172206
- return scored.sort((a, b) => a.rank - b.rank || (a.name < b.name ? -1 : 1)).slice(0, 5).map((s) => s.name);
172207
- }, iconNotFoundError = async (pack, name, read, registry) => {
172208
- const metaRaw = await read(`${pack}/meta.json`);
172209
- if (metaRaw !== null) {
172210
- let icons = {};
172211
- try {
172212
- icons = JSON.parse(metaRaw).icons ?? {};
172213
- } catch {}
172214
- const similar = suggestIconNames(name, icons);
172215
- const hint = similar.length ? ` Did you mean ${similar.map((s) => `"${pack}:${s}"`).join(", ")}?` : ` No similar name in the "${pack}" pack either.`;
172216
- return new Error(`assetIcon: icon "${pack}:${name}" not found.${hint}`);
172217
- }
172218
- const licRaw = await read("licenses.json");
172219
- if (licRaw !== null) {
172220
- try {
172221
- const packs = Object.keys(JSON.parse(licRaw)).sort();
172222
- return new Error(`assetIcon: unknown icon pack "${pack}" (in "${pack}:${name}"). Available packs: ${packs.join(", ")}.`);
172223
- } catch {}
172224
- }
172225
- return Object.assign(new Error(`assetIcon: can't reach the icon registry${registry ? ` (${registry})` : ""} to resolve "${pack}:${name}". ` + `Check your connection, or set LECODES_ICON_REGISTRY.`), { transient: true });
172226
- }, VENDORED_ICON_PATH, collectVendoredIcons = (entries) => {
172227
- const map = new Map;
172228
- for (const e of entries) {
172229
- if (e.type !== "text" || typeof e.text !== "string")
172230
- continue;
172231
- const m = VENDORED_ICON_PATH.exec(e.path);
172232
- if (m)
172233
- map.set(`${m[1]}:${m[2]}`, e.text);
172234
- }
172235
- return map;
172236
- };
172237
- var init_assetIconMacro = __esm(() => {
172238
- ICON_ID = /^([a-z0-9]+(?:-[a-z0-9]+)*):([a-z0-9]+(?:-[a-z0-9]+)*)$/;
172239
- VENDORED_ICON_PATH = /(?:^|\/)assets\/icons\/([a-z0-9]+(?:-[a-z0-9]+)*)\/([a-z0-9]+(?:-[a-z0-9]+)*)\.svg$/;
172240
- });
172241
-
172242
172357
  // ../../sdk/src/compile/index.ts
172243
172358
  var init_compile = __esm(() => {
172244
172359
  init_bundler();
@@ -207593,15 +207708,26 @@ var init_channel = __esm(() => {
207593
207708
  seam = globalThis;
207594
207709
  });
207595
207710
 
207711
+ // ../../sdk/src/server/pushChannel.ts
207712
+ var seam2, setPushSender = (sender) => {
207713
+ if (sender)
207714
+ seam2.__lecodesPushSend = sender;
207715
+ else
207716
+ delete seam2.__lecodesPushSend;
207717
+ }, isPushChannel = (v) => typeof v === "object" && v !== null && v.__pushChannel === true;
207718
+ var init_pushChannel = __esm(() => {
207719
+ seam2 = globalThis;
207720
+ });
207721
+
207596
207722
  // ../../sdk/src/server/context.ts
207597
- var seam2, setRequestProvider = (provider) => {
207723
+ var seam3, setRequestProvider = (provider) => {
207598
207724
  if (provider)
207599
- seam2.__lecodesRequest = provider;
207725
+ seam3.__lecodesRequest = provider;
207600
207726
  else
207601
- delete seam2.__lecodesRequest;
207602
- }, currentRequest = () => seam2.__lecodesRequest?.();
207727
+ delete seam3.__lecodesRequest;
207728
+ }, currentRequest = () => seam3.__lecodesRequest?.();
207603
207729
  var init_context2 = __esm(() => {
207604
- seam2 = globalThis;
207730
+ seam3 = globalThis;
207605
207731
  });
207606
207732
 
207607
207733
  // ../../sdk/src/server/db/fields.ts
@@ -207654,9 +207780,9 @@ var init_fields = __esm(() => {
207654
207780
  });
207655
207781
 
207656
207782
  // ../../sdk/src/server/errors.ts
207657
- var ApiError;
207783
+ var ApiError2;
207658
207784
  var init_errors3 = __esm(() => {
207659
- ApiError = class ApiError extends Error {
207785
+ ApiError2 = class ApiError2 extends Error {
207660
207786
  status;
207661
207787
  constructor(status, message = "") {
207662
207788
  super(message);
@@ -207689,30 +207815,30 @@ var FILE_MODEL = "File", filePublicFields = () => ({
207689
207815
  width: t.int().optional(),
207690
207816
  height: t.int().optional(),
207691
207817
  createdAt: t.date().default("now")
207692
- }), seam3, setFilesSeam = (value) => {
207818
+ }), seam4, setFilesSeam = (value) => {
207693
207819
  if (value)
207694
- seam3.__lecodesFiles = value;
207820
+ seam4.__lecodesFiles = value;
207695
207821
  else
207696
- delete seam3.__lecodesFiles;
207822
+ delete seam4.__lecodesFiles;
207697
207823
  };
207698
207824
  var init_models2 = __esm(() => {
207699
207825
  init_fields();
207700
- seam3 = globalThis;
207826
+ seam4 = globalThis;
207701
207827
  });
207702
207828
 
207703
207829
  // ../../sdk/src/server/db/defineDb.ts
207704
- var seam4, setDbTransport = (transport) => {
207830
+ var seam5, setDbTransport = (transport) => {
207705
207831
  if (transport)
207706
- seam4.__lecodesDbTransport = transport;
207832
+ seam5.__lecodesDbTransport = transport;
207707
207833
  else
207708
- delete seam4.__lecodesDbTransport;
207834
+ delete seam5.__lecodesDbTransport;
207709
207835
  }, takeRegisteredDbs = () => {
207710
- const dbs = seam4.__lecodesDbs ?? [];
207711
- seam4.__lecodesDbs = [];
207836
+ const dbs = seam5.__lecodesDbs ?? [];
207837
+ seam5.__lecodesDbs = [];
207712
207838
  return dbs;
207713
207839
  };
207714
207840
  var init_defineDb = __esm(() => {
207715
- seam4 = globalThis;
207841
+ seam5 = globalThis;
207716
207842
  });
207717
207843
 
207718
207844
  // ../../sdk/src/server/validate.ts
@@ -207827,7 +207953,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
207827
207953
  var isDb = (v) => typeof v === "object" && v !== null && typeof v.$schema === "string", loadServerModules = (reg = globalThis.__lecodesServer) => {
207828
207954
  if (!reg)
207829
207955
  throw new Error("loadServerModules: no server bundle registered (globalThis.__lecodesServer)");
207830
- const out = { endpoints: new Map, channels: new Map, dbs: [], manifest: reg.manifest };
207956
+ const out = { endpoints: new Map, channels: new Map, pushChannels: new Map, dbs: [], manifest: reg.manifest };
207831
207957
  for (const [mod, exports] of Object.entries(reg.modules)) {
207832
207958
  const kinds = new Map((reg.manifest?.modules?.[mod] ?? []).map((e) => [e.name, e.kind]));
207833
207959
  for (const [name, value] of Object.entries(exports)) {
@@ -207838,6 +207964,9 @@ var isDb = (v) => typeof v === "object" && v !== null && typeof v.$schema === "s
207838
207964
  } else if (isChannel(value)) {
207839
207965
  value.__id = id;
207840
207966
  out.channels.set(id, value);
207967
+ } else if (isPushChannel(value)) {
207968
+ value.__id = id;
207969
+ out.pushChannels.set(id, value);
207841
207970
  } else if (isDb(value))
207842
207971
  out.dbs.push(value);
207843
207972
  }
@@ -207857,9 +207986,10 @@ var isDb = (v) => typeof v === "object" && v !== null && typeof v.$schema === "s
207857
207986
  if (name !== "MarciEmbeddedError" && name !== "MarciHttpError" || typeof kind !== "string")
207858
207987
  return null;
207859
207988
  return DB_ERROR_STATUS[kind] ?? null;
207860
- }, als, installProvider = () => setRequestProvider(() => als.getStore()), withSession = (r, rc) => {
207861
- const issued = rc.auth?.issuedToken;
207862
- return issued ? { ...r, session: issued } : r;
207989
+ }, als, installProvider = () => setRequestProvider(() => als.getStore()), envelope = (server2) => (r, rc) => {
207990
+ const s = rc.auth;
207991
+ const out = s?.issuedToken ? { ...r, session: s.issuedToken } : r;
207992
+ return s && s.userId !== null && !s.pushAddress && server2.pushChannels.size ? { ...out, push: true } : out;
207863
207993
  }, createInvoker = (server2, opts = {}) => {
207864
207994
  installProvider();
207865
207995
  const validate = opts.validate ?? true;
@@ -207867,6 +207997,7 @@ var isDb = (v) => typeof v === "object" && v !== null && typeof v.$schema === "s
207867
207997
  const params = server2.manifest.params;
207868
207998
  const auth = opts.auth ?? null;
207869
207999
  const files = opts.files ?? null;
208000
+ const withSession = envelope(server2);
207870
208001
  return async (id, args, ctx = { headers: {} }) => {
207871
208002
  const fn = server2.endpoints.get(id);
207872
208003
  if (!fn)
@@ -207933,6 +208064,7 @@ var isDb = (v) => typeof v === "object" && v !== null && typeof v.$schema === "s
207933
208064
  const onError = opts.onError ?? ((id, e) => console.error(`[server] ${id}:`, e));
207934
208065
  const params = server2.manifest.params;
207935
208066
  const auth = opts.auth ?? null;
208067
+ const withSession = envelope(server2);
207936
208068
  return async (channelId, args = [], ctx = { headers: {} }) => {
207937
208069
  const ch = server2.channels.get(channelId);
207938
208070
  if (!ch)
@@ -207965,6 +208097,7 @@ var isDb = (v) => typeof v === "object" && v !== null && typeof v.$schema === "s
207965
208097
  };
207966
208098
  var init_runtime = __esm(() => {
207967
208099
  init_channel();
208100
+ init_pushChannel();
207968
208101
  init_context2();
207969
208102
  init_defineDb();
207970
208103
  init_validate();
@@ -208111,23 +208244,28 @@ var CODE_TTL_MS, CODE_MAX_ATTEMPTS = 5, SEND_MIN_GAP_MS = 30000, SEND_MAX_PER_10
208111
208244
  return actual.length === expected.length && timingSafeEqual(actual, expected);
208112
208245
  }, checkPassword = (password) => {
208113
208246
  if (typeof password !== "string" || password.length < PASSWORD_MIN)
208114
- throw new ApiError(400, `Password must be at least ${PASSWORD_MIN} characters`);
208247
+ throw new ApiError2(400, `Password must be at least ${PASSWORD_MIN} characters`);
208115
208248
  if (password.length > PASSWORD_MAX)
208116
- throw new ApiError(400, `Password must be at most ${PASSWORD_MAX} characters`);
208249
+ throw new ApiError2(400, `Password must be at most ${PASSWORD_MAX} characters`);
208117
208250
  return password;
208118
208251
  }, EMAIL_RE, normalizeEmail = (email) => {
208119
208252
  const e = typeof email === "string" ? email.trim().toLowerCase() : "";
208120
208253
  if (!e || e.length > 254 || !EMAIL_RE.test(e))
208121
- throw new ApiError(400, "Invalid email");
208254
+ throw new ApiError2(400, "Invalid email");
208122
208255
  return e;
208123
208256
  }, LOGIN_RE, parseLogin = (login) => {
208124
208257
  const l = typeof login === "string" ? login.trim() : "";
208125
208258
  if (l.includes("@"))
208126
208259
  return { provider: "email", subject: normalizeEmail(l) };
208127
208260
  if (!LOGIN_RE.test(l))
208128
- throw new ApiError(400, "Login must be 3–32 letters, digits, dots, dashes or underscores");
208261
+ throw new ApiError2(400, "Login must be 3–32 letters, digits, dots, dashes or underscores");
208129
208262
  return { provider: "login", subject: l.toLowerCase() };
208130
- }, identityKey = (provider, subject) => `${provider}:${subject}`, window2 = (map, key, now, span) => {
208263
+ }, identityKey = (provider, subject) => `${provider}:${subject}`, PUSH_ADDRESS_HEADER = "x-lecodes-push", PUSH_ADDRESS_RE, pushAddressOf = (ctx) => {
208264
+ const headers = ctx.headers ?? {};
208265
+ const raw = headers[PUSH_ADDRESS_HEADER] ?? Object.entries(headers).find(([k]) => k.toLowerCase() === PUSH_ADDRESS_HEADER)?.[1];
208266
+ const value = typeof raw === "string" ? raw.trim() : "";
208267
+ return PUSH_ADDRESS_RE.test(value) ? value : null;
208268
+ }, window2 = (map, key, now, span) => {
208131
208269
  const list = (map.get(key) ?? []).filter((x) => now - x < span);
208132
208270
  map.set(key, list);
208133
208271
  return list;
@@ -208153,14 +208291,15 @@ var CODE_TTL_MS, CODE_MAX_ATTEMPTS = 5, SEND_MIN_GAP_MS = 30000, SEND_MAX_PER_10
208153
208291
  const requireSignedIn = (ctx) => {
208154
208292
  const id = stateOf(ctx).userId;
208155
208293
  if (id === null)
208156
- throw new ApiError(401, "Sign in required");
208294
+ throw new ApiError2(401, "Sign in required");
208157
208295
  return id;
208158
208296
  };
208159
208297
  const mint = async (ctx) => {
208160
208298
  const token = randomToken();
208161
- const { id } = await sessions.insert({ tokenHash: await sha2562(token) });
208162
- const state = { sessionId: id, userId: null, issuedToken: token };
208163
- sessionCache.set(token, { sessionId: id, userId: null, at: now() });
208299
+ const pushAddress = pushAddressOf(ctx);
208300
+ const { id } = await sessions.insert({ tokenHash: await sha2562(token), ...pushAddress ? { pushAddress } : {} });
208301
+ const state = { sessionId: id, userId: null, pushAddress, issuedToken: token };
208302
+ sessionCache.set(token, { sessionId: id, userId: null, pushAddress, at: now() });
208164
208303
  ctx.auth = state;
208165
208304
  ctx.sessionToken = token;
208166
208305
  return state;
@@ -208181,6 +208320,15 @@ var CODE_TTL_MS, CODE_MAX_ATTEMPTS = 5, SEND_MIN_GAP_MS = 30000, SEND_MAX_PER_10
208181
208320
  if (opts.rotate)
208182
208321
  await rotate(ctx);
208183
208322
  };
208323
+ const followPushAddress = async (token, sessionId, held, told) => {
208324
+ if (!told || told === held)
208325
+ return held;
208326
+ await sessions.update(sessionId, { pushAddress: told });
208327
+ const c = sessionCache.get(token);
208328
+ if (c)
208329
+ c.pushAddress = told;
208330
+ return told;
208331
+ };
208184
208332
  const resolve = async (ctx) => {
208185
208333
  if (!tables)
208186
208334
  return;
@@ -208188,16 +208336,18 @@ var CODE_TTL_MS, CODE_MAX_ATTEMPTS = 5, SEND_MIN_GAP_MS = 30000, SEND_MAX_PER_10
208188
208336
  if (token) {
208189
208337
  const c = sessionCache.get(token);
208190
208338
  if (c && now() - c.at < CACHE_TTL_MS) {
208191
- ctx.auth = { sessionId: c.sessionId, userId: c.userId };
208339
+ const pushAddress = await followPushAddress(token, c.sessionId, c.pushAddress, pushAddressOf(ctx));
208340
+ ctx.auth = { sessionId: c.sessionId, userId: c.userId, pushAddress };
208192
208341
  return;
208193
208342
  }
208194
- const row = await sessions.where({ tokenHash: await sha2562(token) }).select({ id: true, revoked: true, lastSeenAt: true, user: { id: true } }).first();
208343
+ const row = await sessions.where({ tokenHash: await sha2562(token) }).select({ id: true, revoked: true, lastSeenAt: true, pushAddress: true, user: { id: true } }).first();
208195
208344
  if (row && !row.revoked) {
208196
208345
  const userId = row.user ? row.user.id : null;
208197
208346
  if (now() - (row.lastSeenAt ?? 0) > SEEN_UPDATE_MS)
208198
208347
  sessions.update(row.id, { lastSeenAt: now() }).then(undefined, () => {});
208199
- sessionCache.set(token, { sessionId: row.id, userId, at: now() });
208200
- ctx.auth = { sessionId: row.id, userId };
208348
+ sessionCache.set(token, { sessionId: row.id, userId, pushAddress: row.pushAddress ?? null, at: now() });
208349
+ const pushAddress = await followPushAddress(token, row.id, row.pushAddress ?? null, pushAddressOf(ctx));
208350
+ ctx.auth = { sessionId: row.id, userId, pushAddress };
208201
208351
  return;
208202
208352
  }
208203
208353
  }
@@ -208210,7 +208360,7 @@ var CODE_TTL_MS, CODE_MAX_ATTEMPTS = 5, SEND_MIN_GAP_MS = 30000, SEND_MAX_PER_10
208210
208360
  const ident = await findIdentity("email", email);
208211
208361
  if (ident) {
208212
208362
  if (ident.user.id !== userId)
208213
- throw new ApiError(409, "This email belongs to another account");
208363
+ throw new ApiError2(409, "This email belongs to another account");
208214
208364
  if (!ident.verifiedAt)
208215
208365
  await identities.update(ident.id, { verifiedAt: now(), hash: null });
208216
208366
  } else if (create) {
@@ -208223,7 +208373,7 @@ var CODE_TTL_MS, CODE_MAX_ATTEMPTS = 5, SEND_MIN_GAP_MS = 30000, SEND_MAX_PER_10
208223
208373
  const { provider, subject } = parseLogin(rawLogin);
208224
208374
  const password = checkPassword(rawPassword);
208225
208375
  if (await findIdentity(provider, subject))
208226
- throw new ApiError(409, provider === "email" ? "An account with this email already exists" : "This login is taken");
208376
+ throw new ApiError2(409, provider === "email" ? "An account with this email already exists" : "This login is taken");
208227
208377
  const userId = await createUser(data);
208228
208378
  await createIdentity(userId, provider, subject, { hash: await hashPassword(password) });
208229
208379
  await bind(ctx, userId, { rotate: true });
@@ -208238,13 +208388,13 @@ var CODE_TTL_MS, CODE_MAX_ATTEMPTS = 5, SEND_MIN_GAP_MS = 30000, SEND_MAX_PER_10
208238
208388
  const byLogin = window2(passwordFails, `l:${who ? identityKey(who.provider, who.subject) : ""}`, t, 10 * 60000);
208239
208389
  const byIp = ctx.ip ? window2(passwordFails, `ip:${ctx.ip}`, t, 10 * 60000) : [];
208240
208390
  if (byLogin.length >= PASSWORD_FAILS_PER_LOGIN || byIp.length >= PASSWORD_FAILS_PER_IP)
208241
- throw new ApiError(429, "Too many attempts — try again later");
208391
+ throw new ApiError2(429, "Too many attempts — try again later");
208242
208392
  const ident = who ? await findIdentity(who.provider, who.subject) : null;
208243
208393
  const ok = !!ident?.hash && typeof rawPassword === "string" && await verifyPassword(rawPassword, ident.hash);
208244
208394
  if (!ok) {
208245
208395
  byLogin.push(t);
208246
208396
  byIp.push(t);
208247
- throw new ApiError(401, "Invalid login or password");
208397
+ throw new ApiError2(401, "Invalid login or password");
208248
208398
  }
208249
208399
  await bind(ctx, ident.user.id, { rotate: true });
208250
208400
  };
@@ -208255,7 +208405,7 @@ var CODE_TTL_MS, CODE_MAX_ATTEMPTS = 5, SEND_MIN_GAP_MS = 30000, SEND_MAX_PER_10
208255
208405
  const mine = await identities.where({ user: { id: userId } }).select({ id: true, provider: true });
208256
208406
  const typed = mine.filter((i) => i.provider === "email" || i.provider === "login");
208257
208407
  if (!typed.length)
208258
- throw new ApiError(400, "Your account has no email or login to sign in with a password — sign in with a code first");
208408
+ throw new ApiError2(400, "Your account has no email or login to sign in with a password — sign in with a code first");
208259
208409
  const hash = await hashPassword(password);
208260
208410
  for (const i of typed)
208261
208411
  await identities.update(i.id, { hash });
@@ -208272,9 +208422,9 @@ var CODE_TTL_MS, CODE_MAX_ATTEMPTS = 5, SEND_MIN_GAP_MS = 30000, SEND_MAX_PER_10
208272
208422
  const t = now();
208273
208423
  const recent = window2(sends, email, t, 10 * 60000);
208274
208424
  if (recent.length && t - recent[recent.length - 1] < SEND_MIN_GAP_MS)
208275
- throw new ApiError(429, `Try again in ${Math.ceil((SEND_MIN_GAP_MS - (t - recent[recent.length - 1])) / 1000)} s`);
208425
+ throw new ApiError2(429, `Try again in ${Math.ceil((SEND_MIN_GAP_MS - (t - recent[recent.length - 1])) / 1000)} s`);
208276
208426
  if (recent.length >= SEND_MAX_PER_10MIN)
208277
- throw new ApiError(429, "Too many codes requested — try again later");
208427
+ throw new ApiError2(429, "Too many codes requested — try again later");
208278
208428
  const code = randomCode();
208279
208429
  await sessions.update(s.sessionId, { pendingEmail: email, codeHash: await codeHash(s.sessionId, code), codeExpires: t + CODE_TTL_MS, codeAttempts: 0 });
208280
208430
  recent.push(t);
@@ -208303,12 +208453,12 @@ var CODE_TTL_MS, CODE_MAX_ATTEMPTS = 5, SEND_MIN_GAP_MS = 30000, SEND_MAX_PER_10
208303
208453
  if (!isDevCode) {
208304
208454
  const row = await sessions.where({ id: s.sessionId }).select({ pendingEmail: true, codeHash: true, codeExpires: true, codeAttempts: true }).first();
208305
208455
  if (!row?.codeHash || row.pendingEmail !== email || !row.codeExpires || row.codeExpires < now())
208306
- throw new ApiError(400, "Code expired — request a new one");
208456
+ throw new ApiError2(400, "Code expired — request a new one");
208307
208457
  if (row.codeAttempts >= CODE_MAX_ATTEMPTS)
208308
- throw new ApiError(429, "Too many attempts — request a new code");
208458
+ throw new ApiError2(429, "Too many attempts — request a new code");
208309
208459
  if (typeof code !== "string" || await codeHash(s.sessionId, code.trim()) !== row.codeHash) {
208310
208460
  await sessions.update(s.sessionId, { codeAttempts: { $increment: 1 } });
208311
- throw new ApiError(400, "Invalid code");
208461
+ throw new ApiError2(400, "Invalid code");
208312
208462
  }
208313
208463
  }
208314
208464
  const userId = await userOfProvenEmail(email);
@@ -208343,7 +208493,7 @@ var CODE_TTL_MS, CODE_MAX_ATTEMPTS = 5, SEND_MIN_GAP_MS = 30000, SEND_MAX_PER_10
208343
208493
  signIn: async (ctx, userId) => {
208344
208494
  requireAuth();
208345
208495
  if (!await users.where({ id: userId }).select({ id: true }).first())
208346
- throw new ApiError(404, "No such user");
208496
+ throw new ApiError2(404, "No such user");
208347
208497
  await bind(ctx, userId, { rotate: true });
208348
208498
  },
208349
208499
  signOut: async (ctx) => {
@@ -208389,6 +208539,92 @@ var init_host = __esm(() => {
208389
208539
  enc = new TextEncoder;
208390
208540
  EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
208391
208541
  LOGIN_RE = /^[\p{L}\p{N}_.-]{3,32}$/u;
208542
+ PUSH_ADDRESS_RE = /^[A-Za-z0-9_-]{1,128}$/;
208543
+ });
208544
+
208545
+ // ../../sdk/src/server/push/host.ts
208546
+ var PUSH_BATCH = 500, chunks = (list, size) => {
208547
+ const out = [];
208548
+ for (let i = 0;i < list.length; i += size)
208549
+ out.push(list.slice(i, i + size));
208550
+ return out;
208551
+ }, createPushHost = (cfg) => {
208552
+ const sessions = cfg.db?.$auth?.session ?? null;
208553
+ const relay = cfg.relay ?? null;
208554
+ const log = cfg.log ?? ((level, text) => level === "warn" ? console.warn(text) : console.info(text));
208555
+ const warned = new Set;
208556
+ const devices = async (userIds) => {
208557
+ const rows = await sessions.where({ user: { id: { $in: userIds } }, revoked: false }).select({ id: true, pushAddress: true });
208558
+ const byAddress = new Map;
208559
+ for (const row of rows) {
208560
+ if (!row.pushAddress)
208561
+ continue;
208562
+ const list = byAddress.get(row.pushAddress) ?? [];
208563
+ list.push(row.id);
208564
+ byAddress.set(row.pushAddress, list);
208565
+ }
208566
+ return byAddress;
208567
+ };
208568
+ const forget = async (byAddress, addresses) => {
208569
+ const ids = addresses.flatMap((a) => byAddress.get(a) ?? []);
208570
+ if (!ids.length)
208571
+ return;
208572
+ await sessions.where({ id: { $in: ids } }).updateMany({ pushAddress: null });
208573
+ };
208574
+ const send = async (channelId, userIds, message) => {
208575
+ if (!sessions) {
208576
+ throw new Error("pushChannel.send: the project's database has no sign-in — a user's devices are the sessions of defineDb({...}).withAuth({ model }), without it there is no one to send to");
208577
+ }
208578
+ if (!relay) {
208579
+ if (!warned.has(channelId)) {
208580
+ warned.add(channelId);
208581
+ log("warn", `${channelId}: notifications are not sent from this backend (no road to the platform's sender) — a deployed backend sends them`);
208582
+ }
208583
+ return { sent: 0 };
208584
+ }
208585
+ const byAddress = await devices(userIds);
208586
+ if (byAddress.size === 0)
208587
+ return { sent: 0 };
208588
+ const wire = {
208589
+ channel: channelId,
208590
+ title: message.title,
208591
+ ...message.body !== undefined ? { body: message.body } : {},
208592
+ ...message.url !== undefined ? { url: message.url } : {},
208593
+ ...message.data !== undefined ? { data: message.data } : {},
208594
+ ...message.badge !== undefined ? { badge: message.badge } : {}
208595
+ };
208596
+ let sent = 0;
208597
+ for (let batch of chunks([...byAddress.keys()], PUSH_BATCH)) {
208598
+ let result = await relay(batch, wire);
208599
+ if (result.unknown?.length) {
208600
+ const unknown = new Set(result.unknown);
208601
+ await forget(byAddress, [...unknown]);
208602
+ batch = batch.filter((a) => !unknown.has(a));
208603
+ if (!batch.length)
208604
+ continue;
208605
+ result = await relay(batch, wire);
208606
+ if (result.unknown?.length)
208607
+ await forget(byAddress, result.unknown);
208608
+ }
208609
+ sent += result.sent;
208610
+ const dead = result.failed.filter((f) => f.reason === "disabled").map((f) => f.address);
208611
+ if (dead.length)
208612
+ await forget(byAddress, dead);
208613
+ }
208614
+ return { sent };
208615
+ };
208616
+ return {
208617
+ get enabled() {
208618
+ return relay !== null;
208619
+ },
208620
+ install() {
208621
+ setPushSender(send);
208622
+ },
208623
+ send
208624
+ };
208625
+ };
208626
+ var init_host2 = __esm(() => {
208627
+ init_pushChannel();
208392
208628
  });
208393
208629
 
208394
208630
  // ../../sdk/src/server/files/host.ts
@@ -208787,7 +209023,7 @@ var UploadError, UUID, JOURNAL = "lecodes-files", MB, TMP_MAX_AGE_MS, MAX_IMAGE_
208787
209023
  }
208788
209024
  };
208789
209025
  };
208790
- var init_host2 = __esm(() => {
209026
+ var init_host3 = __esm(() => {
208791
209027
  init_context2();
208792
209028
  init_models2();
208793
209029
  UploadError = class UploadError extends Error {
@@ -208860,15 +209096,17 @@ var describeField = (f) => {
208860
209096
  models,
208861
209097
  endpoints: [...server2.endpoints.keys()].map((id) => ({ id, params: params[id] ?? null })),
208862
209098
  channels: [...server2.channels.keys()],
209099
+ pushChannels: [...server2.pushChannels.keys()],
208863
209100
  auth: { model: db?.$auth?.model ?? null },
208864
209101
  appName: server2.manifest.app?.name
208865
209102
  };
208866
209103
  };
208867
- var init_host3 = __esm(() => {
209104
+ var init_host4 = __esm(() => {
208868
209105
  init_runtime();
208869
209106
  init_defineDb();
208870
209107
  init_host();
208871
209108
  init_host2();
209109
+ init_host3();
208872
209110
  init_models2();
208873
209111
  init_models();
208874
209112
  });
@@ -209891,7 +210129,7 @@ var require_main = __commonJS(function(exports, module) {
209891
210129
  });
209892
210130
 
209893
210131
  // src/dev/backendWorker.ts
209894
- import { existsSync as existsSync54, mkdirSync as mkdirSync35, mkdtempSync as mkdtempSync8, renameSync as renameSync3, rmSync as rmSync18 } from "node:fs";
210132
+ import { existsSync as existsSync55, mkdirSync as mkdirSync35, mkdtempSync as mkdtempSync8, renameSync as renameSync3, rmSync as rmSync18 } from "node:fs";
209895
210133
  import { readFile as readFile3 } from "node:fs/promises";
209896
210134
  import { tmpdir as tmpdir8 } from "node:os";
209897
210135
  import { join as join58 } from "node:path";
@@ -209935,13 +210173,23 @@ var post = (m) => {
209935
210173
  rmSync18(aside, { recursive: true, force: true });
209936
210174
  renameSync3(dir, aside);
209937
210175
  rmSync18(`${filesDir}-old`, { recursive: true, force: true });
209938
- if (existsSync54(filesDir))
210176
+ if (existsSync55(filesDir))
209939
210177
  renameSync3(filesDir, `${filesDir}-old`);
209940
210178
  mkdirSync35(dir, { recursive: true });
209941
210179
  db = openDatabase(dir);
209942
210180
  await db.$sync(schema);
209943
210181
  return [`database started over — the schema change couldn't be migrated (${reason}); the previous data is in ${aside}`];
209944
210182
  }
210183
+ }, pushRelay = (sender) => async (to, message) => {
210184
+ try {
210185
+ return await sendPush(sender.apiUrl, sender.token, sender.uuid, { to, ...message });
210186
+ } catch (e) {
210187
+ const body = e instanceof ApiError ? e.body : null;
210188
+ if (body?.code === "unknown_addresses" && Array.isArray(body.addresses)) {
210189
+ return { sent: 0, failed: [], unknown: body.addresses.filter((a) => typeof a === "string") };
210190
+ }
210191
+ throw new Error(`push: the platform refused the send: ${e.message}`);
210192
+ }
209945
210193
  }, load = async (m) => {
209946
210194
  const noDb = () => {
209947
210195
  throw new Error("db: the database is not open yet — query it inside an endpoint, not while the module loads");
@@ -209960,6 +210208,11 @@ var post = (m) => {
209960
210208
  log: (level, text) => post({ t: "log", level, text })
209961
210209
  });
209962
210210
  authHost.install();
210211
+ createPushHost({
210212
+ db: server2.dbs.find((d) => d.$auth) ?? null,
210213
+ relay: m.push ? pushRelay(m.push) : null,
210214
+ log: (level, text) => post({ t: "log", level, text })
210215
+ }).install();
209963
210216
  const auth = authHost.enabled ? authHost : null;
209964
210217
  const onError = (id, e) => post({ t: "log", level: "error", text: `${id}: ${e?.stack ?? e}` });
209965
210218
  const files = createFilesHost({
@@ -209982,8 +210235,9 @@ var post = (m) => {
209982
210235
  post({ t: "loaded", describe, notes });
209983
210236
  };
209984
210237
  var init_backendWorker = __esm(() => {
209985
- init_host3();
210238
+ init_host4();
209986
210239
  init_peerInstall();
210240
+ init_api();
209987
210241
  console.log = hook("log");
209988
210242
  console.info = hook("info");
209989
210243
  console.warn = hook("warn");
@@ -210028,7 +210282,7 @@ var init_backendWorker = __esm(() => {
210028
210282
  // src/index.ts
210029
210283
  init_package();
210030
210284
  init_cli();
210031
- import { existsSync as existsSync55, realpathSync as realpathSync2 } from "node:fs";
210285
+ import { existsSync as existsSync56, realpathSync as realpathSync2 } from "node:fs";
210032
210286
  import { delimiter as delimiter3, dirname as dirname28, join as join59 } from "node:path";
210033
210287
 
210034
210288
  // src/commands/app/index.ts
@@ -210635,6 +210889,9 @@ var readManifest2 = (dir, id) => {
210635
210889
  };
210636
210890
  var readManifestIfAny = (dir, id) => existsSync14(join17(dir, MANIFEST_FILE)) ? readManifest2(dir, id) : null;
210637
210891
 
210892
+ // src/commands/app/shared.ts
210893
+ init_compile();
210894
+
210638
210895
  // src/plugin/emitPackage.ts
210639
210896
  class PackageError extends Error {
210640
210897
  }
@@ -210731,7 +210988,12 @@ var urlSchemeOf = (config) => {
210731
210988
  throw new CliError(`app.json "urlScheme" is the app's OWN scheme ("myshop" → myshop://…), not ${scheme}.`);
210732
210989
  return scheme;
210733
210990
  };
210734
- var readAppConfigAt = (root) => readJson2(join18(root, "app.json"));
210991
+ var readAppConfigAt = (root) => {
210992
+ const config = readJson2(join18(root, "app.json"));
210993
+ if (config && "plugins" in config)
210994
+ config.plugins = pluginSpecsOf(config.plugins);
210995
+ return config;
210996
+ };
210735
210997
  var DESKTOP_DEFAULT_WINDOW = { width: 1280, height: 720 };
210736
210998
  var resolveDesktopRenderer = (root, renderer) => {
210737
210999
  const raw = renderer ?? process.env.LECODES_DESKTOP_RENDERER ?? readAppConfigAt(root)?.desktop?.renderer;
@@ -210851,25 +211113,33 @@ var detectEngines = (bundleJs) => {
210851
211113
  }
210852
211114
  const header = lines.join(`
210853
211115
  `);
210854
- return { gl: !/^\/\/ gl: disable$/m.test(header), g2: /^\/\/ 2d: enable$/m.test(header) };
211116
+ const on = (name) => new RegExp(`^// ${name}: enable$`, "m").test(header);
211117
+ return { gl: !/^\/\/ gl: disable$/m.test(header), g2: on("2d"), physics: on("physics"), net: on("net"), nav: on("nav"), audio: on("audio") };
211118
+ };
211119
+ var VARIANT_LACKS = {
211120
+ full: [],
211121
+ "3d": ["g2", "physics", "net", "nav", "audio"],
211122
+ "2d": ["gl", "physics", "net", "nav"],
211123
+ core: ["gl", "g2", "physics", "net", "nav"]
210855
211124
  };
211125
+ var ENGINE_WORDS = { gl: "3D", g2: "2D", physics: "physics", net: "multiplayer", nav: "navigation", audio: "game audio" };
211126
+ var lacks = (variant, uses) => VARIANT_LACKS[variant].filter((e) => uses[e]).map((e) => ENGINE_WORDS[e]);
210856
211127
  var resolveVariant = (requested, bundleJs, configKey = "ios.variant") => {
210857
211128
  const uses = detectEngines(bundleJs);
210858
- const detected = uses.gl && uses.g2 ? "full" : uses.gl ? "3d" : uses.g2 ? "2d" : "core";
211129
+ const detected = SDK_VARIANTS.find((v) => lacks(v, uses).length === 0) ?? "full";
211130
+ const inUse = Object.keys(ENGINE_WORDS).filter((e) => uses[e]).map((e) => ENGINE_WORDS[e]);
211131
+ const why = inUse.length > 0 ? `uses ${inUse.join(" + ")}` : "UI only, no engine use detected";
210859
211132
  if (requested === "auto")
210860
- return { variant: detected, detected };
211133
+ return { variant: detected, detected, why };
210861
211134
  const variant = requested;
210862
- const missing = [
210863
- uses.gl && variant !== "3d" && variant !== "full" ? "3D" : null,
210864
- uses.g2 && variant !== "2d" && variant !== "full" ? "2D" : null
210865
- ].filter(Boolean);
211135
+ const missing = lacks(variant, uses);
210866
211136
  if (missing.length > 0) {
210867
- warn(`project uses the ${missing.join(" + ")} engine but ${configKey} forces "${variant}" — those scenes won't run on device; remove ${configKey} to auto-select "${detected}".`);
211137
+ warn(`project uses ${missing.join(" + ")} but ${configKey} forces "${variant}", which has no such engine — that part won't run on device; remove ${configKey} to auto-select "${detected}".`);
210868
211138
  }
210869
- return { variant, detected };
211139
+ return { variant, detected, why };
210870
211140
  };
210871
211141
  var ANDROID_SDK_VARIANTS = [...SDK_VARIANTS, "vulkan"];
210872
- var ANDROID_SDK_VULKAN_MIN = "2.1.0";
211142
+ var ANDROID_SDK_VULKAN_MIN = "2.2.11";
210873
211143
  var androidSdkArtifact = (variant, sdkVersion) => {
210874
211144
  if (variant === "vulkan" && sdkVersion && cmpVersion(sdkVersion, ANDROID_SDK_VULKAN_MIN) < 0) {
210875
211145
  throw new CliError(`android.variant "vulkan" needs lecodes-android-sdk >= ${ANDROID_SDK_VULKAN_MIN} (pinned: ${sdkVersion}) — run \`lecodes app update\` or set android.sdk.`);
@@ -212992,7 +213262,7 @@ engines the bundle uses and cannot see those, so a project using \`Physics\`,
212992
213262
 
212993
213263
  \`android.variant: "vulkan"\` links \`lecodes-sdk-vulkan\`: the full engine set with Filament on
212994
213264
  Vulkan (OpenGL ES where the device has no Vulkan 1.1), \`arm64-v8a\` only — for 3D games. Never
212995
- auto-selected; needs lecodes-android-sdk >= 2.1.0.
213265
+ auto-selected; needs lecodes-android-sdk >= 2.2.11.
212996
213266
 
212997
213267
  ## App-local native code
212998
213268
 
@@ -213757,7 +214027,7 @@ var initAndroid = async (opts) => {
213757
214027
  if (existsSync19(androidDir)) {
213758
214028
  throw new CliError("android/ already exists — run `lecodes app sync` to update it.");
213759
214029
  }
213760
- const existing = readJson2(join21(root, "app.json")) ?? {};
214030
+ const existing = readAppConfigAt(root) ?? {};
213761
214031
  const variant = opts.variant ?? existing.android?.variant ?? "auto";
213762
214032
  if (variant !== "auto" && !ANDROID_SDK_VARIANTS.includes(variant)) {
213763
214033
  throw new CliError(`--variant must be auto, ${ANDROID_SDK_VARIANTS.join(", ")} (got "${variant}")`);
@@ -213868,20 +214138,14 @@ var syncAndroid = async (root, config, prior) => {
213868
214138
  if (requestedVariant !== "auto" && !ANDROID_SDK_VARIANTS.includes(requestedVariant)) {
213869
214139
  throw new CliError(`app.json android.variant must be "auto" or one of ${ANDROID_SDK_VARIANTS.join(", ")} (got "${requestedVariant}").`);
213870
214140
  }
213871
- const { variant: covered, detected } = resolveVariant(requestedVariant === "auto" ? "auto" : androidVariantEngines(requestedVariant), bundleJs, "android.variant");
214141
+ const { variant: covered, why } = resolveVariant(requestedVariant === "auto" ? "auto" : androidVariantEngines(requestedVariant), bundleJs, "android.variant");
213872
214142
  const variant = requestedVariant === "vulkan" ? "vulkan" : covered;
213873
214143
  let sdkArtifact = androidSdkArtifact(variant, sdkVersion);
213874
214144
  if (sdkArtifact !== "lecodes-sdk" && cmpVersion(sdkVersion, ANDROID_SDK_VARIANTS_MIN) < 0) {
213875
214145
  warn(`lecodes-android-sdk ${sdkVersion} predates the variant matrix (${ANDROID_SDK_VARIANTS_MIN}) — linking the full lecodes-sdk AAR; run \`lecodes app update\` to pick up variant builds.`);
213876
214146
  sdkArtifact = "lecodes-sdk";
213877
214147
  }
213878
- const WHY = {
213879
- full: "3D + 2D engines in use",
213880
- "3d": "3D in use, no 2D",
213881
- "2d": "2D in use, no 3D",
213882
- core: "no 3D/2D engine use detected"
213883
- };
213884
- log(`${c.green("variant")} ${variant} → ${sdkArtifact}${requestedVariant === "auto" ? c.dim(` (auto: ${WHY[detected]})`) : c.dim(" (forced by android.variant)")}`);
214148
+ log(`${c.green("variant")} ${variant} → ${sdkArtifact}${requestedVariant === "auto" ? c.dim(` (auto: ${why})`) : c.dim(" (forced by android.variant)")}`);
213885
214149
  const lockPath = join21(root, "plugins.lock.json");
213886
214150
  const oldLock = readJson2(lockPath)?.plugins ?? {};
213887
214151
  const pluginSpecs = Object.entries(config.plugins ?? {});
@@ -214114,7 +214378,7 @@ var androidBuildFlags = {
214114
214378
  };
214115
214379
  var buildAndroid = async (flags) => {
214116
214380
  const root = projectRoot();
214117
- const config = readJson2(join21(root, "app.json"));
214381
+ const config = readAppConfigAt(root);
214118
214382
  const androidDir = join21(root, "android");
214119
214383
  if (!config?.android || !existsSync19(androidDir)) {
214120
214384
  throw new CliError("No Android shell here — run `lecodes app init android` first.");
@@ -214318,7 +214582,7 @@ var writeSigningConfig = (root, config, storeFile, alias, storePassword, keyPass
214318
214582
  }
214319
214583
  }
214320
214584
  const configPath = join21(root, "app.json");
214321
- const fresh = readJson2(configPath) ?? config;
214585
+ const fresh = readAppConfigAt(root) ?? config;
214322
214586
  fresh.android = { ...fresh.android ?? {}, signing: { storeFile: envPath, keyAlias: alias } };
214323
214587
  writeJson(configPath, fresh);
214324
214588
  log(`${c.green("written")} app.json (android.signing — metadata only, no passwords)`);
@@ -214334,7 +214598,7 @@ var printCertificate = (storeFile, alias, storePassword) => {
214334
214598
  var PLAY_APP_SIGNING_NOTE = "With Play App Signing (default for new Play apps) this is the UPLOAD key — Google keeps the app signing key, and a lost upload key can be reset through Play support. Without it, losing this file means the app can never be updated. Back it up either way.";
214335
214599
  var keystoreProject = () => {
214336
214600
  const root = projectRoot();
214337
- return { root, config: readJson2(join21(root, "app.json")) ?? {} };
214601
+ return { root, config: readAppConfigAt(root) ?? {} };
214338
214602
  };
214339
214603
  var passwordFlag = { password: str("keystore password (else asked on the terminal)", { value: "<pw>" }) };
214340
214604
  var keystoreCreateFlags = {
@@ -215465,7 +215729,7 @@ var dirSize2 = (dir) => readdirSync8(dir, { withFileTypes: true }).reduce((sum,
215465
215729
  }, 0);
215466
215730
  var initDesktop = async (opts) => {
215467
215731
  const root = projectRoot();
215468
- const existing = readJson2(join24(root, "app.json")) ?? {};
215732
+ const existing = readAppConfigAt(root) ?? {};
215469
215733
  const name = opts.name ?? existing.name ?? basename8(root);
215470
215734
  const window2 = {
215471
215735
  width: 1280,
@@ -215499,7 +215763,7 @@ var desktopBuildFlags = {
215499
215763
  };
215500
215764
  var buildDesktop = async (flags) => {
215501
215765
  const root = projectRoot();
215502
- const config = readJson2(join24(root, "app.json")) ?? {};
215766
+ const config = readAppConfigAt(root) ?? {};
215503
215767
  const name = (config.name ?? basename8(root)).trim() || "app";
215504
215768
  const slug = flags.slug ?? cleanFileName(name);
215505
215769
  const out = resolve11(root, flags.out ?? join24(root, "dist", slug));
@@ -215776,7 +216040,7 @@ var initIos = async (opts) => {
215776
216040
  if (existsSync23(iosDir)) {
215777
216041
  throw new CliError("ios/ already exists — run `lecodes app sync` to update it.");
215778
216042
  }
215779
- const existing = readJson2(join25(root, "app.json")) ?? {};
216043
+ const existing = readAppConfigAt(root) ?? {};
215780
216044
  const variant = opts.variant ?? existing.ios?.variant ?? "auto";
215781
216045
  if (variant !== "auto" && !SDK_VARIANTS.includes(variant)) {
215782
216046
  throw new CliError(`--variant must be auto, ${SDK_VARIANTS.join(", ")} (got "${variant}")`);
@@ -215865,7 +216129,7 @@ var init = defineCommand({
215865
216129
  });
215866
216130
  var runSync = async (platformArg) => {
215867
216131
  const root = projectRoot();
215868
- const config = readJson2(join25(root, "app.json"));
216132
+ const config = readAppConfigAt(root);
215869
216133
  const platform = mobilePlatform(platformArg);
215870
216134
  const { hasIos, hasAndroid } = shellsOf(root, config);
215871
216135
  if (platform === "ios" && !hasIos)
@@ -215946,14 +216210,8 @@ var syncIos = async (root, config) => {
215946
216210
  mkdirSync15(resourcesDir, { recursive: true });
215947
216211
  const resources = await compileShell(join25(resourcesDir, "app.js"), { remotePatterns });
215948
216212
  const bundleJs = readFileSync20(join25(resourcesDir, "app.js"), "utf8");
215949
- const { variant, detected } = resolveVariant(requestedVariant, bundleJs);
215950
- const WHY = {
215951
- core: "UI only",
215952
- "2d": "uses the 2D engine",
215953
- "3d": "uses the 3D engine",
215954
- full: "uses the 3D + 2D engines"
215955
- };
215956
- log(`${c.green("variant")} ${variant}${requestedVariant === "auto" ? ` (auto: ${WHY[detected]})` : " (forced by ios.variant)"}`);
216213
+ const { variant, why } = resolveVariant(requestedVariant, bundleJs);
216214
+ log(`${c.green("variant")} ${variant}${requestedVariant === "auto" ? ` (auto: ${why})` : " (forced by ios.variant)"}`);
215957
216215
  const sdkWhy = sdkPin === "latest" ? "latest" : config.ios.sdk ? "pinned by ios.sdk" : "from the shell — set ios.sdk or run `lecodes app update`";
215958
216216
  log(`${c.green("sdk")} lecodes-ios-sdk ${sdkVersion}${c.dim(` (${sdkWhy})`)}`);
215959
216217
  const preload = /^\/\/ preload: (.*)$/m.exec(bundleJs)?.[1].split(";").map((s) => s.trim()).filter(Boolean) ?? [];
@@ -216076,7 +216334,7 @@ Native OTA JS updates at launch (both shells; the current launch runs the newest
216076
216334
  var pluginConfig = () => {
216077
216335
  const root = projectRoot();
216078
216336
  const configPath = join25(root, "app.json");
216079
- const config = readJson2(configPath);
216337
+ const config = readAppConfigAt(root);
216080
216338
  if (!config)
216081
216339
  throw new CliError("No app.json here — run `lecodes app init <ios|android>` first.");
216082
216340
  config.plugins ??= {};
@@ -216181,7 +216439,7 @@ var updateSdkPin = async (root, config, wanted, checkOnly, platform) => {
216181
216439
  };
216182
216440
  var runAppUpdate = async (opts = {}) => {
216183
216441
  const root = projectRoot();
216184
- const config = readJson2(join25(root, "app.json"));
216442
+ const config = readAppConfigAt(root);
216185
216443
  const platform = mobilePlatform(opts.platform);
216186
216444
  const { hasIos, hasAndroid } = shellsOf(root, config);
216187
216445
  if (platform === "ios" && !hasIos)
@@ -216339,7 +216597,7 @@ var doctor = defineCommand({
216339
216597
  examples: ["lecodes app doctor"],
216340
216598
  run: async () => {
216341
216599
  const root = projectRoot();
216342
- const config = readJson2(join25(root, "app.json"));
216600
+ const config = readAppConfigAt(root);
216343
216601
  const { hasIos, hasAndroid } = shellsOf(root, config);
216344
216602
  if (!hasIos && !hasAndroid)
216345
216603
  throw new CliError(NO_IOS_SHELL);
@@ -216386,7 +216644,7 @@ var exportOptionsPlist = (method, teamId, destination) => `<?xml version="1.0" e
216386
216644
  `;
216387
216645
  var requireShell = () => {
216388
216646
  const root = projectRoot();
216389
- const config = readJson2(join25(root, "app.json"));
216647
+ const config = readAppConfigAt(root);
216390
216648
  if (!config?.ios || !existsSync23(join25(root, "ios")))
216391
216649
  throw new CliError(NO_IOS_SHELL);
216392
216650
  return { root, config };
@@ -216499,7 +216757,7 @@ var iosUpload = defineCommand({
216499
216757
  "-allowProvisioningUpdates"
216500
216758
  ]);
216501
216759
  const configPath = join25(root, "app.json");
216502
- const fresh = readJson2(configPath);
216760
+ const fresh = readAppConfigAt(root);
216503
216761
  fresh.ios.buildNumber = (fresh.ios.buildNumber ?? 1) + 1;
216504
216762
  writeJson(configPath, fresh);
216505
216763
  success(`Uploaded build ${(fresh.ios.buildNumber ?? 2) - 1} — check App Store Connect / TestFlight once processing finishes.`);
@@ -216745,7 +217003,7 @@ lecodes render --desktop --view iso --png iso.png # 3D: inspection camera fitt
216745
217003
  # { "view": "iso", "screenshot": "iso.png" } then { "view": "off" }
216746
217004
  lecodes render --desktop --script <file> # the same scenario on the NATIVE host (real GPU, the
216747
217005
  # real hit-test): the same steps, targets and expects as
216748
- # headless (only expect.audio / the json output are headless-only)
217006
+ # headless (only expect.audio / expect.toast / the json output are headless-only)
216749
217007
  lecodes render --sim --script <file> # the WINDOWLESS native host (lecodes-server): the real
216750
217008
  # engines, no GPU, no pixels, no UI hit-test — expect.log /
216751
217009
  # expect.paint only; runs on CI and beside an open desktop window
@@ -216791,11 +217049,12 @@ Three hosts run the same file, at a fixed 16 ms clock on all of them:
216791
217049
  |---|---|---|---|---|
216792
217050
  | headless | (none) | Bun: the runtime + UI renderer + a recording 3D core (no GPU) | names or \`{ x, y }\` | all |
216793
217051
  | sim | \`--sim\` | the windowless native server: real engines, no pixels, no UI | \`{ x, y }\`, straight to the scene | \`log\`, \`paint\` |
216794
- | desktop | \`--desktop\` | the hidden desktop window: pixels, GL materials, the real hit-test | names or \`{ x, y }\` | all but \`audio\` |
217052
+ | desktop | \`--desktop\` | the hidden desktop window: pixels, GL materials, the real hit-test | names or \`{ x, y }\` | all but \`audio\` / \`toast\` |
216795
217053
 
216796
217054
  One flow file, the same verdict on headless and desktop: names resolve the same way (\`name\` →
216797
217055
  exact text → substring → placeholder), every step runs (\`hover\`, \`type\`, \`fill\`, \`scroll\`,
216798
- \`clip\` included) and the semantic expects read the same tree. Only \`expect.audio\` and the
217056
+ \`clip\` included) and the semantic expects read the same tree. Only \`expect.audio\`,
217057
+ \`expect.toast\` (the last \`toast()\` message — a toast is not in the tree) and the
216799
217058
  \`json\` output are the headless renderer's. On the desktop \`type\` taps the input and types into
216800
217059
  it (appends); the sim has no UI, so it takes coordinates and \`expect.log\` / \`expect.paint\` only.
216801
217060
 
@@ -216948,10 +217207,11 @@ init_cli();
216948
217207
  import { basename as basename14 } from "node:path";
216949
217208
 
216950
217209
  // src/dev/projectBackend.ts
216951
- init_host3();
217210
+ init_host4();
216952
217211
  init_projectCompile();
216953
217212
  init_peerInstall();
216954
217213
  init_manifest();
217214
+ init_shared();
216955
217215
  import { rmSync as rmSync9 } from "node:fs";
216956
217216
  import { join as join35, relative as relative5 } from "node:path";
216957
217217
 
@@ -217044,7 +217304,7 @@ var startLocalBackend = (options) => {
217044
217304
  log(`[server:error] the server process exited (${status ?? "killed"}) — it starts again with the next call`);
217045
217305
  }
217046
217306
  });
217047
- child.send({ t: "load", code: bundle, dbDir, filesDir, root: options.root });
217307
+ child.send({ t: "load", code: bundle, dbDir, filesDir, root: options.root, push: options.push ?? null });
217048
217308
  });
217049
217309
  const stop = async (l) => {
217050
217310
  if (live === l)
@@ -217405,7 +217665,15 @@ var createProjectBackend = (options) => {
217405
217665
  subscribe: (_scope, ch, args, ctx) => backend ? backend.subscribe(ch, args, ctx) : Promise.resolve({ ok: false, status: 404, message: "The project has no server files" })
217406
217666
  });
217407
217667
  const sockets = new Set;
217408
- const start = () => startLocalBackend({ root, dbDir, log, onPublish: (ch, group, data) => hub.publish("", ch, group, data) });
217668
+ const push = (() => {
217669
+ try {
217670
+ const { apiUrl, token } = session();
217671
+ return { apiUrl, token, uuid: readManifest(root).uuid };
217672
+ } catch {
217673
+ return null;
217674
+ }
217675
+ })();
217676
+ const start = () => startLocalBackend({ root, dbDir, log, push, onPublish: (ch, group, data) => hub.publish("", ch, group, data) });
217409
217677
  const envelope = (body) => new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json", "cache-control": "no-store", ...CORS } });
217410
217678
  return {
217411
217679
  compile: async (entries) => {
@@ -219979,8 +220247,8 @@ var lightmap_default = defineCommand({
219979
220247
  // src/commands/link.ts
219980
220248
  init_cli();
219981
220249
  init_api();
219982
- import { existsSync as existsSync39 } from "node:fs";
219983
- import { basename as basename21, join as join44, resolve as resolve21 } from "node:path";
220250
+ import { existsSync as existsSync40 } from "node:fs";
220251
+ import { basename as basename21, join as join44, resolve as resolve22 } from "node:path";
219984
220252
  init_ignore();
219985
220253
  init_localFiles();
219986
220254
  init_manifest();
@@ -219990,14 +220258,148 @@ init_types4();
219990
220258
  // src/commands/push.ts
219991
220259
  init_cli();
219992
220260
  init_api();
219993
- import { readFileSync as readFileSync31 } from "node:fs";
220261
+ import { readFileSync as readFileSync32 } from "node:fs";
219994
220262
  import { basename as basename20 } from "node:path";
219995
220263
  init_localFiles();
219996
220264
  init_manifest();
220265
+
220266
+ // src/commands/releases.ts
220267
+ init_cli();
220268
+ init_api();
220269
+ init_localFiles();
220270
+ import { existsSync as existsSync39, readFileSync as readFileSync31 } from "node:fs";
220271
+ import { resolve as resolve21 } from "node:path";
220272
+ init_shared();
220273
+ var compareVersions = (a, b) => {
220274
+ const pa = a.split("."), pb = b.split(".");
220275
+ for (let i = 0;i < Math.max(pa.length, pb.length); i++) {
220276
+ const d = (parseInt(pa[i] ?? "0", 10) || 0) - (parseInt(pb[i] ?? "0", 10) || 0);
220277
+ if (d)
220278
+ return d;
220279
+ }
220280
+ return 0;
220281
+ };
220282
+ var formatSize = (bytes) => bytes < 1024 ? `${bytes} B` : bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1024 / 1024).toFixed(1)} MB`;
220283
+ var qrUrl = (apiUrl, uuid) => `${apiUrl}/qr/${uuid}`;
220284
+ var reportRelease = (release, qr, verb = "Published") => {
220285
+ const parts = [release.unchanged ? `Nothing changed — ${release.version} is current` : `${verb} ${release.version}`, release.url.replace(/\/+$/, "")];
220286
+ if (!release.unchanged) {
220287
+ if (typeof release.size === "number")
220288
+ parts.push(formatSize(release.size));
220289
+ if (release.sdk)
220290
+ parts.push(`SDK ${release.sdk}`);
220291
+ }
220292
+ success(parts.join(" · "));
220293
+ info(` ${c.dim("QR")} ${qr}`);
220294
+ };
220295
+ var withSchemaConfirm = async (call, opts) => {
220296
+ try {
220297
+ return await call(false);
220298
+ } catch (e) {
220299
+ if (!(e instanceof SchemaChangeError))
220300
+ throw e;
220301
+ if (e.incompatible !== null) {
220302
+ throw new CliError(`The production database cannot migrate to this schema in place${e.incompatible ? `: ${e.incompatible}` : ""}. Nothing changed.`, 409);
220303
+ }
220304
+ warn("The schema change deletes data in the production database:");
220305
+ for (const op of e.destructive)
220306
+ log(c.red(` ${op}`));
220307
+ if (!opts.allowDestructive) {
220308
+ if (!process.stdin.isTTY)
220309
+ throw new CliError(`Nothing changed. Run \`${opts.retry} --allow-destructive\` to apply it.`, 409);
220310
+ if (!await confirm("Delete that data and go on?", false)) {
220311
+ info(c.dim("Cancelled — nothing changed."));
220312
+ return null;
220313
+ }
220314
+ }
220315
+ return await call(true);
220316
+ }
220317
+ };
220318
+ var allowDestructiveFlag = { "allow-destructive": bool("apply a schema change that deletes production data without asking") };
220319
+ var use = defineCommand({
220320
+ name: "use",
220321
+ usage: "<version>",
220322
+ summary: "Make a release current again — a rollback, no compile",
220323
+ description: "The release's bundle becomes what the project's address, the viewers and the store run, and its server is deployed to production again (the database migrates to that release's schema: a change that deletes data is shown and asked about, like a publish).",
220324
+ flags: allowDestructiveFlag,
220325
+ examples: ["lecodes releases use 1.2.3"],
220326
+ run: async ({ args, flags }) => {
220327
+ const version = args[0]?.replace(/^v/, "");
220328
+ if (!version)
220329
+ throw new UsageError("Which release? `lecodes releases` lists them.");
220330
+ const { apiUrl, token, manifest } = linkedProject();
220331
+ const releases = await listReleases(apiUrl, token, manifest.uuid);
220332
+ const target = releases.find((r) => r.version === version);
220333
+ if (!target)
220334
+ throw new CliError(`${manifest.name} has no release ${version} — \`lecodes releases\` lists them.`);
220335
+ const previous = releases.find((r) => r.current);
220336
+ const res = await withSchemaConfirm((allowDestructive) => useRelease(apiUrl, token, manifest.uuid, version, { allowDestructive }), { allowDestructive: flags["allow-destructive"], retry: `lecodes releases use ${version}` });
220337
+ if (!res)
220338
+ return;
220339
+ const back = previous !== undefined && compareVersions(res.version, previous.version) < 0;
220340
+ reportRelease({ ...target, ...res }, qrUrl(apiUrl, manifest.uuid), back ? "Rolled back to" : "Published");
220341
+ }
220342
+ });
220343
+ var upload2 = defineCommand({
220344
+ name: "upload",
220345
+ usage: "<app.js>",
220346
+ summary: "Make a bundle compiled here the current release — no build on the platform",
220347
+ description: "For a bundle the platform's build service cannot make any more: compiled with an older SDK's CLI (`compile --remote-assets`, so it names the project's uploaded resources), it becomes the current release with its own `// sdk:` — what the viewers of that SDK line run. Local changes are pushed first as the release's checkpoint. A project with server code cannot take one (its server is built with the client).",
220348
+ flags: { message: str('checkpoint message when there are local changes (default "Release")', { alias: "m", value: "<text>" }) },
220349
+ examples: ["lecodes releases upload app.js", 'lecodes releases upload build/app.js -m "Kungur for the 2.1 app"'],
220350
+ run: async ({ args, flags }) => {
220351
+ const file = args[0];
220352
+ if (!file)
220353
+ throw new UsageError("Which bundle? `lecodes releases upload <app.js>`.");
220354
+ const path = resolve21(file);
220355
+ if (!existsSync39(path))
220356
+ throw new CliError(`No such file: ${file}`);
220357
+ const code = readFileSync31(path, "utf8");
220358
+ const sdk = /^\/\/ sdk: (\S+)/m.exec(code)?.[1];
220359
+ if (!sdk)
220360
+ throw new CliError(`${file} is not a LeCodes bundle: no \`// sdk:\` header line. \`lecodes compile --remote-assets -o app.js\` writes one.`);
220361
+ const map = existsSync39(`${path}.map`) ? readFileSync31(`${path}.map`, "utf8") : null;
220362
+ const { root, manifest, apiUrl, token } = linkedProject();
220363
+ if (hasChanges(diffLocal(manifest, scanProject(root, manifest))))
220364
+ await runPush({ message: flags.message || "Release" });
220365
+ const release = await uploadReleaseBundle(apiUrl, token, manifest.uuid, { code, map });
220366
+ reportRelease(release, qrUrl(apiUrl, manifest.uuid), "Uploaded");
220367
+ }
220368
+ });
220369
+ var releases_default = defineCommand({
220370
+ name: "releases",
220371
+ summary: "List the project's releases (● = the current one); `releases use <version>` rolls back, `releases upload <app.js>` releases a bundle compiled here",
220372
+ description: "Every `lecodes publish` (or the editor's Publish button) makes a release: an immutable build with its own version. The current one is what https://<handle>.le.codes, the viewers and the store run.",
220373
+ commands: [use, upload2],
220374
+ examples: ["lecodes releases", "lecodes releases use 1.2.3", "lecodes releases upload app.js"],
220375
+ run: async () => {
220376
+ const { apiUrl, token, manifest } = linkedProject();
220377
+ const releases = await listReleases(apiUrl, token, manifest.uuid);
220378
+ if (releases.length === 0) {
220379
+ info(`${manifest.name} has no releases yet — \`lecodes publish\` makes the first.`);
220380
+ return;
220381
+ }
220382
+ table([
220383
+ ["", c.dim("VERSION"), c.dim("DATE"), c.dim("SIZE"), c.dim("SDK")],
220384
+ ...releases.map((r) => [
220385
+ r.current ? c.green("●") : "",
220386
+ r.current ? c.bold(r.version) : r.version,
220387
+ relativeTime(r.createdAt),
220388
+ formatSize(r.size),
220389
+ r.sdk
220390
+ ])
220391
+ ]);
220392
+ if (!releases.some((r) => r.current))
220393
+ info(c.dim(" Unpublished: no release is current."));
220394
+ }
220395
+ });
220396
+
220397
+ // src/commands/push.ts
219997
220398
  init_shared();
219998
220399
  var pushFlags = {
219999
220400
  message: str("checkpoint message (asked for when omitted)", { alias: "m", value: "<text>" }),
220000
- publish: bool("also deploy the project live (compile) with the push"),
220401
+ publish: bool("also release the project with the push (= `lecodes publish`)"),
220402
+ "force-publish": bool("--publish, and release even when nothing changed — a new SDK on the build service (= `lecodes publish --force`)"),
220001
220403
  force: bool("push even if the server moved on (shows + confirms which server files get overwritten)"),
220002
220404
  yes: bool("skip the --force overwrite confirmation", { alias: "y" })
220003
220405
  };
@@ -220005,18 +220407,21 @@ var push_default = defineCommand({
220005
220407
  name: "push",
220006
220408
  summary: "Push local changes as one new checkpoint",
220007
220409
  flags: pushFlags,
220008
- examples: ['lecodes push -m "Tweak the menu"', "lecodes push --publish"],
220410
+ examples: ['lecodes push -m "Tweak the menu"', "lecodes push --publish", "lecodes push --force-publish"],
220009
220411
  run: ({ flags }) => runPush(flags)
220010
220412
  });
220011
220413
  var runPush = async (opts) => {
220012
220414
  const { root, manifest, apiUrl, token } = linkedProject();
220013
- const publish = !!opts.publish;
220415
+ const forcePublish = !!opts["force-publish"];
220416
+ const publish = !!opts.publish || forcePublish;
220014
220417
  const scan = scanProject(root, manifest);
220015
220418
  const diff = diffLocal(manifest, scan);
220016
220419
  if (!hasChanges(diff)) {
220017
220420
  if (publish) {
220018
- await publishProject(apiUrl, token, manifest.uuid);
220019
- success("No file changes — published the current project state.");
220421
+ info(c.dim("Working tree clean — publishing the project as it is."));
220422
+ const release = await withSchemaConfirm((allowDestructive) => publishProject(apiUrl, token, manifest.uuid, { allowDestructive, force: forcePublish }), { allowDestructive: false, retry: "lecodes publish" });
220423
+ if (release)
220424
+ reportRelease(release, qrUrl(apiUrl, manifest.uuid));
220020
220425
  } else
220021
220426
  info(c.dim("Working tree clean — nothing to push."));
220022
220427
  await flushComments();
@@ -220058,9 +220463,9 @@ var runPush = async (opts) => {
220058
220463
  if (source)
220059
220464
  entry.assetId = source.id;
220060
220465
  if (isTextKind(kind))
220061
- entry.text = readFileSync31(file.absPath, "utf8");
220466
+ entry.text = readFileSync32(file.absPath, "utf8");
220062
220467
  else if (!source || source.sha !== file.sha) {
220063
- const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename20(file.path), readFileSync31(file.absPath));
220468
+ const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename20(file.path), readFileSync32(file.absPath));
220064
220469
  entry.blobFileId = blob.id;
220065
220470
  uploaded++;
220066
220471
  }
@@ -220069,11 +220474,11 @@ var runPush = async (opts) => {
220069
220474
  const folders = scan.dirs.map((path) => ({ path }));
220070
220475
  if (uploaded > 0)
220071
220476
  info(c.dim(`Uploaded ${uploaded} binary file${uploaded === 1 ? "" : "s"}.`));
220072
- const res = await sync(apiUrl, token, manifest.uuid, { message, files, folders, publish });
220477
+ const res = await sync(apiUrl, token, manifest.uuid, { message, files, folders, publish, ...forcePublish ? { force: true } : {} });
220073
220478
  if (!res.created) {
220074
220479
  info(c.dim("Server reported no changes."));
220075
220480
  if (publish)
220076
- reportPublish(res);
220481
+ reportPublish(res, qrUrl(apiUrl, manifest.uuid));
220077
220482
  return;
220078
220483
  }
220079
220484
  const shaByPath = new Map(scan.files.map((f) => [f.path, f.sha]));
@@ -220089,7 +220494,7 @@ var runPush = async (opts) => {
220089
220494
  ].filter(Boolean).join(", ");
220090
220495
  success(`Pushed checkpoint${res.seq ? ` #${res.seq}` : ""}${counts ? ` (${counts})` : ""}: ${message}`);
220091
220496
  if (publish)
220092
- reportPublish(res);
220497
+ reportPublish(res, qrUrl(apiUrl, manifest.uuid));
220093
220498
  await flushComments();
220094
220499
  };
220095
220500
  var flushComments = async () => {
@@ -220112,11 +220517,11 @@ var serverPathDrift = (project, manifest) => {
220112
220517
  const parts = [added.length ? describe("added", added) : "", removed.length ? describe("removed", removed) : ""].filter(Boolean);
220113
220518
  return parts.length ? parts.join("; ") : null;
220114
220519
  };
220115
- var reportPublish = (res) => {
220116
- if (res.published)
220117
- success("Published — the project is now live.");
220520
+ var reportPublish = (res, qr) => {
220521
+ if (res.published && res.release)
220522
+ reportRelease(res.release, qr);
220118
220523
  else
220119
- warn(`Push landed, but publish failed: ${res.publishError ?? "unknown error"}`);
220524
+ warn(`Push landed, but publish failed: ${res.publishError ?? "unknown error"} — \`lecodes publish\` tries again.`);
220120
220525
  };
220121
220526
 
220122
220527
  // src/commands/link.ts
@@ -220171,10 +220576,10 @@ var link_default = defineCommand({
220171
220576
  run: async ({ args, flags }) => {
220172
220577
  const { apiUrl, token } = session();
220173
220578
  const dirArg = args[0];
220174
- const root = dirArg ? resolve21(dirArg) : process.cwd();
220175
- if (!existsSync39(root))
220579
+ const root = dirArg ? resolve22(dirArg) : process.cwd();
220580
+ if (!existsSync40(root))
220176
220581
  throw new CliError(`No such directory: ${root}`);
220177
- if (existsSync39(join44(root, LECODES_DIR, "manifest.json")) && !flags.force) {
220582
+ if (existsSync40(join44(root, LECODES_DIR, "manifest.json")) && !flags.force) {
220178
220583
  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.");
220179
220584
  }
220180
220585
  let enclosing = null;
@@ -220260,9 +220665,9 @@ var login_default = defineCommand({
220260
220665
  });
220261
220666
 
220262
220667
  // src/commands/navmesh.ts
220263
- import { existsSync as existsSync40, mkdirSync as mkdirSync25, mkdtempSync as mkdtempSync5, readdirSync as readdirSync17, readFileSync as readFileSync32, rmSync as rmSync12, statSync as statSync15, unlinkSync as unlinkSync2, writeFileSync as writeFileSync27 } from "node:fs";
220668
+ import { existsSync as existsSync41, mkdirSync as mkdirSync25, mkdtempSync as mkdtempSync5, readdirSync as readdirSync17, readFileSync as readFileSync33, rmSync as rmSync12, statSync as statSync15, unlinkSync as unlinkSync2, writeFileSync as writeFileSync27 } from "node:fs";
220264
220669
  import { tmpdir as tmpdir5 } from "node:os";
220265
- import { basename as basename22, join as join45, relative as relative7, resolve as resolve22 } from "node:path";
220670
+ import { basename as basename22, join as join45, relative as relative7, resolve as resolve23 } from "node:path";
220266
220671
  init_cli();
220267
220672
  init_shared();
220268
220673
 
@@ -221059,7 +221464,7 @@ var range2 = (name, value, fallback, min, max) => {
221059
221464
  var guessName = (root) => {
221060
221465
  const dirs = [join45(root, "src"), root];
221061
221466
  for (const dir of dirs) {
221062
- if (!existsSync40(dir))
221467
+ if (!existsSync41(dir))
221063
221468
  continue;
221064
221469
  let entries;
221065
221470
  try {
@@ -221071,7 +221476,7 @@ var guessName = (root) => {
221071
221476
  if (!f.endsWith(".scene.ts"))
221072
221477
  continue;
221073
221478
  try {
221074
- if (/\bnavmesh\s*:/.test(readFileSync32(join45(dir, f), "utf8")))
221479
+ if (/\bnavmesh\s*:/.test(readFileSync33(join45(dir, f), "utf8")))
221075
221480
  return f.slice(0, -".scene.ts".length);
221076
221481
  } catch {}
221077
221482
  }
@@ -221137,9 +221542,9 @@ var bake2 = defineCommand({
221137
221542
  examples: ["lecodes navmesh bake", "lecodes navmesh bake --name yard --cell 0.2 --obj --logs"],
221138
221543
  run: async ({ flags }) => {
221139
221544
  const { root } = localProject();
221140
- const outDir = resolve22(root, flags.out);
221545
+ const outDir = resolve23(root, flags.out);
221141
221546
  const entryFile = flags.entry;
221142
- if (!existsSync40(resolve22(root, entryFile))) {
221547
+ if (!existsSync41(resolve23(root, entryFile))) {
221143
221548
  throw new CliError(`No ${entryFile} in ${root} — pass --entry <file> (the file that builds the level: a scene file with env.navmesh, or code calling NavMesh.load).`);
221144
221549
  }
221145
221550
  const name = flags.name ?? guessName(root) ?? "scene";
@@ -221150,7 +221555,7 @@ var bake2 = defineCommand({
221150
221555
  const logs = flags.logs;
221151
221556
  mkdirSync25(outDir, { recursive: true });
221152
221557
  const navmeshPath = join45(outDir, `${name}.navmesh`);
221153
- if (!existsSync40(navmeshPath))
221558
+ if (!existsSync41(navmeshPath))
221154
221559
  writeFileSync27(navmeshPath, placeholderLnav());
221155
221560
  const importPath = "./" + entryFile.replace(/\\/g, "/").replace(/\.tsx?$/, "");
221156
221561
  const entryPath = "/.navmesh-entry.ts";
@@ -221219,9 +221624,9 @@ import ${JSON.stringify(importPath)}
221219
221624
  if (!config)
221220
221625
  throw new CliError("The host reported a dump but no `[navmesh] config` line — SDK/host mismatch?");
221221
221626
  const geoPath = join45(outDir, `${name}.navgeo`);
221222
- if (!existsSync40(geoPath))
221627
+ if (!existsSync41(geoPath))
221223
221628
  throw new CliError(`The host reported a dump but ${geoPath} is missing.`);
221224
- const geo = readNgeo(new Uint8Array(readFileSync32(geoPath)));
221629
+ const geo = readNgeo(new Uint8Array(readFileSync33(geoPath)));
221225
221630
  const params = {
221226
221631
  agentRadius: range2("radius", flags.radius, config.agent.radius, 0.05, 10),
221227
221632
  agentHeight: range2("height", flags.height, config.agent.height, 0.1, 20),
@@ -221295,25 +221700,44 @@ init_cli();
221295
221700
 
221296
221701
  // src/plugin/generate.ts
221297
221702
  init_cli();
221298
- import { existsSync as existsSync41, mkdirSync as mkdirSync26, readFileSync as readFileSync33, readdirSync as readdirSync18, statSync as statSync16, writeFileSync as writeFileSync28 } from "node:fs";
221703
+ import { existsSync as existsSync42, mkdirSync as mkdirSync26, readFileSync as readFileSync34, readdirSync as readdirSync18, statSync as statSync16, writeFileSync as writeFileSync28 } from "node:fs";
221299
221704
  import { createRequire as createRequire7 } from "node:module";
221300
- import { basename as basename23, join as join46, relative as relative8, resolve as resolve23, sep as sep3 } from "node:path";
221705
+ import { basename as basename23, join as join46, relative as relative8, resolve as resolve24, sep as sep3 } from "node:path";
221301
221706
 
221302
221707
  // src/plugin/permissions.ts
221303
221708
  var PERMISSION_KINDS = ["camera", "microphone", "location", "speech"];
221304
221709
  var isPermissionKind = (s) => PERMISSION_KINDS.includes(s);
221305
221710
  var PERMISSION_NEEDS = {
221306
- camera: { infoPlist: ["NSCameraUsageDescription"], android: [["android.permission.CAMERA"]] },
221307
- microphone: { infoPlist: ["NSMicrophoneUsageDescription"], android: [["android.permission.RECORD_AUDIO"]] },
221711
+ camera: { infoPlist: ["NSCameraUsageDescription"], android: [["android.permission.CAMERA"]], androidFeatures: ["android.hardware.camera"] },
221712
+ microphone: {
221713
+ infoPlist: ["NSMicrophoneUsageDescription"],
221714
+ android: [["android.permission.RECORD_AUDIO"]],
221715
+ androidFeatures: ["android.hardware.microphone"]
221716
+ },
221308
221717
  location: {
221309
221718
  infoPlist: ["NSLocationWhenInUseUsageDescription"],
221310
- android: [["android.permission.ACCESS_FINE_LOCATION", "android.permission.ACCESS_COARSE_LOCATION"]]
221719
+ android: [["android.permission.ACCESS_FINE_LOCATION", "android.permission.ACCESS_COARSE_LOCATION"]],
221720
+ androidFeatures: ["android.hardware.location"]
221311
221721
  },
221312
- speech: { infoPlist: ["NSSpeechRecognitionUsageDescription"], android: [["android.permission.RECORD_AUDIO"]] }
221722
+ speech: {
221723
+ infoPlist: ["NSSpeechRecognitionUsageDescription"],
221724
+ android: [["android.permission.RECORD_AUDIO"]],
221725
+ androidFeatures: ["android.hardware.microphone"]
221726
+ }
221313
221727
  };
221314
221728
  var usesPermissions = (xml) => [...xml.matchAll(/<uses-permission(?:-sdk-23)?\b[^>]*\bandroid:name\s*=\s*"([^"]+)"/g)].map((m) => m[1]);
221315
- var missingDeclarations = (kinds, infoPlist, androidNames) => {
221316
- const out = { infoPlist: [], android: [] };
221729
+ var usesFeatures = (xml) => {
221730
+ const out = [];
221731
+ for (const m of xml.replace(/<!--[\s\S]*?-->/g, "").matchAll(/<uses-feature\b[^>]*>/g)) {
221732
+ const attr = (re) => re.exec(m[0])?.[2];
221733
+ const name = attr(/\bandroid:name\s*=\s*(["'])(.*?)\1/);
221734
+ if (name)
221735
+ out.push({ name, required: attr(/\bandroid:required\s*=\s*(["'])(.*?)\1/)?.trim() !== "false" });
221736
+ }
221737
+ return out;
221738
+ };
221739
+ var missingDeclarations = (kinds, infoPlist, androidNames, androidFeatures) => {
221740
+ const out = { infoPlist: [], android: [], androidFeatures: [] };
221317
221741
  for (const kind of kinds) {
221318
221742
  const needs = PERMISSION_NEEDS[kind];
221319
221743
  if (infoPlist)
@@ -221326,8 +221750,14 @@ var missingDeclarations = (kinds, infoPlist, androidNames) => {
221326
221750
  if (!group.some((name) => androidNames.includes(name)))
221327
221751
  out.android.push(`${group.join(" or ")} (${kind})`);
221328
221752
  }
221753
+ if (androidFeatures)
221754
+ for (const feature of needs.androidFeatures) {
221755
+ const declared = androidFeatures.filter((f) => f.name === feature);
221756
+ if (declared.length === 0 || declared.some((f) => f.required))
221757
+ out.androidFeatures.push(`${feature} (${kind})`);
221758
+ }
221329
221759
  }
221330
- return { infoPlist: [...new Set(out.infoPlist)], android: [...new Set(out.android)] };
221760
+ return { infoPlist: [...new Set(out.infoPlist)], android: [...new Set(out.android)], androidFeatures: [...new Set(out.androidFeatures)] };
221331
221761
  };
221332
221762
 
221333
221763
  // src/plugin/contract.ts
@@ -224135,13 +224565,13 @@ const wire = {
224135
224565
  var shown = (from, to) => relative8(from, to).split(sep3).join("/");
224136
224566
  var CONTRACT_FILE = "contract.d.ts";
224137
224567
  var findPlugins = (path) => {
224138
- const root = resolve23(path);
224139
- if (existsSync41(join46(root, CONTRACT_FILE)))
224568
+ const root = resolve24(path);
224569
+ if (existsSync42(join46(root, CONTRACT_FILE)))
224140
224570
  return [{ id: basename23(root), dir: root }];
224141
224571
  const holder = join46(root, "plugins");
224142
- if (!existsSync41(holder))
224572
+ if (!existsSync42(holder))
224143
224573
  return [];
224144
- return readdirSync18(holder).sort().map((id) => ({ id, dir: join46(holder, id) })).filter((p) => statSync16(p.dir).isDirectory() && existsSync41(join46(p.dir, CONTRACT_FILE)));
224574
+ return readdirSync18(holder).sort().map((id) => ({ id, dir: join46(holder, id) })).filter((p) => statSync16(p.dir).isDirectory() && existsSync42(join46(p.dir, CONTRACT_FILE)));
224145
224575
  };
224146
224576
  var loadTypescript = (dir) => {
224147
224577
  for (const from of [join46(dir, "package.json"), import.meta.url]) {
@@ -224156,25 +224586,25 @@ var loadTypescript = (dir) => {
224156
224586
  };
224157
224587
  var generatePlugin = (ts, plugin) => {
224158
224588
  const file = join46(plugin.dir, CONTRACT_FILE);
224159
- const contract = readContract(ts, file, readFileSync33(file, "utf8"));
224589
+ const contract = readContract(ts, file, readFileSync34(file, "utf8"));
224160
224590
  const manifest = readManifestIfAny(plugin.dir, plugin.id);
224161
224591
  checkPermissions(plugin, contract, manifest);
224162
- const handWritten = existsSync41(join46(plugin.dir, "sdk", `${plugin.id}.ts`));
224592
+ const handWritten = existsSync42(join46(plugin.dir, "sdk", `${plugin.id}.ts`));
224163
224593
  const single = contract.channels.length === 1;
224164
224594
  const files = contract.channels.map((channel) => ({
224165
224595
  file: join46(plugin.dir, "sdk", single ? `${plugin.id}.gen.ts` : `${plugin.id}.${channel.iface}.gen.ts`),
224166
224596
  text: emitSdk(contract, channel, { id: plugin.id, handWritten })
224167
224597
  }));
224168
- if (existsSync41(join46(plugin.dir, "ios"))) {
224598
+ if (existsSync42(join46(plugin.dir, "ios"))) {
224169
224599
  const name = single ? contract.channels[0].iface : contract.channels[0].iface + "Plugin";
224170
224600
  files.push({ file: join46(plugin.dir, "ios", `${name}.gen.swift`), text: emitSwift(lower3(contract), { id: plugin.id }) });
224171
224601
  }
224172
- if (existsSync41(join46(plugin.dir, "android"))) {
224602
+ if (existsSync42(join46(plugin.dir, "android"))) {
224173
224603
  const name = single ? contract.channels[0].iface : contract.channels[0].iface + "Plugin";
224174
224604
  const pkg = kotlinPackage(join46(plugin.dir, "android"));
224175
224605
  files.push({ file: join46(pkg.dir, `${name}.gen.kt`), text: emitKotlin(lower3(contract), { id: plugin.id, package: pkg.name }) });
224176
224606
  }
224177
- if (existsSync41(join46(plugin.dir, "web"))) {
224607
+ if (existsSync42(join46(plugin.dir, "web"))) {
224178
224608
  files.push({ file: join46(plugin.dir, "web", `${plugin.id}.gen.ts`), text: emitWeb(lower3(contract), { id: plugin.id }) });
224179
224609
  const foreign = hostImports(join46(plugin.dir, "web"));
224180
224610
  if (foreign.length)
@@ -224196,13 +224626,15 @@ var checkPermissions = (plugin, contract, manifest) => {
224196
224626
  if (kinds.length === 0)
224197
224627
  return;
224198
224628
  const androidManifest = join46(plugin.dir, "android", "src", "main", "AndroidManifest.xml");
224199
- const androidNames = manifest.android ? existsSync41(androidManifest) ? usesPermissions(readFileSync33(androidManifest, "utf8")) : [] : null;
224200
- const missing = missingDeclarations(kinds, manifest.ios ? manifest.ios.infoPlist ?? {} : null, androidNames);
224629
+ const androidXml = manifest.android ? existsSync42(androidManifest) ? readFileSync34(androidManifest, "utf8") : "" : null;
224630
+ const missing = missingDeclarations(kinds, manifest.ios ? manifest.ios.infoPlist ?? {} : null, androidXml === null ? null : usesPermissions(androidXml), androidXml === null ? null : usesFeatures(androidXml));
224201
224631
  const problems = [];
224202
224632
  if (missing.infoPlist.length)
224203
224633
  problems.push(`ios.infoPlist: the contract's @permission needs ${missing.infoPlist.join(", ")} — the usage string the dialog shows`);
224204
224634
  if (missing.android.length)
224205
224635
  problems.push(`android/src/main/AndroidManifest.xml: the contract's @permission needs <uses-permission android:name="…"/> for ${missing.android.join(", ")}`);
224636
+ if (missing.androidFeatures.length)
224637
+ problems.push(`android/src/main/AndroidManifest.xml: the contract's @permission implies <uses-feature android:name="…" android:required="false"/> for ${missing.androidFeatures.join(", ")} — without it Play requires the hardware`);
224206
224638
  if (problems.length)
224207
224639
  throw new ManifestError(join46(plugin.dir, MANIFEST_FILE), problems);
224208
224640
  };
@@ -224218,7 +224650,7 @@ var hostImports = (web) => {
224218
224650
  }
224219
224651
  if (!/\.(ts|tsx|js|mjs)$/.test(entry))
224220
224652
  continue;
224221
- readFileSync33(path, "utf8").split(`
224653
+ readFileSync34(path, "utf8").split(`
224222
224654
  `).forEach((line, i) => {
224223
224655
  const m = /(?:from\s+|import\s*\(\s*|import\s+)["'](lecodes-[^"']*)["']/.exec(line);
224224
224656
  if (m && !/^\s*\/\//.test(line))
@@ -224232,7 +224664,7 @@ var hostImports = (web) => {
224232
224664
  var kotlinPackage = (android) => {
224233
224665
  const root = join46(android, "src", "main", "java");
224234
224666
  let dir = root;
224235
- while (existsSync41(dir)) {
224667
+ while (existsSync42(dir)) {
224236
224668
  const entries = readdirSync18(dir).filter((e) => !e.startsWith("."));
224237
224669
  const dirs = entries.filter((e) => statSync16(join46(dir, e)).isDirectory());
224238
224670
  if (dirs.length !== 1 || entries.length !== 1)
@@ -224246,10 +224678,10 @@ var kotlinPackage = (android) => {
224246
224678
  };
224247
224679
  var repoSdkPath = (root) => {
224248
224680
  const file = join46(root, "package.json");
224249
- if (!existsSync41(file))
224681
+ if (!existsSync42(file))
224250
224682
  return DEFAULT_SDK_PATH;
224251
224683
  try {
224252
- const pkg = JSON.parse(readFileSync33(file, "utf8"));
224684
+ const pkg = JSON.parse(readFileSync34(file, "utf8"));
224253
224685
  const path = pkg.lecodes?.iosSdkPath;
224254
224686
  if (path !== undefined && typeof path !== "string")
224255
224687
  throw new PackageError(`package.json lecodes.iosSdkPath: a path, as a string`);
@@ -224263,13 +224695,13 @@ var repoSdkPath = (root) => {
224263
224695
  var generate = (path, opts = {}) => {
224264
224696
  const plugins = findPlugins(path);
224265
224697
  if (plugins.length === 0)
224266
- throw new CliError(`No plugin under ${resolve23(path)}: a plugin is a directory with a ${CONTRACT_FILE} (one, or plugins/<id>/ of a repo).`);
224698
+ throw new CliError(`No plugin under ${resolve24(path)}: a plugin is a directory with a ${CONTRACT_FILE} (one, or plugins/<id>/ of a repo).`);
224267
224699
  const ts = loadTypescript(plugins[0].dir);
224268
- const root = resolve23(path);
224700
+ const root = resolve24(path);
224269
224701
  const report = { plugins: [], repo: { written: [], stale: [], fresh: [] }, failures: [] };
224270
224702
  const place = (f, entry) => {
224271
224703
  const name = shown(root, f.file) || basename23(f.file);
224272
- const current = existsSync41(f.file) ? readFileSync33(f.file, "utf8") : null;
224704
+ const current = existsSync42(f.file) ? readFileSync34(f.file, "utf8") : null;
224273
224705
  if (current === f.text) {
224274
224706
  entry.fresh.push(name);
224275
224707
  return;
@@ -224306,7 +224738,7 @@ ${e.problems.map((p) => ` ${p}`).join(`
224306
224738
  throw e;
224307
224739
  }
224308
224740
  }
224309
- const isRepo = !existsSync41(join46(root, CONTRACT_FILE));
224741
+ const isRepo = !existsSync42(join46(root, CONTRACT_FILE));
224310
224742
  if (isRepo && report.failures.length === 0 && manifests.some((m) => m.manifest.ios)) {
224311
224743
  try {
224312
224744
  place({ file: join46(root, PACKAGE_FILE), text: emitPackageSwift(manifests, repoSdkPath(root)) }, report.repo);
@@ -224582,12 +225014,37 @@ var pn_default = defineCommand({
224582
225014
  commands: [send, devices, keys]
224583
225015
  });
224584
225016
 
225017
+ // src/commands/publish.ts
225018
+ init_cli();
225019
+ init_api();
225020
+ init_localFiles();
225021
+ init_shared();
225022
+ var publish_default = defineCommand({
225023
+ name: "publish",
225024
+ summary: "Release the project: push local changes, build, make it the live version",
225025
+ description: 'The editor\'s Publish button. Local changes are pushed first as a checkpoint ("Release", or -m). The platform builds the project, deploys its server to production and makes the build the current release at https://<handle>.le.codes — what the viewers and the store run. A project that builds to what is live already makes no release: "Nothing changed — <version> is current" (--force releases it all the same). A schema change that deletes production data is shown and asked about; without a terminal it needs --allow-destructive. `lecodes releases` lists the releases, `lecodes releases use <version>` rolls back.',
225026
+ flags: {
225027
+ message: str('checkpoint message when there are local changes (default "Release")', { alias: "m", value: "<text>" }),
225028
+ force: bool("rebuild even when nothing changed — a new SDK on the build service"),
225029
+ "allow-destructive": bool("apply a schema change that deletes production data without asking")
225030
+ },
225031
+ examples: ["lecodes publish", 'lecodes publish -m "Checkout v2"', "lecodes publish --force", "lecodes publish --allow-destructive"],
225032
+ run: async ({ flags }) => {
225033
+ const { root, manifest, apiUrl, token } = linkedProject();
225034
+ if (hasChanges(diffLocal(manifest, scanProject(root, manifest))))
225035
+ await runPush({ message: flags.message || "Release" });
225036
+ const release = await withSchemaConfirm((allowDestructive) => publishProject(apiUrl, token, manifest.uuid, { allowDestructive, force: flags.force }), { allowDestructive: flags["allow-destructive"], retry: flags.force ? "lecodes publish --force" : "lecodes publish" });
225037
+ if (release)
225038
+ reportRelease(release, qrUrl(apiUrl, manifest.uuid));
225039
+ }
225040
+ });
225041
+
224585
225042
  // src/commands/pull.ts
224586
225043
  init_cli();
224587
225044
  init_api();
224588
225045
  init_localFiles();
224589
225046
  init_manifest();
224590
- import { existsSync as existsSync42, mkdirSync as mkdirSync27, rmSync as rmSync13, writeFileSync as writeFileSync29 } from "node:fs";
225047
+ import { existsSync as existsSync43, mkdirSync as mkdirSync27, rmSync as rmSync13, writeFileSync as writeFileSync29 } from "node:fs";
224591
225048
  import { dirname as dirname21, join as join47 } from "node:path";
224592
225049
  init_paths();
224593
225050
  init_types4();
@@ -224618,7 +225075,7 @@ var pull_default = defineCommand({
224618
225075
  if (remotePaths.has(path))
224619
225076
  continue;
224620
225077
  const abs = join47(root, ...path.replace(/^\//, "").split("/"));
224621
- if (existsSync42(abs))
225078
+ if (existsSync43(abs))
224622
225079
  rmSync13(abs);
224623
225080
  }
224624
225081
  const files = await materializeAssets(root, project, apiUrl, token, progress);
@@ -224679,8 +225136,8 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
224679
225136
  };
224680
225137
 
224681
225138
  // src/commands/render.ts
224682
- import { mkdirSync as mkdirSync29, readFileSync as readFileSync35, writeFileSync as writeFileSync31 } from "node:fs";
224683
- import { dirname as dirname22, resolve as resolve27 } from "node:path";
225139
+ import { mkdirSync as mkdirSync29, readFileSync as readFileSync36, writeFileSync as writeFileSync31 } from "node:fs";
225140
+ import { dirname as dirname22, resolve as resolve28 } from "node:path";
224684
225141
 
224685
225142
  // src/compile/headlessBundle.ts
224686
225143
  init_api();
@@ -224767,22 +225224,22 @@ var compileHeadlessBundle = async (opts = {}) => {
224767
225224
  init_designMeta();
224768
225225
  init_cli();
224769
225226
  init_paths();
224770
- import { existsSync as existsSync43, readFileSync as readFileSync34, statSync as statSync17 } from "node:fs";
224771
- import { isAbsolute, join as join48, relative as relative9, resolve as resolve24 } from "node:path";
225227
+ import { existsSync as existsSync44, readFileSync as readFileSync35, statSync as statSync17 } from "node:fs";
225228
+ import { isAbsolute, join as join48, relative as relative9, resolve as resolve25 } from "node:path";
224772
225229
  var SCREEN_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs"];
224773
225230
  var SCREEN_ENTRY_PATH = "/.screen-entry.ts";
224774
225231
  var DEFAULT_DESIGN_DIR = "design";
224775
225232
  var hasSeparator = (file) => file.includes("/") || file.includes("\\");
224776
225233
  var findScreenFile = (root, file, dirFlag) => {
224777
- const candidates = [resolve24(process.cwd(), file)];
225234
+ const candidates = [resolve25(process.cwd(), file)];
224778
225235
  if (!hasSeparator(file)) {
224779
225236
  candidates.push(join48(root, (dirFlag ?? DEFAULT_DESIGN_DIR).replace(/[/\\]+$/, ""), "screens", file));
224780
225237
  }
224781
225238
  for (const base of candidates) {
224782
- if (existsSync43(base) && statSync17(base).isFile())
225239
+ if (existsSync44(base) && statSync17(base).isFile())
224783
225240
  return base;
224784
225241
  for (const ext of SCREEN_EXTS)
224785
- if (existsSync43(base + ext))
225242
+ if (existsSync44(base + ext))
224786
225243
  return base + ext;
224787
225244
  }
224788
225245
  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/.`));
@@ -224792,7 +225249,7 @@ var designDirOf = (root, key) => {
224792
225249
  if (!m)
224793
225250
  return null;
224794
225251
  const dir = join48(root, m[1]);
224795
- return existsSync43(join48(dir, "meta.json")) ? { dir, id: m[2] } : null;
225252
+ return existsSync44(join48(dir, "meta.json")) ? { dir, id: m[2] } : null;
224796
225253
  };
224797
225254
  var resolveScreenTarget = (root, file, opts = {}) => {
224798
225255
  const abs = findScreenFile(root, file, opts.dir);
@@ -224801,7 +225258,7 @@ var resolveScreenTarget = (root, file, opts = {}) => {
224801
225258
  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.`);
224802
225259
  }
224803
225260
  const key = toKey(rel);
224804
- if (!/^\s*export\s+default\b/m.test(readFileSync34(abs, "utf8"))) {
225261
+ if (!/^\s*export\s+default\b/m.test(readFileSync35(abs, "utf8"))) {
224805
225262
  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.`);
224806
225263
  }
224807
225264
  const invoke = opts.state !== undefined ? `screen(${JSON.stringify(opts.state)})` : "screen()";
@@ -224837,9 +225294,9 @@ init_manifest();
224837
225294
 
224838
225295
  // src/hosts/flowPick.ts
224839
225296
  init_cli();
224840
- import { existsSync as existsSync44, mkdirSync as mkdirSync28, mkdtempSync as mkdtempSync6, rmSync as rmSync14, writeFileSync as writeFileSync30 } from "node:fs";
225297
+ import { existsSync as existsSync45, mkdirSync as mkdirSync28, mkdtempSync as mkdtempSync6, rmSync as rmSync14, writeFileSync as writeFileSync30 } from "node:fs";
224841
225298
  import { tmpdir as tmpdir6 } from "node:os";
224842
- import { join as join49, resolve as resolve25 } from "node:path";
225299
+ import { join as join49, resolve as resolve26 } from "node:path";
224843
225300
  import { deflateSync as deflateSync2 } from "node:zlib";
224844
225301
  var MAX_SIDE = 4096;
224845
225302
  var crcTable = (() => {
@@ -224908,8 +225365,8 @@ var write = (name, bytes) => {
224908
225365
  };
224909
225366
  var fileOf = (at, value, baseDir) => {
224910
225367
  if (typeof value === "string") {
224911
- const path = resolve25(baseDir, value);
224912
- if (!existsSync44(path))
225368
+ const path = resolve26(baseDir, value);
225369
+ if (!existsSync45(path))
224913
225370
  throw new CliError(`${at}: "pick": no file "${value}" (looked for ${path}).`);
224914
225371
  return path;
224915
225372
  }
@@ -224949,7 +225406,7 @@ var resolvePicks = (steps, baseDir) => {
224949
225406
 
224950
225407
  // src/hosts/desktopScript.ts
224951
225408
  init_cli();
224952
- import { resolve as resolve26 } from "node:path";
225409
+ import { resolve as resolve27 } from "node:path";
224953
225410
 
224954
225411
  // ../../runtime/headless/src/headless/paintDump.ts
224955
225412
  var FLAG_INTERACTIVE = 1;
@@ -225132,7 +225589,7 @@ var FRAME_MS = 16;
225132
225589
  var SETTLE_FRAMES = 640;
225133
225590
  var ACTIONS = ["tap", "type", "fill", "scroll", "wait", "frames", "drag", "hold", "hover", "key", "keyDown", "keyUp", "look", "mouseTo", "wheel", "stick", "gamepad", "camera", "view"];
225134
225591
  var SEMANTIC_EXPECT = ["text", "notText", "node", "notNode", "screen", "value"];
225135
- var UNSUPPORTED_EXPECT = ["audio"];
225592
+ var UNSUPPORTED_EXPECT = ["audio", "toast"];
225136
225593
  var VIEW_NAMES = ["iso", "top", "front", "side", "back"];
225137
225594
  var isVec3 = (v) => Array.isArray(v) && v.length === 3 && v.every((n) => typeof n === "number");
225138
225595
  var pt = (p) => typeof p === "string" ? p : `(${p.x},${p.y})`;
@@ -225394,7 +225851,7 @@ var translateScenario = (rawSteps, opts) => {
225394
225851
  }
225395
225852
  }
225396
225853
  if (needDump) {
225397
- plan.paintDump = resolve26(opts.outDir, `.dump-${i}.json`);
225854
+ plan.paintDump = resolve27(opts.outDir, `.dump-${i}.json`);
225398
225855
  plan.lines.push(emit(`frames 1`));
225399
225856
  plan.lines.push(emit(`dump ${plan.paintDump}`));
225400
225857
  budgetMs += 2 * FRAME_MS;
@@ -225409,7 +225866,7 @@ var translateScenario = (rawSteps, opts) => {
225409
225866
  throw new CliError(`${at}: "screenshot" must be a path.`);
225410
225867
  if (opts.sim)
225411
225868
  throw new CliError(`${at}: "screenshot" isn't available with --sim (the windowless host paints nothing) — judge the run by expect.log, or run it with --desktop for pixels.`);
225412
- plan.screenshot = resolve26(opts.outDir, s.screenshot);
225869
+ plan.screenshot = resolve27(opts.outDir, s.screenshot);
225413
225870
  const clip = s.clip;
225414
225871
  if (clip !== undefined) {
225415
225872
  if (typeof clip === "string") {
@@ -225435,7 +225892,7 @@ var translateScenario = (rawSteps, opts) => {
225435
225892
  if (s.dump !== undefined) {
225436
225893
  if (typeof s.dump !== "string")
225437
225894
  throw new CliError(`${at}: "dump" must be a path.`);
225438
- plan.dump = resolve26(opts.outDir, s.dump);
225895
+ plan.dump = resolve27(opts.outDir, s.dump);
225439
225896
  plan.lines.push(emit(`frames 1`));
225440
225897
  plan.lines.push(emit(`dump ${plan.dump}`));
225441
225898
  budgetMs += 2 * FRAME_MS;
@@ -225480,7 +225937,7 @@ var evaluateDesktopRun = (plan, output, readFile = () => null) => {
225480
225937
  let ok = true;
225481
225938
  for (const st of plan.steps) {
225482
225939
  const skipped = [
225483
- ...st.skippedExpect.map((k) => `expect.${k} (the headless audio recorder's — not on a native host)`),
225940
+ ...st.skippedExpect.map((k) => `expect.${k} (the headless renderer's record — not on a native host)`),
225484
225941
  ...st.skippedOutputs.map((k) => `${k} output (the headless renderer's shape — use "dump" on a native host)`)
225485
225942
  ];
225486
225943
  let error;
@@ -225586,7 +226043,7 @@ var parseNativeViews = (raw) => {
225586
226043
  var requireRenderer = async () => loadPeer("lecodes-headless", "headless", { for: "lecodes render" });
225587
226044
  var fileScenarioIO = (dir) => ({
225588
226045
  write: (relPath, data) => {
225589
- const abs = resolve27(dir, relPath);
226046
+ const abs = resolve28(dir, relPath);
225590
226047
  mkdirSync29(dirname22(abs), { recursive: true });
225591
226048
  writeFileSync31(abs, data);
225592
226049
  }
@@ -225596,9 +226053,9 @@ var resolveFixtures = (value, baseDir) => {
225596
226053
  return;
225597
226054
  let map = value;
225598
226055
  if (typeof value === "string") {
225599
- const abs = resolve27(baseDir, value);
226056
+ const abs = resolve28(baseDir, value);
225600
226057
  try {
225601
- map = JSON.parse(readFileSync35(abs, "utf8"));
226058
+ map = JSON.parse(readFileSync36(abs, "utf8"));
225602
226059
  } catch (e) {
225603
226060
  throw new CliError(`Can't read fixtures "${value}": ${e instanceof Error ? e.message : String(e)}`);
225604
226061
  }
@@ -225611,18 +226068,18 @@ var resolveFixtures = (value, baseDir) => {
225611
226068
  var readScenarioFile = (path) => {
225612
226069
  let raw;
225613
226070
  try {
225614
- raw = JSON.parse(readFileSync35(resolve27(path), "utf8"));
226071
+ raw = JSON.parse(readFileSync36(resolve28(path), "utf8"));
225615
226072
  } catch (e) {
225616
226073
  throw new CliError(`Can't read scenario "${path}": ${e instanceof Error ? e.message : String(e)}`);
225617
226074
  }
225618
226075
  if (Array.isArray(raw))
225619
- return { steps: resolvePicks(raw, dirname22(resolve27(path))) };
226076
+ return { steps: resolvePicks(raw, dirname22(resolve28(path))) };
225620
226077
  if (typeof raw !== "object" || raw === null)
225621
226078
  throw new CliError(`Scenario "${path}" must be a JSON object with "steps" (or a bare step array).`);
225622
226079
  const file = raw;
225623
226080
  if (file.steps === undefined)
225624
226081
  throw new CliError(`Scenario "${path}" has no "steps".`);
225625
- return { ...file, steps: resolvePicks(file.steps, dirname22(resolve27(path))) };
226082
+ return { ...file, steps: resolvePicks(file.steps, dirname22(resolve28(path))) };
225626
226083
  };
225627
226084
  var parseClip = (raw) => {
225628
226085
  if (raw === undefined)
@@ -225747,7 +226204,7 @@ var renderDesktop = async (flags, file) => {
225747
226204
  entryOverride: screen?.entryPath
225748
226205
  });
225749
226206
  if (!sim)
225750
- mkdirSync29(dirname22(resolve27(pngFlag)), { recursive: true });
226207
+ mkdirSync29(dirname22(resolve28(pngFlag)), { recursive: true });
225751
226208
  if (scenario) {
225752
226209
  const outDir = flags["out-dir"];
225753
226210
  const plan = translateScenario(scenario.steps, { outDir, headroomFrames: frames, sim });
@@ -225843,7 +226300,7 @@ var renderHeadless = async (flags, file) => {
225843
226300
  const width = flags.width || scenario?.width || screen?.frame?.[0] || projectViewport?.width || 390;
225844
226301
  const height = flags.height || scenario?.height || screen?.frame?.[1] || projectViewport?.height || 844;
225845
226302
  const onConsole = flags.logs ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
225846
- const envelopeFixtures = resolveFixtures(scenario?.fixtures, flags.script !== undefined ? dirname22(resolve27(flags.script)) : ".");
226303
+ const envelopeFixtures = resolveFixtures(scenario?.fixtures, flags.script !== undefined ? dirname22(resolve28(flags.script)) : ".");
225847
226304
  const flagFixtures = resolveFixtures(flags.fixtures, ".");
225848
226305
  const fixtures = envelopeFixtures || flagFixtures ? { ...envelopeFixtures, ...flagFixtures } : undefined;
225849
226306
  if (fixtures) {
@@ -225884,7 +226341,7 @@ var renderHeadless = async (flags, file) => {
225884
226341
  ${c.red(st.error ?? "failed")}`);
225885
226342
  }
225886
226343
  if (!result.ok && result.failureArtifacts.length > 0) {
225887
- logErr(c.dim(` failing frame: ${result.failureArtifacts.map((p) => resolve27(outDir, p)).join(", ")}`));
226344
+ logErr(c.dim(` failing frame: ${result.failureArtifacts.map((p) => resolve28(outDir, p)).join(", ")}`));
225888
226345
  }
225889
226346
  logErr(result.ok ? c.green(`✓ ${result.steps.length}/${steps.length} steps passed`) : c.red(`✗ failed at step ${result.steps.length} of ${steps.length}`));
225890
226347
  return done((cb) => cb(), result.ok ? 0 : 1);
@@ -225905,10 +226362,10 @@ var renderHeadless = async (flags, file) => {
225905
226362
  for (const w of [...warnings, ...mapWarnings])
225906
226363
  warnErr(w);
225907
226364
  const pngPath = mapFlag.endsWith(".png") ? mapFlag : mapFlag + ".png";
225908
- mkdirSync29(dirname22(resolve27(pngPath)), { recursive: true });
225909
- writeFileSync31(resolve27(pngPath), png);
226365
+ mkdirSync29(dirname22(resolve28(pngPath)), { recursive: true });
226366
+ writeFileSync31(resolve28(pngPath), png);
225910
226367
  const jsonPath = pngPath.slice(0, -4) + ".json";
225911
- writeFileSync31(resolve27(jsonPath), JSON.stringify(data, null, 2));
226368
+ writeFileSync31(resolve28(jsonPath), JSON.stringify(data, null, 2));
225912
226369
  note(`Wrote ${pngPath} + ${jsonPath}`);
225913
226370
  return done((cb) => cb());
225914
226371
  }
@@ -225928,8 +226385,8 @@ var renderHeadless = async (flags, file) => {
225928
226385
  if (isNullSink(pngFlag)) {
225929
226386
  note(`Discarded PNG (${pngFlag})`);
225930
226387
  } else {
225931
- mkdirSync29(dirname22(resolve27(pngFlag)), { recursive: true });
225932
- writeFileSync31(resolve27(pngFlag), png);
226388
+ mkdirSync29(dirname22(resolve28(pngFlag)), { recursive: true });
226389
+ writeFileSync31(resolve28(pngFlag), png);
225933
226390
  note(`Wrote ${pngFlag} (${png.length} bytes, ${outW}x${outH}${clip !== undefined ? ", clipped" : ""})`);
225934
226391
  }
225935
226392
  return done((cb) => cb());
@@ -225944,7 +226401,7 @@ var renderHeadless = async (flags, file) => {
225944
226401
  return done((cb) => cb());
225945
226402
  }
225946
226403
  if (out) {
225947
- writeFileSync31(resolve27(out), result.text);
226404
+ writeFileSync31(resolve28(out), result.text);
225948
226405
  note(`Wrote ${out} (${result.text.length} bytes)`);
225949
226406
  return done((cb) => cb());
225950
226407
  }
@@ -225956,8 +226413,8 @@ var renderHeadless = async (flags, file) => {
225956
226413
 
225957
226414
  // src/commands/scene.ts
225958
226415
  init_cli();
225959
- import { existsSync as existsSync46, readFileSync as readFileSync37, readdirSync as readdirSync19, statSync as statSync18, writeFileSync as writeFileSync33 } from "node:fs";
225960
- import { basename as basename24, dirname as dirname23, join as join51, relative as relative10, resolve as resolve28 } from "node:path";
226416
+ import { existsSync as existsSync47, readFileSync as readFileSync38, readdirSync as readdirSync19, statSync as statSync18, writeFileSync as writeFileSync33 } from "node:fs";
226417
+ import { basename as basename24, dirname as dirname23, join as join51, relative as relative10, resolve as resolve29 } from "node:path";
225961
226418
 
225962
226419
  // src/compile/sceneCompile.ts
225963
226420
  init_localFiles();
@@ -225965,7 +226422,7 @@ init_ignore();
225965
226422
  init_cli();
225966
226423
  init_projectCompile();
225967
226424
  import { createHash as createHash8 } from "node:crypto";
225968
- import { existsSync as existsSync45, mkdirSync as mkdirSync30, readFileSync as readFileSync36, writeFileSync as writeFileSync32 } from "node:fs";
226425
+ import { existsSync as existsSync46, mkdirSync as mkdirSync30, readFileSync as readFileSync37, writeFileSync as writeFileSync32 } from "node:fs";
225969
226426
  import { join as join50 } from "node:path";
225970
226427
  var TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i;
225971
226428
  var rel = (key) => key.replace(/^\//, "");
@@ -225977,9 +226434,9 @@ var compileSceneEditor = async (opts) => {
225977
226434
  for (const file of scan.files) {
225978
226435
  const path = "./" + rel(file.path);
225979
226436
  if (TEXT_EXT.test(path)) {
225980
- entries.push({ path, type: "text", text: readFileSync36(file.absPath, "utf8") });
226437
+ entries.push({ path, type: "text", text: readFileSync37(file.absPath, "utf8") });
225981
226438
  } else if (/\.mat$/i.test(path)) {
225982
- entries.push({ path, type: "shader", text: readFileSync36(file.absPath, "utf8"), absPath: file.absPath });
226439
+ entries.push({ path, type: "shader", text: readFileSync37(file.absPath, "utf8"), absPath: file.absPath });
225983
226440
  } else {
225984
226441
  entries.push({ path, type: "resource", fileSrc: "/project/" + rel(file.path) });
225985
226442
  }
@@ -225997,7 +226454,7 @@ var compileSceneEditor = async (opts) => {
225997
226454
  const dir = shadersCacheDir(root);
225998
226455
  mkdirSync30(dir, { recursive: true });
225999
226456
  const liteName = `${hash}_webgl-lite.filamat`;
226000
- if (!existsSync45(join50(dir, liteName)))
226457
+ if (!existsSync46(join50(dir, liteName)))
226001
226458
  writeFileSync32(join50(dir, liteName), JSON.stringify(buildLiteMaterialArtifact(e.text ?? "")));
226002
226459
  const src = `/project/.lecodes/shaders/${hash}_opengl.filamat`;
226003
226460
  e.shaders = ["opengl", "webgl-lite"].map((platform) => ({ platform, src }));
@@ -226035,7 +226492,7 @@ var DEFAULT_PORT3 = 4488;
226035
226492
  var findAppRoot = (start) => {
226036
226493
  let dir = start;
226037
226494
  while (true) {
226038
- if (existsSync46(join51(dir, "app.json")))
226495
+ if (existsSync47(join51(dir, "app.json")))
226039
226496
  return dir;
226040
226497
  const parent = dirname23(dir);
226041
226498
  if (parent === dir)
@@ -226060,10 +226517,10 @@ var getContext2 = (pathArg) => {
226060
226517
  root = process.cwd();
226061
226518
  manifest = null;
226062
226519
  if (pathArg) {
226063
- const abs = resolve28(process.cwd(), pathArg);
226064
- if (existsSync46(abs) && statSync18(abs).isDirectory())
226520
+ const abs = resolve29(process.cwd(), pathArg);
226521
+ if (existsSync47(abs) && statSync18(abs).isDirectory())
226065
226522
  root = abs;
226066
- else if (abs.endsWith(".scene.ts") && existsSync46(abs))
226523
+ else if (abs.endsWith(".scene.ts") && existsSync47(abs))
226067
226524
  root = findAppRoot(dirname23(abs)) ?? dirname23(abs);
226068
226525
  else
226069
226526
  throw new CliError(`No such folder or .scene.ts: ${pathArg}`);
@@ -226073,9 +226530,9 @@ var getContext2 = (pathArg) => {
226073
226530
  }
226074
226531
  let scene = null;
226075
226532
  if (pathArg?.endsWith(".scene.ts")) {
226076
- const abs = resolve28(process.cwd(), pathArg);
226533
+ const abs = resolve29(process.cwd(), pathArg);
226077
226534
  const rel = relative10(root, abs).replace(/\\/g, "/");
226078
- if (existsSync46(abs) && !rel.startsWith(".."))
226535
+ if (existsSync47(abs) && !rel.startsWith(".."))
226079
226536
  scene = "./" + rel;
226080
226537
  }
226081
226538
  return { root, manifest, name: manifest?.name ?? basename24(root), scene };
@@ -226084,9 +226541,9 @@ var writeMcpConfig2 = (root, port) => {
226084
226541
  const path = join51(root, ".mcp.json");
226085
226542
  const entry = { type: "http", url: `http://localhost:${port}/mcp` };
226086
226543
  let config = {};
226087
- if (existsSync46(path)) {
226544
+ if (existsSync47(path)) {
226088
226545
  try {
226089
- config = JSON.parse(readFileSync37(path, "utf8"));
226546
+ config = JSON.parse(readFileSync38(path, "utf8"));
226090
226547
  } catch {
226091
226548
  warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-scene server by hand.");
226092
226549
  return;
@@ -226201,7 +226658,7 @@ var serve_default = defineCommand({
226201
226658
  });
226202
226659
 
226203
226660
  // src/commands/shaders/index.ts
226204
- import { readFileSync as readFileSync39 } from "node:fs";
226661
+ import { readFileSync as readFileSync40 } from "node:fs";
226205
226662
  import { relative as relative12 } from "node:path";
226206
226663
  init_cli();
226207
226664
  init_shared();
@@ -226210,15 +226667,15 @@ init_shared();
226210
226667
  init_compile();
226211
226668
  init_distRoot();
226212
226669
  init_cli();
226213
- import { existsSync as existsSync47, mkdirSync as mkdirSync31, readdirSync as readdirSync20, readFileSync as readFileSync38, writeFileSync as writeFileSync34 } from "node:fs";
226214
- import { basename as basename26, join as join52, relative as relative11, resolve as resolve29 } from "node:path";
226670
+ import { existsSync as existsSync48, mkdirSync as mkdirSync31, readdirSync as readdirSync20, readFileSync as readFileSync39, writeFileSync as writeFileSync34 } from "node:fs";
226671
+ import { basename as basename26, join as join52, relative as relative11, resolve as resolve30 } from "node:path";
226215
226672
  var engineMaterialsDir = () => {
226216
226673
  const candidates = [
226217
226674
  join52(distRoot, "runtime", "materials"),
226218
226675
  join52(distRoot, "..", "..", "engines", "gl", "materials", "src")
226219
226676
  ];
226220
226677
  for (const c of candidates)
226221
- if (existsSync47(join52(c, "unlit.mat")))
226678
+ if (existsSync48(join52(c, "unlit.mat")))
226222
226679
  return c;
226223
226680
  return null;
226224
226681
  };
@@ -226300,11 +226757,11 @@ var writeNewShader = (o) => {
226300
226757
  const name = o.name.endsWith(".mat") ? o.name.slice(0, -4) : o.name;
226301
226758
  if (!NAME_RE.test(name))
226302
226759
  throw new CliError(`"${o.name}" is not a shader name — letters, digits, - and _ (starting with a letter), e.g. hologram or rim-light`);
226303
- const dir = resolve29(o.cwd ?? process.cwd(), o.dir ?? ".");
226760
+ const dir = resolve30(o.cwd ?? process.cwd(), o.dir ?? ".");
226304
226761
  const matPath = join52(dir, `${name}.mat`), assetPath = join52(dir, `${name}.material.ts`);
226305
226762
  if (!o.force) {
226306
226763
  for (const p of [matPath, assetPath])
226307
- if (existsSync47(p))
226764
+ if (existsSync48(p))
226308
226765
  throw new CliError(`${relative11(o.cwd ?? process.cwd(), p) || basename26(p)} exists — pick another name or pass --force to overwrite`);
226309
226766
  }
226310
226767
  let mat;
@@ -226317,7 +226774,7 @@ var writeNewShader = (o) => {
226317
226774
  const known = engineMaterialNames();
226318
226775
  if (!known.includes(ref))
226319
226776
  throw new CliError(`no engine material "${ref}". Known: ${known.join(", ")}`);
226320
- mat = refMat(name, ref, readFileSync38(join52(srcDir, `${ref}.mat`), "utf8"));
226777
+ mat = refMat(name, ref, readFileSync39(join52(srcDir, `${ref}.mat`), "utf8"));
226321
226778
  } else
226322
226779
  mat = starterMat(name);
226323
226780
  mkdirSync31(dir, { recursive: true });
@@ -226349,7 +226806,7 @@ var status2 = async () => {
226349
226806
  if (matcId === null)
226350
226807
  state = c.dim("unknown (matc not downloaded)");
226351
226808
  else {
226352
- const text = readFileSync39(shader.absPath, "utf8");
226809
+ const text = readFileSync40(shader.absPath, "utf8");
226353
226810
  if (shaderIsCompiled(root, text, matcId))
226354
226811
  state = "compiled";
226355
226812
  else {
@@ -226358,7 +226815,7 @@ var status2 = async () => {
226358
226815
  }
226359
226816
  }
226360
226817
  log(` ${shader.path.padEnd(width)} ${state}`);
226361
- const schema = parseShaderSchema(readFileSync39(shader.absPath, "utf8"));
226818
+ const schema = parseShaderSchema(readFileSync40(shader.absPath, "utf8"));
226362
226819
  for (const p of schema.params) {
226363
226820
  const range = p.min !== undefined ? ` [${p.min}..${p.max}]` : "";
226364
226821
  const def = p.default !== null && p.default !== undefined ? ` = ${JSON.stringify(p.default)}` : "";
@@ -226399,7 +226856,7 @@ var compileShaders = async (platforms) => {
226399
226856
  }
226400
226857
  const matcExe = await resolveMatcExe();
226401
226858
  const matcId = matcIdentity(matcExe);
226402
- const wasCompiled = new Map(shaders.map((s) => [s.path, shaderIsCompiled(root, readFileSync39(s.absPath, "utf8"), matcId)]));
226859
+ const wasCompiled = new Map(shaders.map((s) => [s.path, shaderIsCompiled(root, readFileSync40(s.absPath, "utf8"), matcId)]));
226403
226860
  const errors = [];
226404
226861
  for (const shader of shaders) {
226405
226862
  try {
@@ -226501,14 +226958,14 @@ var status_default = defineCommand({
226501
226958
  });
226502
226959
 
226503
226960
  // src/commands/test.ts
226504
- import { existsSync as existsSync49, mkdirSync as mkdirSync33, readFileSync as readFileSync40, readdirSync as readdirSync21 } from "node:fs";
226505
- import { dirname as dirname25, resolve as resolve30 } from "node:path";
226961
+ import { existsSync as existsSync50, mkdirSync as mkdirSync33, readFileSync as readFileSync41, readdirSync as readdirSync21 } from "node:fs";
226962
+ import { dirname as dirname25, resolve as resolve31 } from "node:path";
226506
226963
  init_manifest();
226507
226964
 
226508
226965
  // src/hosts/liteRunner.ts
226509
226966
  init_cli();
226510
226967
  import { spawn as spawn7 } from "node:child_process";
226511
- import { existsSync as existsSync48, mkdirSync as mkdirSync32, rmSync as rmSync15, writeFileSync as writeFileSync35 } from "node:fs";
226968
+ import { existsSync as existsSync49, mkdirSync as mkdirSync32, rmSync as rmSync15, writeFileSync as writeFileSync35 } from "node:fs";
226512
226969
  import { createServer as createServer3 } from "node:net";
226513
226970
  import { dirname as dirname24, join as join53 } from "node:path";
226514
226971
  import { createInterface as createInterface2 } from "node:readline";
@@ -226521,7 +226978,7 @@ var NO_NODE = "--lite needs Node on the PATH: the browser is driven from a Node
226521
226978
  var NO_PLAYWRIGHT = "--lite runs the flows in a browser and needs Playwright: `npm install -D playwright` in the project, then `npx playwright install chromium` once.";
226522
226979
  var startDriver = async () => {
226523
226980
  const script = join53(distRoot, "src", "hosts", "liteBrowser.mjs");
226524
- if (!existsSync48(script))
226981
+ if (!existsSync49(script))
226525
226982
  throw new CliError("--lite is not available in this build of the CLI (its browser driver ships with the npm package).");
226526
226983
  const child = spawn7("node", [script], { stdio: ["pipe", "pipe", "inherit"] });
226527
226984
  const waiting = new Map;
@@ -227018,7 +227475,7 @@ var slug = (s) => s.replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "").sli
227018
227475
  var readFlowFile = (abs, displayName) => {
227019
227476
  let raw;
227020
227477
  try {
227021
- raw = JSON.parse(readFileSync40(abs, "utf8"));
227478
+ raw = JSON.parse(readFileSync41(abs, "utf8"));
227022
227479
  } catch (e) {
227023
227480
  throw new CliError(`Can't read "${displayName}": ${e instanceof Error ? e.message : String(e)}`);
227024
227481
  }
@@ -227112,39 +227569,39 @@ var runTests = async (args, flags) => {
227112
227569
  if (nativeFixedDt < 0 || nativeFixedDt > 1000)
227113
227570
  throw new CliError(`--fixed-dt wants a number of ms (0 = wall clock, max 1000), got "${nativeFixedDt}".`);
227114
227571
  const projectViewport = desktopDefaultViewport(root);
227115
- const testsDir = resolve30(root, flags.dir);
227116
- const designMetaPath = resolve30(root, flags["design-dir"], "meta.json");
227572
+ const testsDir = resolve31(root, flags.dir);
227573
+ const designMetaPath = resolve31(root, flags["design-dir"], "meta.json");
227117
227574
  const asJson = flags.json;
227118
227575
  const out = asJson ? logErr : log;
227119
227576
  const cases = [];
227120
227577
  if (args.length > 0) {
227121
227578
  for (const given of args) {
227122
- const candidates = [resolve30(given), resolve30(testsDir, given), resolve30(testsDir, `${given}.flow.json`)];
227123
- const abs = candidates.find((p) => existsSync49(p));
227579
+ const candidates = [resolve31(given), resolve31(testsDir, given), resolve31(testsDir, `${given}.flow.json`)];
227580
+ const abs = candidates.find((p) => existsSync50(p));
227124
227581
  if (!abs)
227125
227582
  throw new CliError(`No such test: "${given}" (looked in ${testsDir}).`);
227126
227583
  cases.push(readFlowFile(abs, given));
227127
227584
  }
227128
- } else if (existsSync49(testsDir)) {
227585
+ } else if (existsSync50(testsDir)) {
227129
227586
  for (const f of readdirSync21(testsDir).filter((f) => f.endsWith(".flow.json")).sort()) {
227130
- cases.push(readFlowFile(resolve30(testsDir, f), f));
227587
+ cases.push(readFlowFile(resolve31(testsDir, f), f));
227131
227588
  }
227132
227589
  }
227133
- const defaultPack = resolve30(testsDir, "fixtures.json");
227134
- const flowFixtures = flags.fixtures !== undefined ? resolveFixtures(flags.fixtures, ".") : existsSync49(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
227590
+ const defaultPack = resolve31(testsDir, "fixtures.json");
227591
+ const flowFixtures = flags.fixtures !== undefined ? resolveFixtures(flags.fixtures, ".") : existsSync50(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
227135
227592
  const flowsOnly = flags.flows;
227136
227593
  const wantFlows = !flags["no-flows"] && args.length === 0;
227137
227594
  if (flowsOnly)
227138
227595
  cases.length = 0;
227139
227596
  let skipped = [];
227140
227597
  let entryScreen = null;
227141
- if ((desktop || lite) && wantFlows && existsSync49(designMetaPath)) {
227598
+ if ((desktop || lite) && wantFlows && existsSync50(designMetaPath)) {
227142
227599
  note("Design-derived flows skipped with --desktop / --lite (they read expect.screen and fill inputs; run `lecodes test` headless for them).");
227143
227600
  }
227144
- if (!desktop && !lite && (wantFlows || flowsOnly) && existsSync49(designMetaPath)) {
227601
+ if (!desktop && !lite && (wantFlows || flowsOnly) && existsSync50(designMetaPath)) {
227145
227602
  let meta;
227146
227603
  try {
227147
- meta = JSON.parse(readFileSync40(designMetaPath, "utf8"));
227604
+ meta = JSON.parse(readFileSync41(designMetaPath, "utf8"));
227148
227605
  } catch (e) {
227149
227606
  throw new CliError(`Can't read ${designMetaPath}: ${e instanceof Error ? e.message : String(e)}`);
227150
227607
  }
@@ -227177,7 +227634,7 @@ var runTests = async (args, flags) => {
227177
227634
  }, timeoutMs);
227178
227635
  killer.unref?.();
227179
227636
  const onConsole = flags.logs ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
227180
- const artifactsRoot = resolve30(root, flags.artifacts ?? resolve30(testsDir, ".artifacts"));
227637
+ const artifactsRoot = resolve31(root, flags.artifacts ?? resolve31(testsDir, ".artifacts"));
227181
227638
  const results = [];
227182
227639
  const reportCase = (result) => {
227183
227640
  const skippedCount = result.steps.reduce((n, s) => n + (s.skipped?.length ?? 0), 0);
@@ -227197,13 +227654,13 @@ var runTests = async (args, flags) => {
227197
227654
  }
227198
227655
  };
227199
227656
  const runNativeCase = async (kase) => {
227200
- const caseDir = resolve30(artifactsRoot, slug(kase.name));
227657
+ const caseDir = resolve31(artifactsRoot, slug(kase.name));
227201
227658
  const plan = translateScenario(kase.steps, { outDir: caseDir, headroomFrames: nativeFrames, sim });
227202
227659
  for (const st of plan.steps)
227203
227660
  if (st.screenshot)
227204
227661
  mkdirSync33(dirname25(st.screenshot), { recursive: true });
227205
227662
  mkdirSync33(caseDir, { recursive: true });
227206
- const finalPng = resolve30(caseDir, "final.png");
227663
+ const finalPng = resolve31(caseDir, "final.png");
227207
227664
  const { output } = await runDesktopRender(native.js, native.resources, {
227208
227665
  renderer: desktopRenderer,
227209
227666
  host: sim ? "server" : "desktop",
@@ -227217,7 +227674,7 @@ var runTests = async (args, flags) => {
227217
227674
  script: plan.text,
227218
227675
  fixedDtMs: nativeFixedDt
227219
227676
  });
227220
- const r = evaluateDesktopRun(plan, output, (path) => existsSync49(path) ? readFileSync40(path, "utf8") : null);
227677
+ const r = evaluateDesktopRun(plan, output, (path) => existsSync50(path) ? readFileSync41(path, "utf8") : null);
227221
227678
  if (!r.loaded)
227222
227679
  throw new CliError("This desktop host doesn't support scripted input (CREATOR_SCRIPT). Update it: lecodes desktop update (or point LECODES_DESKTOP_EXE at a newer build).");
227223
227680
  return {
@@ -227248,7 +227705,7 @@ var runTests = async (args, flags) => {
227248
227705
  }
227249
227706
  }
227250
227707
  const runLiteCase = async (kase) => {
227251
- const caseDir = resolve30(artifactsRoot, slug(kase.name));
227708
+ const caseDir = resolve31(artifactsRoot, slug(kase.name));
227252
227709
  const plan = translateScenario(kase.steps, { outDir: caseDir, headroomFrames: 0 });
227253
227710
  mkdirSync33(caseDir, { recursive: true });
227254
227711
  const output = await liteSession.run(plan, {
@@ -227256,7 +227713,7 @@ var runTests = async (args, flags) => {
227256
227713
  height: flags.height || kase.height || projectViewport?.height || 844,
227257
227714
  onConsole
227258
227715
  });
227259
- const r = evaluateDesktopRun(plan, output, (path) => existsSync49(path) ? readFileSync40(path, "utf8") : null);
227716
+ const r = evaluateDesktopRun(plan, output, (path) => existsSync50(path) ? readFileSync41(path, "utf8") : null);
227260
227717
  return {
227261
227718
  name: kase.name,
227262
227719
  kind: kase.kind,
@@ -227307,14 +227764,14 @@ var runTests = async (args, flags) => {
227307
227764
  onConsole
227308
227765
  });
227309
227766
  try {
227310
- const io = fileScenarioIO(resolve30(artifactsRoot, slug(kase.name)));
227767
+ const io = fileScenarioIO(resolve31(artifactsRoot, slug(kase.name)));
227311
227768
  const outcome = await renderer.runScenario(session, steps, io);
227312
227769
  result = {
227313
227770
  name: kase.name,
227314
227771
  kind: kase.kind,
227315
227772
  ok: outcome.ok,
227316
227773
  steps: outcome.steps.map((s) => ({ desc: s.desc, ok: s.ok, error: s.error })),
227317
- artifacts: outcome.failureArtifacts.map((p) => resolve30(artifactsRoot, slug(kase.name), p))
227774
+ artifacts: outcome.failureArtifacts.map((p) => resolve31(artifactsRoot, slug(kase.name), p))
227318
227775
  };
227319
227776
  } finally {
227320
227777
  session.dispose();
@@ -227365,9 +227822,9 @@ var runTests = async (args, flags) => {
227365
227822
  init_peerInstall();
227366
227823
  import { createRequire as createRequire8 } from "node:module";
227367
227824
  import { execFileSync as execFileSync8 } from "node:child_process";
227368
- import { existsSync as existsSync50, mkdtempSync as mkdtempSync7, readdirSync as readdirSync22, rmSync as rmSync16, statSync as statSync19 } from "node:fs";
227825
+ import { existsSync as existsSync51, mkdtempSync as mkdtempSync7, readdirSync as readdirSync22, rmSync as rmSync16, statSync as statSync19 } from "node:fs";
227369
227826
  import { tmpdir as tmpdir7 } from "node:os";
227370
- import { extname, join as join54, relative as relative13, resolve as resolve31 } from "node:path";
227827
+ import { extname, join as join54, relative as relative13, resolve as resolve32 } from "node:path";
227371
227828
  init_cli();
227372
227829
  init_paths();
227373
227830
  var DEFAULT_SIZE = 256;
@@ -227385,7 +227842,7 @@ var findKtx = () => {
227385
227842
  "/opt/homebrew/bin/ktx"
227386
227843
  ].filter((p) => !!p);
227387
227844
  for (const path of candidates) {
227388
- if (existsSync50(path))
227845
+ if (existsSync51(path))
227389
227846
  return path;
227390
227847
  }
227391
227848
  try {
@@ -227417,7 +227874,7 @@ var ktxToPng = (ktx, file, out) => {
227417
227874
  run(["extract", "--transcode", "rgba8", "--level", "0", file, out]);
227418
227875
  } catch (e) {
227419
227876
  run(["extract", "--level", "0", file, out]);
227420
- if (!existsSync50(out))
227877
+ if (!existsSync51(out))
227421
227878
  throw e;
227422
227879
  }
227423
227880
  };
@@ -227452,8 +227909,8 @@ var thumbs = async (targets, size, force) => {
227452
227909
  const rendered = [];
227453
227910
  if (targets.length > 0) {
227454
227911
  for (const arg of targets) {
227455
- const abs = resolve31(process.cwd(), arg);
227456
- if (!existsSync50(abs))
227912
+ const abs = resolve32(process.cwd(), arg);
227913
+ if (!existsSync51(abs))
227457
227914
  throw new CliError(`No such file or folder: ${arg}`);
227458
227915
  if (statSync19(abs).isDirectory())
227459
227916
  files.push(...collect(abs));
@@ -227532,9 +227989,9 @@ init_peerInstall();
227532
227989
  init_archives();
227533
227990
  init_packageManager();
227534
227991
  import { spawnSync as spawnSync11 } from "node:child_process";
227535
- import { existsSync as existsSync51, readdirSync as readdirSync23 } from "node:fs";
227992
+ import { existsSync as existsSync52, readdirSync as readdirSync23 } from "node:fs";
227536
227993
  import { homedir as homedir14 } from "node:os";
227537
- import { dirname as dirname26, join as join55, resolve as resolve33 } from "node:path";
227994
+ import { dirname as dirname26, join as join55, resolve as resolve34 } from "node:path";
227538
227995
 
227539
227996
  // src/commands/update/types.ts
227540
227997
  init_api();
@@ -227542,10 +227999,10 @@ init_config();
227542
227999
  init_manifest();
227543
228000
  init_types4();
227544
228001
  init_cli();
227545
- import { resolve as resolve32 } from "node:path";
228002
+ import { resolve as resolve33 } from "node:path";
227546
228003
  var label = (bundle) => `${Object.keys(bundle).length} file(s)`;
227547
228004
  var refreshTypes = async (opts) => {
227548
- const root = opts.dir ? resolve32(opts.dir) : (() => {
228005
+ const root = opts.dir ? resolve33(opts.dir) : (() => {
227549
228006
  try {
227550
228007
  return findProjectRoot(process.cwd());
227551
228008
  } catch {
@@ -227603,10 +228060,10 @@ var refreshTypes = async (opts) => {
227603
228060
 
227604
228061
  // src/commands/update/index.ts
227605
228062
  var findProjectDir = (start) => {
227606
- const home = resolve33(homedir14());
227607
- let dir = resolve33(start);
228063
+ const home = resolve34(homedir14());
228064
+ let dir = resolve34(start);
227608
228065
  while (true) {
227609
- if (dir !== home && (existsSync51(join55(dir, "app.json")) || existsSync51(join55(dir, ".lecodes", "manifest.json")) || existsSync51(join55(dir, ".lecodes", "types"))))
228066
+ if (dir !== home && (existsSync52(join55(dir, "app.json")) || existsSync52(join55(dir, ".lecodes", "manifest.json")) || existsSync52(join55(dir, ".lecodes", "types"))))
227610
228067
  return dir;
227611
228068
  const parent = dirname26(dir);
227612
228069
  if (parent === dir)
@@ -227643,7 +228100,7 @@ var semverNewer2 = (a, b) => {
227643
228100
  return 0;
227644
228101
  };
227645
228102
  var FROM_SOURCE = /[\\/]src[\\/]commands[\\/]/.test(import.meta.url);
227646
- var STANDALONE = existsSync51(join55(dirname26(process.execPath), "runtime")) && existsSync51(join55(dirname26(process.execPath), process.platform === "win32" ? "chisel.exe" : "chisel"));
228103
+ var STANDALONE = existsSync52(join55(dirname26(process.execPath), "runtime")) && existsSync52(join55(dirname26(process.execPath), process.platform === "win32" ? "chisel.exe" : "chisel"));
227647
228104
  var update_default = defineCommand({
227648
228105
  name: "update",
227649
228106
  summary: "Move everything the project depends on to its newest published state",
@@ -227703,7 +228160,7 @@ ${c.bold(name)} ${c.dim(what)}`);
227703
228160
  return ["skipped", "not inside a project"];
227704
228161
  if (flags["no-shells"])
227705
228162
  return ["skipped", "--no-shells"];
227706
- const present = ["ios", "android"].filter((d) => existsSync51(join55(root, d)) && readdirSync23(join55(root, d)).length > 0);
228163
+ const present = ["ios", "android"].filter((d) => existsSync52(join55(root, d)) && readdirSync23(join55(root, d)).length > 0);
227707
228164
  if (present.length === 0)
227708
228165
  return ["skipped", "no ios/ or android/ shell in this project (`lecodes app init ios|android`)"];
227709
228166
  await runSubcommand(app_default, "update");
@@ -227781,8 +228238,8 @@ init_cli();
227781
228238
 
227782
228239
  // src/web/build.ts
227783
228240
  import { createHash as createHash9 } from "node:crypto";
227784
- import { copyFileSync as copyFileSync2, cpSync as cpSync6, existsSync as existsSync52, mkdirSync as mkdirSync34, readFileSync as readFileSync41, readdirSync as readdirSync24, rmSync as rmSync17, statSync as statSync20, writeFileSync as writeFileSync36 } from "node:fs";
227785
- import { dirname as dirname27, extname as extname2, join as join56, resolve as resolve34 } from "node:path";
228241
+ import { copyFileSync as copyFileSync2, cpSync as cpSync6, existsSync as existsSync53, mkdirSync as mkdirSync34, readFileSync as readFileSync42, readdirSync as readdirSync24, rmSync as rmSync17, statSync as statSync20, writeFileSync as writeFileSync36 } from "node:fs";
228242
+ import { dirname as dirname27, extname as extname2, join as join56, resolve as resolve35 } from "node:path";
227786
228243
  init_fonts();
227787
228244
  init_assetIcons();
227788
228245
  init_projectCompile();
@@ -227806,8 +228263,8 @@ var walk = (dir, prefix = "") => {
227806
228263
  };
227807
228264
  var buildWeb = async (options) => {
227808
228265
  const { root, manifest, name, lite } = options;
227809
- const out = resolve34(root, options.out);
227810
- if (out === resolve34(root) || resolve34(root).startsWith(out + "\\") || resolve34(root).startsWith(out + "/")) {
228266
+ const out = resolve35(root, options.out);
228267
+ if (out === resolve35(root) || resolve35(root).startsWith(out + "\\") || resolve35(root).startsWith(out + "/")) {
227811
228268
  throw new CliError(`The output folder is cleaned before a build: "${options.out}" holds the project itself.`);
227812
228269
  }
227813
228270
  const config = readAppConfigAt(root)?.web ?? {};
@@ -227817,7 +228274,7 @@ var buildWeb = async (options) => {
227817
228274
  const assetUrl = (path) => "assets" + encodeURI(path);
227818
228275
  const collected = collectLocalEntries(root, manifest, "assets");
227819
228276
  const entries = collected.entries.filter((e) => {
227820
- const abs = e.absPath ? resolve34(e.absPath) : null;
228277
+ const abs = e.absPath ? resolve35(e.absPath) : null;
227821
228278
  return !(abs && (abs === out || abs.startsWith(out + "\\") || abs.startsWith(out + "/")));
227822
228279
  });
227823
228280
  for (const w of collected.warnings)
@@ -227859,8 +228316,8 @@ var buildWeb = async (options) => {
227859
228316
  mkdirSync34(out, { recursive: true });
227860
228317
  cpSync6(lite, out, { recursive: true });
227861
228318
  const table = join56(out, "features.json");
227862
- if (existsSync52(table)) {
227863
- const { features, fonts } = JSON.parse(readFileSync41(table, "utf8"));
228319
+ if (existsSync53(table)) {
228320
+ const { features, fonts } = JSON.parse(readFileSync42(table, "utf8"));
227864
228321
  if (config.font === "system")
227865
228322
  for (const file of fonts ?? [])
227866
228323
  rmSync17(join56(out, file), { force: true });
@@ -227873,9 +228330,9 @@ var buildWeb = async (options) => {
227873
228330
  rmSync17(table);
227874
228331
  }
227875
228332
  const pluginsTable = join56(out, "plugins.json");
227876
- if (existsSync52(pluginsTable)) {
228333
+ if (existsSync53(pluginsTable)) {
227877
228334
  const listed = new Set((/^\/\/ plugins: (.+)$/m.exec(header)?.[1] ?? "").split(";").map((id) => id.trim()).filter(Boolean));
227878
- const { plugins } = JSON.parse(readFileSync41(pluginsTable, "utf8"));
228335
+ const { plugins } = JSON.parse(readFileSync42(pluginsTable, "utf8"));
227879
228336
  const kept = {};
227880
228337
  for (const id of Object.keys(plugins)) {
227881
228338
  if (listed.has(id))
@@ -227901,7 +228358,7 @@ var buildWeb = async (options) => {
227901
228358
  if (!js.includes(entry.fileSrc))
227902
228359
  continue;
227903
228360
  const to = join56(out, "assets", ...entry.path.split("/").filter(Boolean));
227904
- if (existsSync52(to)) {
228361
+ if (existsSync53(to)) {
227905
228362
  warnings.push(`${entry.path}: a file of the page has this name in the built folder — the project's file was not copied`);
227906
228363
  continue;
227907
228364
  }
@@ -227909,12 +228366,12 @@ var buildWeb = async (options) => {
227909
228366
  copyFileSync2(entry.absPath, to);
227910
228367
  }
227911
228368
  const app = readAppConfigAt(root);
227912
- let html = readFileSync41(join56(lite, "index.html"), "utf8");
228369
+ let html = readFileSync42(join56(lite, "index.html"), "utf8");
227913
228370
  const head = [];
227914
228371
  head.push(`<meta name="theme-color" content="${escapeHtml(config.themeColor ?? "#000000")}" />`);
227915
228372
  if (app?.icon) {
227916
- const icon = resolve34(root, app.icon);
227917
- if (existsSync52(icon)) {
228373
+ const icon = resolve35(root, app.icon);
228374
+ if (existsSync53(icon)) {
227918
228375
  const file = `icon${extname2(icon).toLowerCase() || ".png"}`;
227919
228376
  copyFileSync2(icon, join56(out, file));
227920
228377
  head.push(`<link rel="icon" href="${file}" />`, `<link rel="apple-touch-icon" href="${file}" />`);
@@ -227949,12 +228406,12 @@ var buildWeb = async (options) => {
227949
228406
  };
227950
228407
 
227951
228408
  // src/web/preview.ts
227952
- import { createReadStream as createReadStream3, existsSync as existsSync53, statSync as statSync21 } from "node:fs";
228409
+ import { createReadStream as createReadStream3, existsSync as existsSync54, statSync as statSync21 } from "node:fs";
227953
228410
  import { createServer as createServer4 } from "node:http";
227954
- import { join as join57, resolve as resolve35 } from "node:path";
228411
+ import { join as join57, resolve as resolve36 } from "node:path";
227955
228412
  var startPreview = async (dir, port) => {
227956
- const root = resolve35(dir);
227957
- const fallback = existsSync53(join57(root, "404.html")) ? join57(root, "index.html") : null;
228413
+ const root = resolve36(dir);
228414
+ const fallback = existsSync54(join57(root, "404.html")) ? join57(root, "index.html") : null;
227958
228415
  const server2 = createServer4((req, res) => {
227959
228416
  let path;
227960
228417
  try {
@@ -227971,10 +228428,10 @@ var startPreview = async (dir, port) => {
227971
228428
  return;
227972
228429
  }
227973
228430
  let file = join57(root, ...parts);
227974
- if (existsSync53(file) && statSync21(file).isDirectory())
228431
+ if (existsSync54(file) && statSync21(file).isDirectory())
227975
228432
  file = join57(file, "index.html");
227976
228433
  let status = 200;
227977
- if (!existsSync53(file) || !statSync21(file).isFile()) {
228434
+ if (!existsSync54(file) || !statSync21(file).isFile()) {
227978
228435
  if (!fallback || /\.[a-z0-9]+$/i.test(parts[parts.length - 1] ?? "")) {
227979
228436
  res.writeHead(404, { "content-type": "text/plain" });
227980
228437
  res.end("Not found");
@@ -228164,7 +228621,7 @@ var web_default = defineCommand({
228164
228621
 
228165
228622
  // src/commands/index.ts
228166
228623
  var commandGroups = (opts) => [
228167
- { title: "Project", commands: [init_default, create_default, clone_default, link_default, status_default, diff_default, pull_default, push_default, login_default] },
228624
+ { title: "Project", commands: [init_default, create_default, clone_default, link_default, status_default, diff_default, pull_default, push_default, publish_default, releases_default, login_default] },
228168
228625
  { title: "Run & test", commands: [
228169
228626
  dev_default,
228170
228627
  web_default,
@@ -228201,7 +228658,7 @@ init_peers();
228201
228658
  var STANDALONE2 = (() => {
228202
228659
  const dir = dirname28(process.execPath);
228203
228660
  const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
228204
- return existsSync55(join59(dir, "runtime")) && existsSync55(join59(dir, chisel));
228661
+ return existsSync56(join59(dir, "runtime")) && existsSync56(join59(dir, chisel));
228205
228662
  })();
228206
228663
  var INSTALLED = STANDALONE2 && (() => {
228207
228664
  const exe = process.platform === "win32" ? "lecodes.exe" : "lecodes";
@@ -228217,7 +228674,7 @@ var INSTALLED = STANDALONE2 && (() => {
228217
228674
  return false;
228218
228675
  try {
228219
228676
  const cand = join59(d, exe);
228220
- return existsSync55(cand) && realpathSync2(cand) === self2;
228677
+ return existsSync56(cand) && realpathSync2(cand) === self2;
228221
228678
  } catch {
228222
228679
  return false;
228223
228680
  }