lecodes-cli 2.2.10 → 2.2.13

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.10",
67
+ version: "2.2.13",
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.10",
91
+ "lecodes-headless": "^2.2.13",
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.10";
161
+ var SDK_VERSION = "2.2.13";
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
@@ -171163,7 +171246,7 @@ var init_shaderSchema = __esm(() => {
171163
171246
  });
171164
171247
 
171165
171248
  // ../../sdk/src/compile/header.ts
171166
- var buildHeader = (name, code, opts = {}) => {
171249
+ var ENGINE_SIGNALS, buildHeader = (name, code, opts = {}) => {
171167
171250
  let header = `// sdk: ${SDK_VERSION}
171168
171251
  `;
171169
171252
  header += `// name: ${name.slice(0, 20)}
@@ -171181,6 +171264,10 @@ var buildHeader = (name, code, opts = {}) => {
171181
171264
  `;
171182
171265
  if (code.includes("_creator2d.createScene"))
171183
171266
  header += `// 2d: enable
171267
+ `;
171268
+ for (const [name, re] of ENGINE_SIGNALS)
171269
+ if (re.test(code))
171270
+ header += `// ${name}: enable
171184
171271
  `;
171185
171272
  if (usesAR(code, opts.plugins))
171186
171273
  header += `// ar: enable
@@ -171214,7 +171301,14 @@ var buildHeader = (name, code, opts = {}) => {
171214
171301
  `;
171215
171302
  return header;
171216
171303
  }, usesAR = (code, plugins) => !!plugins?.includes("ar") && code.includes("_creator.createScene");
171217
- var init_header = () => {};
171304
+ var init_header = __esm(() => {
171305
+ ENGINE_SIGNALS = [
171306
+ ["physics", /_creator\.(?:physicsCreateBody|physicsConfigure|physicsRaycast|characterCreate|vehicleCreate|ragdollCreate)\(/],
171307
+ ["net", /\b_creatorNet\b/],
171308
+ ["nav", /_creatorNav\.\w+\(/],
171309
+ ["audio", /_creatorAudio\.(?:loadClip|play)\(/]
171310
+ ];
171311
+ });
171218
171312
 
171219
171313
  // ../../sdk/src/compile/sourcemap.ts
171220
171314
  var offsetSourceMap = (mapJson, lines) => {
@@ -171228,295 +171322,8 @@ var offsetSourceMap = (mapJson, lines) => {
171228
171322
  return `//# lecodesMap=data:application/json;charset=utf-8;base64,${b64}`;
171229
171323
  };
171230
171324
 
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) => {
171325
+ // ../../sdk/src/compile/assetIconMacro.ts
171326
+ 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
171327
  const quote = src[start];
171521
171328
  const n = src.length;
171522
171329
  let i = start + 1;
@@ -171626,12 +171433,7 @@ var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c) => c >= "A" && c <= "Z
171626
171433
  if (inner.includes(q) && !inner.includes("\\" + q))
171627
171434
  return null;
171628
171435
  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) => {
171436
+ }, scanAssetIconCalls = (source) => {
171635
171437
  const calls = [];
171636
171438
  const n = source.length;
171637
171439
  let i = 0;
@@ -171659,7 +171461,7 @@ var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c) => c >= "A" && c <= "Z
171659
171461
  let j = i + 1;
171660
171462
  while (j < n && isIdentPart(source[j]))
171661
171463
  j++;
171662
- if (source.slice(i, j) === "font" && prevNonSpace(source, i) !== ".") {
171464
+ if (source.slice(i, j) === "assetIcon" && prevNonSpace(source, i) !== ".") {
171663
171465
  const call = parseCall(source, i, j);
171664
171466
  if (call) {
171665
171467
  calls.push(call);
@@ -171673,6 +171475,11 @@ var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c) => c >= "A" && c <= "Z
171673
171475
  i++;
171674
171476
  }
171675
171477
  return calls;
171478
+ }, prevNonSpace = (src, i) => {
171479
+ let k = i - 1;
171480
+ while (k >= 0 && /\s/.test(src[k]))
171481
+ k--;
171482
+ return k >= 0 ? src[k] : "";
171676
171483
  }, parseCall = (src, start, identEnd) => {
171677
171484
  const n = src.length;
171678
171485
  let i = identEnd;
@@ -171683,9 +171490,621 @@ var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c) => c >= "A" && c <= "Z
171683
171490
  const close = matchParen(src, i);
171684
171491
  if (close === -1)
171685
171492
  return null;
171686
- const call = { start, end: close + 1, arg: "", kind: "registry" };
171493
+ const call = { start, end: close + 1, id: "", pack: "", name: "" };
171687
171494
  const args = splitTopLevel(src.slice(i + 1, close), ",");
171688
- const arg = stringLiteralValue(args[0] ?? "");
171495
+ const id = stringLiteralValue(args[0] ?? "");
171496
+ if (id === null) {
171497
+ call.error = `assetIcon(...) needs a string-literal id like "lucide:bell" (got \`${(args[0] ?? "").trim() || "()"}\`).`;
171498
+ return call;
171499
+ }
171500
+ const m = ICON_ID.exec(id);
171501
+ if (!m) {
171502
+ call.error = `assetIcon("${id}"): id must be "pack:name", lowercase [a-z0-9-] (e.g. "lucide:bell").`;
171503
+ return call;
171504
+ }
171505
+ call.id = id;
171506
+ call.pack = m[1];
171507
+ call.name = m[2];
171508
+ const optsText = args[1]?.trim();
171509
+ if (optsText) {
171510
+ const color = extractColor(optsText);
171511
+ if (color.error) {
171512
+ call.error = color.error;
171513
+ return call;
171514
+ }
171515
+ if (color.bake !== undefined)
171516
+ call.bake = color.bake;
171517
+ else if (color.tint !== undefined)
171518
+ call.tint = color.tint;
171519
+ }
171520
+ return call;
171521
+ }, extractColor = (optsText) => {
171522
+ if (!optsText.startsWith("{") || !optsText.endsWith("}")) {
171523
+ return { error: `assetIcon(id, opts): opts must be an object literal like { color: "#5b8cff" }.` };
171524
+ }
171525
+ for (const prop of splitTopLevel(optsText.slice(1, -1), ",")) {
171526
+ const kv = splitTopLevel(prop, ":");
171527
+ if (kv.length < 2)
171528
+ continue;
171529
+ const key = kv[0].trim().replace(/^["'`]|["'`]$/g, "");
171530
+ if (key !== "color")
171531
+ continue;
171532
+ const raw = kv.slice(1).join(":").trim();
171533
+ if (!raw)
171534
+ continue;
171535
+ const literal = stringLiteralValue(raw);
171536
+ return literal !== null ? { bake: literal } : { tint: raw };
171537
+ }
171538
+ return {};
171539
+ }, rewriteAssetIcons = (source, calls, svgById) => {
171540
+ let out = source;
171541
+ for (const call of [...calls].sort((a, b) => b.start - a.start)) {
171542
+ const svg = svgById.get(call.id);
171543
+ if (svg === undefined)
171544
+ continue;
171545
+ out = out.slice(0, call.start) + iconExpr(svg, call) + out.slice(call.end);
171546
+ }
171547
+ return out;
171548
+ }, iconExpr = (svg, call) => {
171549
+ const body = call.bake ? svg.split("currentColor").join(call.bake) : svg;
171550
+ const src = `SvgSource(\`${escapeTemplate(body)}\`)`;
171551
+ return call.tint ? `Object.assign(${src}, { tintColor: (${call.tint}) })` : src;
171552
+ }, escapeTemplate = (s) => s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${"), applyAssetIconMacro = async (entries, resolveSvgs) => {
171553
+ 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);
171554
+ if (scanned.length === 0)
171555
+ return [];
171556
+ const errors = scanned.flatMap((s) => s.calls.filter((c) => c.error).map((c) => c.error));
171557
+ if (errors.length)
171558
+ throw new Error([...new Set(errors)].join(`
171559
+ `));
171560
+ const ids = [...new Set(scanned.flatMap((s) => s.calls.map((c) => c.id)))];
171561
+ const svgById = await resolveSvgs(ids);
171562
+ const missing = ids.filter((id) => !svgById.has(id));
171563
+ if (missing.length)
171564
+ throw new Error(`assetIcon: could not resolve icon(s): ${missing.join(", ")}.`);
171565
+ for (const { entry, calls } of scanned)
171566
+ entry.text = rewriteAssetIcons(entry.text, calls, svgById);
171567
+ return ids;
171568
+ }, buildIconsHeaderValue = (ids) => [...new Set(ids.map((id) => id.split(":")[0]))].sort().join(";"), editDistance = (a, b, cap) => {
171569
+ if (Math.abs(a.length - b.length) > cap)
171570
+ return cap + 1;
171571
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
171572
+ for (let i = 1;i <= a.length; i++) {
171573
+ const cur = [i];
171574
+ let rowMin = i;
171575
+ for (let j = 1;j <= b.length; j++) {
171576
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
171577
+ if (cur[j] < rowMin)
171578
+ rowMin = cur[j];
171579
+ }
171580
+ if (rowMin > cap)
171581
+ return cap + 1;
171582
+ prev = cur;
171583
+ }
171584
+ return prev[b.length];
171585
+ }, suggestIconNames = (want, icons) => {
171586
+ const wantParts = new Set(want.split("-").filter(Boolean));
171587
+ const scored = [];
171588
+ for (const [name, info] of Object.entries(icons)) {
171589
+ const lenDiff = Math.abs(name.length - want.length) / 100;
171590
+ let rank;
171591
+ if (name.includes(want) || want.includes(name))
171592
+ rank = lenDiff;
171593
+ else {
171594
+ const d = editDistance(want, name, 2);
171595
+ if (d <= 2)
171596
+ rank = 1 + d;
171597
+ else if (info.kw?.some((k) => wantParts.has(k)))
171598
+ rank = 4 + lenDiff;
171599
+ else
171600
+ continue;
171601
+ }
171602
+ scored.push({ name, rank });
171603
+ }
171604
+ return scored.sort((a, b) => a.rank - b.rank || (a.name < b.name ? -1 : 1)).slice(0, 5).map((s) => s.name);
171605
+ }, iconNotFoundError = async (pack, name, read, registry) => {
171606
+ const metaRaw = await read(`${pack}/meta.json`);
171607
+ if (metaRaw !== null) {
171608
+ let icons = {};
171609
+ try {
171610
+ icons = JSON.parse(metaRaw).icons ?? {};
171611
+ } catch {}
171612
+ const similar = suggestIconNames(name, icons);
171613
+ const hint = similar.length ? ` Did you mean ${similar.map((s) => `"${pack}:${s}"`).join(", ")}?` : ` No similar name in the "${pack}" pack either.`;
171614
+ return new Error(`assetIcon: icon "${pack}:${name}" not found.${hint}`);
171615
+ }
171616
+ const licRaw = await read("licenses.json");
171617
+ if (licRaw !== null) {
171618
+ try {
171619
+ const packs = Object.keys(JSON.parse(licRaw)).sort();
171620
+ return new Error(`assetIcon: unknown icon pack "${pack}" (in "${pack}:${name}"). Available packs: ${packs.join(", ")}.`);
171621
+ } catch {}
171622
+ }
171623
+ 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 });
171624
+ }, VENDORED_ICON_PATH, collectVendoredIcons = (entries) => {
171625
+ const map = new Map;
171626
+ for (const e of entries) {
171627
+ if (e.type !== "text" || typeof e.text !== "string")
171628
+ continue;
171629
+ const m = VENDORED_ICON_PATH.exec(e.path);
171630
+ if (m)
171631
+ map.set(`${m[1]}:${m[2]}`, e.text);
171632
+ }
171633
+ return map;
171634
+ };
171635
+ var init_assetIconMacro = __esm(() => {
171636
+ ICON_ID = /^([a-z0-9]+(?:-[a-z0-9]+)*):([a-z0-9]+(?:-[a-z0-9]+)*)$/;
171637
+ VENDORED_ICON_PATH = /(?:^|\/)assets\/icons\/([a-z0-9]+(?:-[a-z0-9]+)*)\/([a-z0-9]+(?:-[a-z0-9]+)*)\.svg$/;
171638
+ });
171639
+
171640
+ // ../../sdk/src/compile/compileProject.ts
171641
+ var localAssetBase = (path) => path.split(/[\\/]/).pop() ?? path, localAssetNames = (entries) => {
171642
+ const seen = new Map;
171643
+ for (const e of entries)
171644
+ if (e.type === "resource") {
171645
+ const b = localAssetBase(e.path);
171646
+ seen.set(b, (seen.get(b) ?? 0) + 1);
171647
+ }
171648
+ const names = new Map;
171649
+ for (const e of entries)
171650
+ if (e.type === "resource") {
171651
+ const b = localAssetBase(e.path);
171652
+ names.set(e.path, (seen.get(b) ?? 0) > 1 ? e.path.replace(/^[./]+/, "").replace(/[\\/]/g, "__") : b);
171653
+ }
171654
+ return names;
171655
+ }, entriesToFiles = (entries, opts) => {
171656
+ const files = {};
171657
+ const localNames = localAssetNames(entries);
171658
+ const localAssetId = (nameExpr) => `_creatorFetch.local(${nameExpr})`;
171659
+ for (const e of entries) {
171660
+ if (e.type === "resource") {
171661
+ if (opts.localAssets && !e.remote) {
171662
+ const name = localNames.get(e.path) ?? localAssetBase(e.path);
171663
+ files[e.path] = `export default ("id:" + ${localAssetId(JSON.stringify(name))})`;
171664
+ } else {
171665
+ files[e.path] = `export default "${opts.publicUrl}${e.fileSrc ?? ""}"`;
171666
+ }
171667
+ } else if (e.type === "shader") {
171668
+ const shaders = e.shaders ?? [];
171669
+ if (e.shadersLocal && shaders.length > 0) {
171670
+ const name = shaders[0].src.split(/[\/]/).pop() ?? "";
171671
+ const [prefix, suffix = ".filamat"] = name.split("_opengl");
171672
+ files[e.path] = `export default ("id:" + ${localAssetId(`${JSON.stringify(prefix + "_")} + _creator.backend + ${JSON.stringify(suffix)}`)})`;
171673
+ } else if (shaders.length > 0) {
171674
+ files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.backend}")}\``;
171675
+ } else {
171676
+ files[e.path] = `export default ""`;
171677
+ }
171678
+ } else if (e.path.toLowerCase().endsWith(".svg")) {
171679
+ files[e.path] = `export default SvgSource(\`${escapeTemplate(e.text ?? "")}\`)`;
171680
+ } else if (e.path.toLowerCase().endsWith(".json")) {
171681
+ files[e.path] = `export default ${e.text?.trim() || "null"}`;
171682
+ } else if (!CODE_EXT.test(e.path)) {
171683
+ files[e.path] = `export default ${JSON.stringify(e.text ?? "")}`;
171684
+ } else {
171685
+ files[e.path] = e.text ?? "";
171686
+ }
171687
+ }
171688
+ return files;
171689
+ }, pluginSpecsOf = (plugins) => {
171690
+ if (Array.isArray(plugins))
171691
+ return Object.fromEntries(plugins.filter((p) => typeof p === "string").map((id) => [id, "*"]));
171692
+ if (plugins && typeof plugins === "object") {
171693
+ return Object.fromEntries(Object.entries(plugins).map(([id, v]) => [id, typeof v === "string" ? v : "*"]));
171694
+ }
171695
+ return {};
171696
+ }, pluginIdsOf = (plugins) => Object.keys(pluginSpecsOf(plugins)), pluginsOfAppJson = (entries) => {
171697
+ const app = entries.find((e) => e.path.replace(/^[./\\]+/, "") === "app.json");
171698
+ if (!app?.text)
171699
+ return [];
171700
+ try {
171701
+ return pluginIdsOf(JSON.parse(app.text).plugins);
171702
+ } catch {
171703
+ return [];
171704
+ }
171705
+ }, pluginsOfBundle = (mapJson) => {
171706
+ const ids = new Set;
171707
+ try {
171708
+ for (const source of JSON.parse(mapJson).sources ?? []) {
171709
+ const m = /\/plugins\/gen\/([^/]+)\/sdk\//.exec(source);
171710
+ if (m)
171711
+ ids.add(m[1]);
171712
+ }
171713
+ } catch {}
171714
+ return [...ids].sort();
171715
+ }, compileProjectWithMap = async (opts) => {
171716
+ const published = clientPublishes(opts.entries);
171717
+ if (published.length)
171718
+ throw new ClientPublishError(published);
171719
+ const entries = stubServerEntries(opts.entries, opts.serverUrl ?? "");
171720
+ const entry = opts.entryOverride ?? detectEntry(entries, scanModulesChisel).entry;
171721
+ if (!entry) {
171722
+ throw new Error("Couldn't find an entrypoint (no .ts/.js project file — name yours main.ts). Pass entryOverride.");
171723
+ }
171724
+ const files = entriesToFiles(entries, opts);
171725
+ const { code, map } = await bundleProjectWithMap(entry, files, opts.format ?? "esm", opts.minify, opts.editor, opts.libraries, opts.sourcemap ?? true);
171726
+ const plugins = opts.plugins ?? pluginsOfAppJson(entries);
171727
+ if (map && opts.onWarning) {
171728
+ const missing = pluginsOfBundle(map).filter((id) => !plugins.includes(id));
171729
+ if (missing.length) {
171730
+ 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(", ")} }`);
171731
+ }
171732
+ }
171733
+ if (opts.header === false)
171734
+ return { code, map, entry };
171735
+ const header = buildHeader(opts.name, code, { icon: opts.icon, fonts: opts.fontsHeader, icons: opts.iconsHeader, plugins });
171736
+ const headerLines = (header.match(/\n/g) ?? []).length;
171737
+ return { code: header + code, map: map ? offsetSourceMap(map, headerLines) : null, entry };
171738
+ }, compileProject = async (opts) => {
171739
+ const { code, map } = await compileProjectWithMap(opts);
171740
+ return map ? `${code}
171741
+ ${inlineSourceMapComment(map)}` : code;
171742
+ };
171743
+ var init_compileProject = __esm(() => {
171744
+ init_bundler();
171745
+ init_detectEntry();
171746
+ init_header();
171747
+ init_serverSplit();
171748
+ init_assetMacro();
171749
+ init_assetIconMacro();
171750
+ });
171751
+
171752
+ // ../../sdk/src/compile/sceneEditor.ts
171753
+ var SCENE_HARNESS_DIR = "./.scene-editor", SCENE_ENTRY_PATH = "./.scene-entry.ts", resolveRelative = (fromPath, spec) => {
171754
+ const segs = (p) => p.split("/").filter((s) => s !== "" && s !== ".");
171755
+ const out = segs(fromPath).slice(0, -1);
171756
+ for (const seg of segs(spec)) {
171757
+ if (seg === "..")
171758
+ out.pop();
171759
+ else
171760
+ out.push(seg);
171761
+ }
171762
+ return "./" + out.join("/");
171763
+ }, sceneImportsOf = (sceneText, scenePath, projectPaths) => {
171764
+ const known = new Set(projectPaths);
171765
+ const out = [];
171766
+ const re = /^\s*import\s+[A-Za-z_$][\w$]*\s+from\s+["']([^"']+\.scene(?:\.ts)?)["']/gm;
171767
+ for (const m of sceneText.matchAll(re)) {
171768
+ const spec = m[1];
171769
+ if (!spec.startsWith("."))
171770
+ continue;
171771
+ let p = resolveRelative(scenePath, spec);
171772
+ if (!p.endsWith(".ts"))
171773
+ p += ".ts";
171774
+ if (known.has(p) && !out.includes(p))
171775
+ out.push(p);
171776
+ }
171777
+ return out;
171778
+ }, sceneEditorEntries = (scenePath, harness, projectPaths = [], sceneImports = []) => {
171779
+ const entries = Object.entries(harness).map(([name, text]) => ({
171780
+ path: `${SCENE_HARNESS_DIR}/${name}`,
171781
+ type: "text",
171782
+ text
171783
+ }));
171784
+ entries.push({
171785
+ path: `${SCENE_HARNESS_DIR}/.edit-mode.ts`,
171786
+ type: "text",
171787
+ text: `;(globalThis as any).__lecodesSceneEdit = true
171788
+ export {}
171789
+ `
171790
+ });
171791
+ const editorImports = projectPaths.filter((p) => p.endsWith(".editor.ts")).map((p) => `import "${p}"
171792
+ `).join("");
171793
+ const materialPaths = projectPaths.filter((p) => p.endsWith(".material.ts"));
171794
+ const materialImports = materialPaths.map((p, i) => `import __mat${i} from "${p}"
171795
+ `).join("");
171796
+ const materialRegister = materialPaths.length === 0 ? "" : `;(globalThis as any).__lecodesMaterials = { ${materialPaths.map((p, i) => `${JSON.stringify(p)}: __mat${i}`).join(", ")} }
171797
+ `;
171798
+ const sceneImportLines = sceneImports.map((p, i) => `import __scn${i} from "${p}"
171799
+ `).join("");
171800
+ const sceneRegister = sceneImports.length === 0 ? "" : `;(globalThis as any).__lecodesSceneFiles = { ${sceneImports.map((p, i) => `${JSON.stringify(p)}: __scn${i}`).join(", ")} }
171801
+ `;
171802
+ entries.push({
171803
+ path: SCENE_ENTRY_PATH,
171804
+ type: "text",
171805
+ text: `import "${SCENE_HARNESS_DIR}/.edit-mode.ts"
171806
+ ` + materialImports + sceneImportLines + `import "${scenePath}"
171807
+ ` + editorImports + `import "${SCENE_HARNESS_DIR}/main.ts"
171808
+ ` + materialRegister + sceneRegister
171809
+ });
171810
+ return entries;
171811
+ };
171812
+
171813
+ // ../../sdk/src/compile/fontRegistry.ts
171814
+ 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;
171815
+ var init_fontRegistry = __esm(() => {
171816
+ LATIN = ["latin"];
171817
+ CYR = ["latin", "cyrillic"];
171818
+ FONT_REGISTRY = {
171819
+ inter: f("Inter", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700, 800, 900], [], [400, 500, 700]),
171820
+ manrope: f("Manrope", "sans-serif", CYR, [200, 300, 400, 500, 600, 700, 800], [], [400, 500, 700]),
171821
+ "golos-text": f("Golos Text", "sans-serif", CYR, [400, 500, 600, 700, 800, 900], [], [400, 500, 700]),
171822
+ onest: f("Onest", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700, 800, 900], [], [400, 500, 700]),
171823
+ rubik: f("Rubik", "sans-serif", CYR, [300, 400, 500, 600, 700, 800, 900], [300, 400, 500, 600, 700, 800, 900], [400, 500, 700]),
171824
+ nunito: f("Nunito", "sans-serif", CYR, [200, 300, 400, 500, 600, 700, 800, 900], [400, 700], [400, 600, 700]),
171825
+ montserrat: f("Montserrat", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700, 800, 900], [400, 700], [400, 500, 700]),
171826
+ "ibm-plex-sans": f("IBM Plex Sans", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700], [400, 700], [400, 500, 700]),
171827
+ "pt-sans": f("PT Sans", "sans-serif", CYR, [400, 700], [400, 700], [400, 700]),
171828
+ jost: f("Jost", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700, 800, 900], [400, 700], [400, 500, 700]),
171829
+ "exo-2": f("Exo 2", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700, 800, 900], [400, 700], [400, 500, 700]),
171830
+ unbounded: f("Unbounded", "display", CYR, [200, 300, 400, 500, 600, 700, 800, 900], [], [400, 700]),
171831
+ "russo-one": f("Russo One", "display", CYR, [400], [], [400]),
171832
+ oswald: f("Oswald", "display", CYR, [200, 300, 400, 500, 600, 700], [], [400, 700]),
171833
+ comfortaa: f("Comfortaa", "display", CYR, [300, 400, 500, 600, 700], [], [400, 700]),
171834
+ "space-grotesk": f("Space Grotesk", "display", LATIN, [300, 400, 500, 600, 700], [], [400, 700]),
171835
+ "playfair-display": f("Playfair Display", "serif", CYR, [400, 500, 600, 700, 800, 900], [400, 700], [400, 700]),
171836
+ lora: f("Lora", "serif", CYR, [400, 500, 600, 700], [400, 700], [400, 700]),
171837
+ "pt-serif": f("PT Serif", "serif", CYR, [400, 700], [400, 700], [400, 700]),
171838
+ cormorant: f("Cormorant", "serif", CYR, [300, 400, 500, 600, 700], [400, 700], [400, 700]),
171839
+ literata: f("Literata", "serif", CYR, [200, 300, 400, 500, 600, 700, 800, 900], [400, 700], [400, 700]),
171840
+ "jetbrains-mono": f("JetBrains Mono", "monospace", CYR, [100, 200, 300, 400, 500, 600, 700, 800], [400, 700], [400, 700]),
171841
+ "fira-code": f("Fira Code", "monospace", CYR, [300, 400, 500, 600, 700], [], [400, 700]),
171842
+ "ibm-plex-mono": f("IBM Plex Mono", "monospace", CYR, [100, 200, 300, 400, 500, 600, 700], [400, 700], [400, 700]),
171843
+ caveat: f("Caveat", "handwriting", CYR, [400, 500, 600, 700], [], [400, 700]),
171844
+ pacifico: f("Pacifico", "handwriting", CYR, [400], [], [400])
171845
+ };
171846
+ });
171847
+
171848
+ // ../../sdk/src/compile/sfnt.ts
171849
+ var TAG_TTF = 65536, TAG_OTTO = 1330926671, TAG_TTCF = 1953784678, TAG_WOFF = 2001684038, TAG_WOF2 = 2001684018, parseSfnt = (bytes) => {
171850
+ const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
171851
+ if (bytes.byteLength < 12)
171852
+ throw new Error("not a font file (truncated)");
171853
+ const version = dv.getUint32(0);
171854
+ if (version === TAG_TTCF)
171855
+ throw new Error("font collections (.ttc) aren't supported — export a single face");
171856
+ if (version === TAG_WOFF || version === TAG_WOF2)
171857
+ throw new Error("WOFF/WOFF2 project fonts aren't supported — use the .ttf/.otf");
171858
+ if (version !== TAG_TTF && version !== TAG_OTTO)
171859
+ throw new Error("not a TTF/OTF font file");
171860
+ const numTables = dv.getUint16(4);
171861
+ const tables = new Map;
171862
+ for (let i = 0;i < numTables; i++) {
171863
+ const rec = 12 + i * 16;
171864
+ if (rec + 16 > bytes.byteLength)
171865
+ throw new Error("not a font file (truncated table directory)");
171866
+ const tag = String.fromCharCode(bytes[rec], bytes[rec + 1], bytes[rec + 2], bytes[rec + 3]);
171867
+ tables.set(tag, { offset: dv.getUint32(rec + 8), length: dv.getUint32(rec + 12) });
171868
+ }
171869
+ const family = readFamily(dv, bytes, tables.get("name"));
171870
+ if (!family)
171871
+ throw new Error("no family name in the font's `name` table");
171872
+ let weight = 400;
171873
+ let italic = false;
171874
+ const os2 = tables.get("OS/2");
171875
+ if (os2 && os2.offset + 64 <= bytes.byteLength) {
171876
+ weight = dv.getUint16(os2.offset + 4);
171877
+ italic = (dv.getUint16(os2.offset + 62) & 1) === 1;
171878
+ } else {
171879
+ const head = tables.get("head");
171880
+ if (head && head.offset + 46 <= bytes.byteLength) {
171881
+ const macStyle = dv.getUint16(head.offset + 44);
171882
+ if (macStyle & 1)
171883
+ weight = 700;
171884
+ italic = (macStyle & 2) === 2;
171885
+ }
171886
+ }
171887
+ if (weight < 1 || weight > 1000)
171888
+ weight = 400;
171889
+ return { family, weight, italic };
171890
+ }, readFamily = (dv, bytes, name) => {
171891
+ if (!name || name.offset + 6 > bytes.byteLength)
171892
+ return "";
171893
+ const base = name.offset;
171894
+ const count = dv.getUint16(base + 2);
171895
+ const stringOffset = base + dv.getUint16(base + 4);
171896
+ let best = "";
171897
+ let bestScore = -1;
171898
+ for (let i = 0;i < count; i++) {
171899
+ const rec = base + 6 + i * 12;
171900
+ if (rec + 12 > bytes.byteLength)
171901
+ break;
171902
+ const platformID = dv.getUint16(rec);
171903
+ const languageID = dv.getUint16(rec + 4);
171904
+ const nameID = dv.getUint16(rec + 6);
171905
+ const length = dv.getUint16(rec + 8);
171906
+ const offset = stringOffset + dv.getUint16(rec + 10);
171907
+ if (nameID !== 16 && nameID !== 1)
171908
+ continue;
171909
+ if (offset + length > bytes.byteLength)
171910
+ continue;
171911
+ let score = nameID === 16 ? 100 : 0;
171912
+ if (platformID === 3)
171913
+ score += languageID === 1033 ? 30 : 20;
171914
+ else if (platformID === 0)
171915
+ score += 10;
171916
+ if (score <= bestScore)
171917
+ continue;
171918
+ const text = platformID === 1 ? decodeLatin1(bytes, offset, length) : decodeUtf16be(bytes, offset, length);
171919
+ if (!text.trim())
171920
+ continue;
171921
+ best = text.trim();
171922
+ bestScore = score;
171923
+ }
171924
+ return best;
171925
+ }, decodeUtf16be = (bytes, offset, length) => {
171926
+ let out = "";
171927
+ for (let i = 0;i + 1 < length; i += 2)
171928
+ out += String.fromCharCode(bytes[offset + i] << 8 | bytes[offset + i + 1]);
171929
+ return out;
171930
+ }, decodeLatin1 = (bytes, offset, length) => {
171931
+ let out = "";
171932
+ for (let i = 0;i < length; i++)
171933
+ out += String.fromCharCode(bytes[offset + i]);
171934
+ return out;
171935
+ };
171936
+
171937
+ // ../../sdk/src/compile/fontMacro.ts
171938
+ 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) => {
171939
+ const quote = src[start];
171940
+ const n = src.length;
171941
+ let i = start + 1;
171942
+ while (i < n) {
171943
+ const c = src[i];
171944
+ if (c === "\\") {
171945
+ i += 2;
171946
+ continue;
171947
+ }
171948
+ if (quote === "`" && c === "$" && src[i + 1] === "{") {
171949
+ i = skipBraces2(src, i + 1);
171950
+ continue;
171951
+ }
171952
+ if (c === quote)
171953
+ return i + 1;
171954
+ i++;
171955
+ }
171956
+ return n;
171957
+ }, skipBraces2 = (src, open) => {
171958
+ const n = src.length;
171959
+ let depth = 0;
171960
+ let i = open;
171961
+ while (i < n) {
171962
+ const c = src[i];
171963
+ if (c === '"' || c === "'" || c === "`") {
171964
+ i = skipString3(src, i);
171965
+ continue;
171966
+ }
171967
+ if (c === "{")
171968
+ depth++;
171969
+ else if (c === "}") {
171970
+ depth--;
171971
+ if (depth === 0)
171972
+ return i + 1;
171973
+ }
171974
+ i++;
171975
+ }
171976
+ return n;
171977
+ }, matchParen2 = (src, open) => {
171978
+ const n = src.length;
171979
+ let depth = 0;
171980
+ let i = open;
171981
+ while (i < n) {
171982
+ const c = src[i];
171983
+ if (c === '"' || c === "'" || c === "`") {
171984
+ i = skipString3(src, i);
171985
+ continue;
171986
+ }
171987
+ if (c === "/" && src[i + 1] === "/") {
171988
+ i += 2;
171989
+ while (i < n && src[i] !== `
171990
+ `)
171991
+ i++;
171992
+ continue;
171993
+ }
171994
+ if (c === "/" && src[i + 1] === "*") {
171995
+ i += 2;
171996
+ while (i < n && !(src[i] === "*" && src[i + 1] === "/"))
171997
+ i++;
171998
+ i += 2;
171999
+ continue;
172000
+ }
172001
+ if (c === "(")
172002
+ depth++;
172003
+ else if (c === ")") {
172004
+ depth--;
172005
+ if (depth === 0)
172006
+ return i;
172007
+ }
172008
+ i++;
172009
+ }
172010
+ return -1;
172011
+ }, splitTopLevel2 = (text, delim) => {
172012
+ const parts = [];
172013
+ const n = text.length;
172014
+ let depth = 0;
172015
+ let last = 0;
172016
+ let i = 0;
172017
+ while (i < n) {
172018
+ const c = text[i];
172019
+ if (c === '"' || c === "'" || c === "`") {
172020
+ i = skipString3(text, i);
172021
+ continue;
172022
+ }
172023
+ if (c === "(" || c === "[" || c === "{")
172024
+ depth++;
172025
+ else if (c === ")" || c === "]" || c === "}")
172026
+ depth = Math.max(0, depth - 1);
172027
+ else if (depth === 0 && c === delim) {
172028
+ parts.push(text.slice(last, i));
172029
+ last = i + 1;
172030
+ }
172031
+ i++;
172032
+ }
172033
+ parts.push(text.slice(last));
172034
+ return parts;
172035
+ }, stringLiteralValue2 = (text) => {
172036
+ const t = text.trim();
172037
+ if (t.length < 2)
172038
+ return null;
172039
+ const q = t[0];
172040
+ if (q !== '"' && q !== "'" && q !== "`" || t[t.length - 1] !== q)
172041
+ return null;
172042
+ const inner = t.slice(1, -1);
172043
+ if (q === "`" && inner.includes("${"))
172044
+ return null;
172045
+ if (inner.includes(q) && !inner.includes("\\" + q))
172046
+ return null;
172047
+ return inner.replace(/\\(.)/g, "$1");
172048
+ }, prevNonSpace2 = (src, i) => {
172049
+ let k = i - 1;
172050
+ while (k >= 0 && /\s/.test(src[k]))
172051
+ k--;
172052
+ return k >= 0 ? src[k] : "";
172053
+ }, scanFontCalls = (source) => {
172054
+ const calls = [];
172055
+ const n = source.length;
172056
+ let i = 0;
172057
+ while (i < n) {
172058
+ const c = source[i];
172059
+ if (c === '"' || c === "'" || c === "`") {
172060
+ i = skipString3(source, i);
172061
+ continue;
172062
+ }
172063
+ if (c === "/" && source[i + 1] === "/") {
172064
+ i += 2;
172065
+ while (i < n && source[i] !== `
172066
+ `)
172067
+ i++;
172068
+ continue;
172069
+ }
172070
+ if (c === "/" && source[i + 1] === "*") {
172071
+ i += 2;
172072
+ while (i < n && !(source[i] === "*" && source[i + 1] === "/"))
172073
+ i++;
172074
+ i += 2;
172075
+ continue;
172076
+ }
172077
+ if (isIdentStart2(c)) {
172078
+ let j = i + 1;
172079
+ while (j < n && isIdentPart2(source[j]))
172080
+ j++;
172081
+ if (source.slice(i, j) === "font" && prevNonSpace2(source, i) !== ".") {
172082
+ const call = parseCall2(source, i, j);
172083
+ if (call) {
172084
+ calls.push(call);
172085
+ i = call.end;
172086
+ continue;
172087
+ }
172088
+ }
172089
+ i = j;
172090
+ continue;
172091
+ }
172092
+ i++;
172093
+ }
172094
+ return calls;
172095
+ }, parseCall2 = (src, start, identEnd) => {
172096
+ const n = src.length;
172097
+ let i = identEnd;
172098
+ while (i < n && /\s/.test(src[i]))
172099
+ i++;
172100
+ if (src[i] !== "(")
172101
+ return null;
172102
+ const close = matchParen2(src, i);
172103
+ if (close === -1)
172104
+ return null;
172105
+ const call = { start, end: close + 1, arg: "", kind: "registry" };
172106
+ const args = splitTopLevel2(src.slice(i + 1, close), ",");
172107
+ const arg = stringLiteralValue2(args[0] ?? "");
171689
172108
  if (arg === null) {
171690
172109
  call.error = `font(...) needs a string-literal argument — a registry id ("manrope") or a project path ("./fonts/Brand.ttf"). Got \`${(args[0] ?? "").trim() || "()"}\`.`;
171691
172110
  return call;
@@ -171726,10 +172145,10 @@ var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c) => c >= "A" && c <= "Z
171726
172145
  if (!optsText.startsWith("{") || !optsText.endsWith("}")) {
171727
172146
  return `font("${call.arg}", opts): opts must be an object literal like { weights: [400, 700] }.`;
171728
172147
  }
171729
- for (const prop of splitTopLevel(optsText.slice(1, -1), ",")) {
172148
+ for (const prop of splitTopLevel2(optsText.slice(1, -1), ",")) {
171730
172149
  if (!prop.trim())
171731
172150
  continue;
171732
- const kv = splitTopLevel(prop, ":");
172151
+ const kv = splitTopLevel2(prop, ":");
171733
172152
  if (kv.length < 2)
171734
172153
  return `font("${call.arg}"): couldn't parse option \`${prop.trim()}\`.`;
171735
172154
  const key = kv[0].trim().replace(/^["'`]|["'`]$/g, "");
@@ -171924,321 +172343,6 @@ var init_fontMacro = __esm(() => {
171924
172343
  DECLARES_FONT = /\b(?:function|const|let|var|class)\s+font\b|\bimport\s+(?:\{[^}]*\bfont\b[^}]*\}|font)\s+from\b/;
171925
172344
  });
171926
172345
 
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
172346
  // ../../sdk/src/compile/index.ts
172243
172347
  var init_compile = __esm(() => {
172244
172348
  init_bundler();
@@ -207654,9 +207758,9 @@ var init_fields = __esm(() => {
207654
207758
  });
207655
207759
 
207656
207760
  // ../../sdk/src/server/errors.ts
207657
- var ApiError;
207761
+ var ApiError2;
207658
207762
  var init_errors3 = __esm(() => {
207659
- ApiError = class ApiError extends Error {
207763
+ ApiError2 = class ApiError2 extends Error {
207660
207764
  status;
207661
207765
  constructor(status, message = "") {
207662
207766
  super(message);
@@ -208111,21 +208215,21 @@ var CODE_TTL_MS, CODE_MAX_ATTEMPTS = 5, SEND_MIN_GAP_MS = 30000, SEND_MAX_PER_10
208111
208215
  return actual.length === expected.length && timingSafeEqual(actual, expected);
208112
208216
  }, checkPassword = (password) => {
208113
208217
  if (typeof password !== "string" || password.length < PASSWORD_MIN)
208114
- throw new ApiError(400, `Password must be at least ${PASSWORD_MIN} characters`);
208218
+ throw new ApiError2(400, `Password must be at least ${PASSWORD_MIN} characters`);
208115
208219
  if (password.length > PASSWORD_MAX)
208116
- throw new ApiError(400, `Password must be at most ${PASSWORD_MAX} characters`);
208220
+ throw new ApiError2(400, `Password must be at most ${PASSWORD_MAX} characters`);
208117
208221
  return password;
208118
208222
  }, EMAIL_RE, normalizeEmail = (email) => {
208119
208223
  const e = typeof email === "string" ? email.trim().toLowerCase() : "";
208120
208224
  if (!e || e.length > 254 || !EMAIL_RE.test(e))
208121
- throw new ApiError(400, "Invalid email");
208225
+ throw new ApiError2(400, "Invalid email");
208122
208226
  return e;
208123
208227
  }, LOGIN_RE, parseLogin = (login) => {
208124
208228
  const l = typeof login === "string" ? login.trim() : "";
208125
208229
  if (l.includes("@"))
208126
208230
  return { provider: "email", subject: normalizeEmail(l) };
208127
208231
  if (!LOGIN_RE.test(l))
208128
- throw new ApiError(400, "Login must be 3–32 letters, digits, dots, dashes or underscores");
208232
+ throw new ApiError2(400, "Login must be 3–32 letters, digits, dots, dashes or underscores");
208129
208233
  return { provider: "login", subject: l.toLowerCase() };
208130
208234
  }, identityKey = (provider, subject) => `${provider}:${subject}`, window2 = (map, key, now, span) => {
208131
208235
  const list = (map.get(key) ?? []).filter((x) => now - x < span);
@@ -208153,7 +208257,7 @@ var CODE_TTL_MS, CODE_MAX_ATTEMPTS = 5, SEND_MIN_GAP_MS = 30000, SEND_MAX_PER_10
208153
208257
  const requireSignedIn = (ctx) => {
208154
208258
  const id = stateOf(ctx).userId;
208155
208259
  if (id === null)
208156
- throw new ApiError(401, "Sign in required");
208260
+ throw new ApiError2(401, "Sign in required");
208157
208261
  return id;
208158
208262
  };
208159
208263
  const mint = async (ctx) => {
@@ -208210,7 +208314,7 @@ var CODE_TTL_MS, CODE_MAX_ATTEMPTS = 5, SEND_MIN_GAP_MS = 30000, SEND_MAX_PER_10
208210
208314
  const ident = await findIdentity("email", email);
208211
208315
  if (ident) {
208212
208316
  if (ident.user.id !== userId)
208213
- throw new ApiError(409, "This email belongs to another account");
208317
+ throw new ApiError2(409, "This email belongs to another account");
208214
208318
  if (!ident.verifiedAt)
208215
208319
  await identities.update(ident.id, { verifiedAt: now(), hash: null });
208216
208320
  } else if (create) {
@@ -208223,7 +208327,7 @@ var CODE_TTL_MS, CODE_MAX_ATTEMPTS = 5, SEND_MIN_GAP_MS = 30000, SEND_MAX_PER_10
208223
208327
  const { provider, subject } = parseLogin(rawLogin);
208224
208328
  const password = checkPassword(rawPassword);
208225
208329
  if (await findIdentity(provider, subject))
208226
- throw new ApiError(409, provider === "email" ? "An account with this email already exists" : "This login is taken");
208330
+ throw new ApiError2(409, provider === "email" ? "An account with this email already exists" : "This login is taken");
208227
208331
  const userId = await createUser(data);
208228
208332
  await createIdentity(userId, provider, subject, { hash: await hashPassword(password) });
208229
208333
  await bind(ctx, userId, { rotate: true });
@@ -208238,13 +208342,13 @@ var CODE_TTL_MS, CODE_MAX_ATTEMPTS = 5, SEND_MIN_GAP_MS = 30000, SEND_MAX_PER_10
208238
208342
  const byLogin = window2(passwordFails, `l:${who ? identityKey(who.provider, who.subject) : ""}`, t, 10 * 60000);
208239
208343
  const byIp = ctx.ip ? window2(passwordFails, `ip:${ctx.ip}`, t, 10 * 60000) : [];
208240
208344
  if (byLogin.length >= PASSWORD_FAILS_PER_LOGIN || byIp.length >= PASSWORD_FAILS_PER_IP)
208241
- throw new ApiError(429, "Too many attempts — try again later");
208345
+ throw new ApiError2(429, "Too many attempts — try again later");
208242
208346
  const ident = who ? await findIdentity(who.provider, who.subject) : null;
208243
208347
  const ok = !!ident?.hash && typeof rawPassword === "string" && await verifyPassword(rawPassword, ident.hash);
208244
208348
  if (!ok) {
208245
208349
  byLogin.push(t);
208246
208350
  byIp.push(t);
208247
- throw new ApiError(401, "Invalid login or password");
208351
+ throw new ApiError2(401, "Invalid login or password");
208248
208352
  }
208249
208353
  await bind(ctx, ident.user.id, { rotate: true });
208250
208354
  };
@@ -208255,7 +208359,7 @@ var CODE_TTL_MS, CODE_MAX_ATTEMPTS = 5, SEND_MIN_GAP_MS = 30000, SEND_MAX_PER_10
208255
208359
  const mine = await identities.where({ user: { id: userId } }).select({ id: true, provider: true });
208256
208360
  const typed = mine.filter((i) => i.provider === "email" || i.provider === "login");
208257
208361
  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");
208362
+ throw new ApiError2(400, "Your account has no email or login to sign in with a password — sign in with a code first");
208259
208363
  const hash = await hashPassword(password);
208260
208364
  for (const i of typed)
208261
208365
  await identities.update(i.id, { hash });
@@ -208272,9 +208376,9 @@ var CODE_TTL_MS, CODE_MAX_ATTEMPTS = 5, SEND_MIN_GAP_MS = 30000, SEND_MAX_PER_10
208272
208376
  const t = now();
208273
208377
  const recent = window2(sends, email, t, 10 * 60000);
208274
208378
  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`);
208379
+ throw new ApiError2(429, `Try again in ${Math.ceil((SEND_MIN_GAP_MS - (t - recent[recent.length - 1])) / 1000)} s`);
208276
208380
  if (recent.length >= SEND_MAX_PER_10MIN)
208277
- throw new ApiError(429, "Too many codes requested — try again later");
208381
+ throw new ApiError2(429, "Too many codes requested — try again later");
208278
208382
  const code = randomCode();
208279
208383
  await sessions.update(s.sessionId, { pendingEmail: email, codeHash: await codeHash(s.sessionId, code), codeExpires: t + CODE_TTL_MS, codeAttempts: 0 });
208280
208384
  recent.push(t);
@@ -208303,12 +208407,12 @@ var CODE_TTL_MS, CODE_MAX_ATTEMPTS = 5, SEND_MIN_GAP_MS = 30000, SEND_MAX_PER_10
208303
208407
  if (!isDevCode) {
208304
208408
  const row = await sessions.where({ id: s.sessionId }).select({ pendingEmail: true, codeHash: true, codeExpires: true, codeAttempts: true }).first();
208305
208409
  if (!row?.codeHash || row.pendingEmail !== email || !row.codeExpires || row.codeExpires < now())
208306
- throw new ApiError(400, "Code expired — request a new one");
208410
+ throw new ApiError2(400, "Code expired — request a new one");
208307
208411
  if (row.codeAttempts >= CODE_MAX_ATTEMPTS)
208308
- throw new ApiError(429, "Too many attempts — request a new code");
208412
+ throw new ApiError2(429, "Too many attempts — request a new code");
208309
208413
  if (typeof code !== "string" || await codeHash(s.sessionId, code.trim()) !== row.codeHash) {
208310
208414
  await sessions.update(s.sessionId, { codeAttempts: { $increment: 1 } });
208311
- throw new ApiError(400, "Invalid code");
208415
+ throw new ApiError2(400, "Invalid code");
208312
208416
  }
208313
208417
  }
208314
208418
  const userId = await userOfProvenEmail(email);
@@ -208343,7 +208447,7 @@ var CODE_TTL_MS, CODE_MAX_ATTEMPTS = 5, SEND_MIN_GAP_MS = 30000, SEND_MAX_PER_10
208343
208447
  signIn: async (ctx, userId) => {
208344
208448
  requireAuth();
208345
208449
  if (!await users.where({ id: userId }).select({ id: true }).first())
208346
- throw new ApiError(404, "No such user");
208450
+ throw new ApiError2(404, "No such user");
208347
208451
  await bind(ctx, userId, { rotate: true });
208348
208452
  },
208349
208453
  signOut: async (ctx) => {
@@ -209891,7 +209995,7 @@ var require_main = __commonJS(function(exports, module) {
209891
209995
  });
209892
209996
 
209893
209997
  // src/dev/backendWorker.ts
209894
- import { existsSync as existsSync54, mkdirSync as mkdirSync35, mkdtempSync as mkdtempSync8, renameSync as renameSync3, rmSync as rmSync18 } from "node:fs";
209998
+ import { existsSync as existsSync55, mkdirSync as mkdirSync35, mkdtempSync as mkdtempSync8, renameSync as renameSync3, rmSync as rmSync18 } from "node:fs";
209895
209999
  import { readFile as readFile3 } from "node:fs/promises";
209896
210000
  import { tmpdir as tmpdir8 } from "node:os";
209897
210001
  import { join as join58 } from "node:path";
@@ -209935,7 +210039,7 @@ var post = (m) => {
209935
210039
  rmSync18(aside, { recursive: true, force: true });
209936
210040
  renameSync3(dir, aside);
209937
210041
  rmSync18(`${filesDir}-old`, { recursive: true, force: true });
209938
- if (existsSync54(filesDir))
210042
+ if (existsSync55(filesDir))
209939
210043
  renameSync3(filesDir, `${filesDir}-old`);
209940
210044
  mkdirSync35(dir, { recursive: true });
209941
210045
  db = openDatabase(dir);
@@ -210028,7 +210132,7 @@ var init_backendWorker = __esm(() => {
210028
210132
  // src/index.ts
210029
210133
  init_package();
210030
210134
  init_cli();
210031
- import { existsSync as existsSync55, realpathSync as realpathSync2 } from "node:fs";
210135
+ import { existsSync as existsSync56, realpathSync as realpathSync2 } from "node:fs";
210032
210136
  import { delimiter as delimiter3, dirname as dirname28, join as join59 } from "node:path";
210033
210137
 
210034
210138
  // src/commands/app/index.ts
@@ -210635,6 +210739,9 @@ var readManifest2 = (dir, id) => {
210635
210739
  };
210636
210740
  var readManifestIfAny = (dir, id) => existsSync14(join17(dir, MANIFEST_FILE)) ? readManifest2(dir, id) : null;
210637
210741
 
210742
+ // src/commands/app/shared.ts
210743
+ init_compile();
210744
+
210638
210745
  // src/plugin/emitPackage.ts
210639
210746
  class PackageError extends Error {
210640
210747
  }
@@ -210731,7 +210838,12 @@ var urlSchemeOf = (config) => {
210731
210838
  throw new CliError(`app.json "urlScheme" is the app's OWN scheme ("myshop" → myshop://…), not ${scheme}.`);
210732
210839
  return scheme;
210733
210840
  };
210734
- var readAppConfigAt = (root) => readJson2(join18(root, "app.json"));
210841
+ var readAppConfigAt = (root) => {
210842
+ const config = readJson2(join18(root, "app.json"));
210843
+ if (config && "plugins" in config)
210844
+ config.plugins = pluginSpecsOf(config.plugins);
210845
+ return config;
210846
+ };
210735
210847
  var DESKTOP_DEFAULT_WINDOW = { width: 1280, height: 720 };
210736
210848
  var resolveDesktopRenderer = (root, renderer) => {
210737
210849
  const raw = renderer ?? process.env.LECODES_DESKTOP_RENDERER ?? readAppConfigAt(root)?.desktop?.renderer;
@@ -210851,25 +210963,33 @@ var detectEngines = (bundleJs) => {
210851
210963
  }
210852
210964
  const header = lines.join(`
210853
210965
  `);
210854
- return { gl: !/^\/\/ gl: disable$/m.test(header), g2: /^\/\/ 2d: enable$/m.test(header) };
210966
+ const on = (name) => new RegExp(`^// ${name}: enable$`, "m").test(header);
210967
+ return { gl: !/^\/\/ gl: disable$/m.test(header), g2: on("2d"), physics: on("physics"), net: on("net"), nav: on("nav"), audio: on("audio") };
210855
210968
  };
210969
+ var VARIANT_LACKS = {
210970
+ full: [],
210971
+ "3d": ["g2", "physics", "net", "nav", "audio"],
210972
+ "2d": ["gl", "physics", "net", "nav"],
210973
+ core: ["gl", "g2", "physics", "net", "nav"]
210974
+ };
210975
+ var ENGINE_WORDS = { gl: "3D", g2: "2D", physics: "physics", net: "multiplayer", nav: "navigation", audio: "game audio" };
210976
+ var lacks = (variant, uses) => VARIANT_LACKS[variant].filter((e) => uses[e]).map((e) => ENGINE_WORDS[e]);
210856
210977
  var resolveVariant = (requested, bundleJs, configKey = "ios.variant") => {
210857
210978
  const uses = detectEngines(bundleJs);
210858
- const detected = uses.gl && uses.g2 ? "full" : uses.gl ? "3d" : uses.g2 ? "2d" : "core";
210979
+ const detected = SDK_VARIANTS.find((v) => lacks(v, uses).length === 0) ?? "full";
210980
+ const inUse = Object.keys(ENGINE_WORDS).filter((e) => uses[e]).map((e) => ENGINE_WORDS[e]);
210981
+ const why = inUse.length > 0 ? `uses ${inUse.join(" + ")}` : "UI only, no engine use detected";
210859
210982
  if (requested === "auto")
210860
- return { variant: detected, detected };
210983
+ return { variant: detected, detected, why };
210861
210984
  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);
210985
+ const missing = lacks(variant, uses);
210866
210986
  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}".`);
210987
+ 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
210988
  }
210869
- return { variant, detected };
210989
+ return { variant, detected, why };
210870
210990
  };
210871
210991
  var ANDROID_SDK_VARIANTS = [...SDK_VARIANTS, "vulkan"];
210872
- var ANDROID_SDK_VULKAN_MIN = "2.1.0";
210992
+ var ANDROID_SDK_VULKAN_MIN = "2.2.11";
210873
210993
  var androidSdkArtifact = (variant, sdkVersion) => {
210874
210994
  if (variant === "vulkan" && sdkVersion && cmpVersion(sdkVersion, ANDROID_SDK_VULKAN_MIN) < 0) {
210875
210995
  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 +213112,7 @@ engines the bundle uses and cannot see those, so a project using \`Physics\`,
212992
213112
 
212993
213113
  \`android.variant: "vulkan"\` links \`lecodes-sdk-vulkan\`: the full engine set with Filament on
212994
213114
  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.
213115
+ auto-selected; needs lecodes-android-sdk >= 2.2.11.
212996
213116
 
212997
213117
  ## App-local native code
212998
213118
 
@@ -213757,7 +213877,7 @@ var initAndroid = async (opts) => {
213757
213877
  if (existsSync19(androidDir)) {
213758
213878
  throw new CliError("android/ already exists — run `lecodes app sync` to update it.");
213759
213879
  }
213760
- const existing = readJson2(join21(root, "app.json")) ?? {};
213880
+ const existing = readAppConfigAt(root) ?? {};
213761
213881
  const variant = opts.variant ?? existing.android?.variant ?? "auto";
213762
213882
  if (variant !== "auto" && !ANDROID_SDK_VARIANTS.includes(variant)) {
213763
213883
  throw new CliError(`--variant must be auto, ${ANDROID_SDK_VARIANTS.join(", ")} (got "${variant}")`);
@@ -213868,20 +213988,14 @@ var syncAndroid = async (root, config, prior) => {
213868
213988
  if (requestedVariant !== "auto" && !ANDROID_SDK_VARIANTS.includes(requestedVariant)) {
213869
213989
  throw new CliError(`app.json android.variant must be "auto" or one of ${ANDROID_SDK_VARIANTS.join(", ")} (got "${requestedVariant}").`);
213870
213990
  }
213871
- const { variant: covered, detected } = resolveVariant(requestedVariant === "auto" ? "auto" : androidVariantEngines(requestedVariant), bundleJs, "android.variant");
213991
+ const { variant: covered, why } = resolveVariant(requestedVariant === "auto" ? "auto" : androidVariantEngines(requestedVariant), bundleJs, "android.variant");
213872
213992
  const variant = requestedVariant === "vulkan" ? "vulkan" : covered;
213873
213993
  let sdkArtifact = androidSdkArtifact(variant, sdkVersion);
213874
213994
  if (sdkArtifact !== "lecodes-sdk" && cmpVersion(sdkVersion, ANDROID_SDK_VARIANTS_MIN) < 0) {
213875
213995
  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
213996
  sdkArtifact = "lecodes-sdk";
213877
213997
  }
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)")}`);
213998
+ log(`${c.green("variant")} ${variant} → ${sdkArtifact}${requestedVariant === "auto" ? c.dim(` (auto: ${why})`) : c.dim(" (forced by android.variant)")}`);
213885
213999
  const lockPath = join21(root, "plugins.lock.json");
213886
214000
  const oldLock = readJson2(lockPath)?.plugins ?? {};
213887
214001
  const pluginSpecs = Object.entries(config.plugins ?? {});
@@ -214114,7 +214228,7 @@ var androidBuildFlags = {
214114
214228
  };
214115
214229
  var buildAndroid = async (flags) => {
214116
214230
  const root = projectRoot();
214117
- const config = readJson2(join21(root, "app.json"));
214231
+ const config = readAppConfigAt(root);
214118
214232
  const androidDir = join21(root, "android");
214119
214233
  if (!config?.android || !existsSync19(androidDir)) {
214120
214234
  throw new CliError("No Android shell here — run `lecodes app init android` first.");
@@ -214318,7 +214432,7 @@ var writeSigningConfig = (root, config, storeFile, alias, storePassword, keyPass
214318
214432
  }
214319
214433
  }
214320
214434
  const configPath = join21(root, "app.json");
214321
- const fresh = readJson2(configPath) ?? config;
214435
+ const fresh = readAppConfigAt(root) ?? config;
214322
214436
  fresh.android = { ...fresh.android ?? {}, signing: { storeFile: envPath, keyAlias: alias } };
214323
214437
  writeJson(configPath, fresh);
214324
214438
  log(`${c.green("written")} app.json (android.signing — metadata only, no passwords)`);
@@ -214334,7 +214448,7 @@ var printCertificate = (storeFile, alias, storePassword) => {
214334
214448
  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
214449
  var keystoreProject = () => {
214336
214450
  const root = projectRoot();
214337
- return { root, config: readJson2(join21(root, "app.json")) ?? {} };
214451
+ return { root, config: readAppConfigAt(root) ?? {} };
214338
214452
  };
214339
214453
  var passwordFlag = { password: str("keystore password (else asked on the terminal)", { value: "<pw>" }) };
214340
214454
  var keystoreCreateFlags = {
@@ -215465,7 +215579,7 @@ var dirSize2 = (dir) => readdirSync8(dir, { withFileTypes: true }).reduce((sum,
215465
215579
  }, 0);
215466
215580
  var initDesktop = async (opts) => {
215467
215581
  const root = projectRoot();
215468
- const existing = readJson2(join24(root, "app.json")) ?? {};
215582
+ const existing = readAppConfigAt(root) ?? {};
215469
215583
  const name = opts.name ?? existing.name ?? basename8(root);
215470
215584
  const window2 = {
215471
215585
  width: 1280,
@@ -215499,7 +215613,7 @@ var desktopBuildFlags = {
215499
215613
  };
215500
215614
  var buildDesktop = async (flags) => {
215501
215615
  const root = projectRoot();
215502
- const config = readJson2(join24(root, "app.json")) ?? {};
215616
+ const config = readAppConfigAt(root) ?? {};
215503
215617
  const name = (config.name ?? basename8(root)).trim() || "app";
215504
215618
  const slug = flags.slug ?? cleanFileName(name);
215505
215619
  const out = resolve11(root, flags.out ?? join24(root, "dist", slug));
@@ -215776,7 +215890,7 @@ var initIos = async (opts) => {
215776
215890
  if (existsSync23(iosDir)) {
215777
215891
  throw new CliError("ios/ already exists — run `lecodes app sync` to update it.");
215778
215892
  }
215779
- const existing = readJson2(join25(root, "app.json")) ?? {};
215893
+ const existing = readAppConfigAt(root) ?? {};
215780
215894
  const variant = opts.variant ?? existing.ios?.variant ?? "auto";
215781
215895
  if (variant !== "auto" && !SDK_VARIANTS.includes(variant)) {
215782
215896
  throw new CliError(`--variant must be auto, ${SDK_VARIANTS.join(", ")} (got "${variant}")`);
@@ -215865,7 +215979,7 @@ var init = defineCommand({
215865
215979
  });
215866
215980
  var runSync = async (platformArg) => {
215867
215981
  const root = projectRoot();
215868
- const config = readJson2(join25(root, "app.json"));
215982
+ const config = readAppConfigAt(root);
215869
215983
  const platform = mobilePlatform(platformArg);
215870
215984
  const { hasIos, hasAndroid } = shellsOf(root, config);
215871
215985
  if (platform === "ios" && !hasIos)
@@ -215946,14 +216060,8 @@ var syncIos = async (root, config) => {
215946
216060
  mkdirSync15(resourcesDir, { recursive: true });
215947
216061
  const resources = await compileShell(join25(resourcesDir, "app.js"), { remotePatterns });
215948
216062
  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)"}`);
216063
+ const { variant, why } = resolveVariant(requestedVariant, bundleJs);
216064
+ log(`${c.green("variant")} ${variant}${requestedVariant === "auto" ? ` (auto: ${why})` : " (forced by ios.variant)"}`);
215957
216065
  const sdkWhy = sdkPin === "latest" ? "latest" : config.ios.sdk ? "pinned by ios.sdk" : "from the shell — set ios.sdk or run `lecodes app update`";
215958
216066
  log(`${c.green("sdk")} lecodes-ios-sdk ${sdkVersion}${c.dim(` (${sdkWhy})`)}`);
215959
216067
  const preload = /^\/\/ preload: (.*)$/m.exec(bundleJs)?.[1].split(";").map((s) => s.trim()).filter(Boolean) ?? [];
@@ -216076,7 +216184,7 @@ Native OTA JS updates at launch (both shells; the current launch runs the newest
216076
216184
  var pluginConfig = () => {
216077
216185
  const root = projectRoot();
216078
216186
  const configPath = join25(root, "app.json");
216079
- const config = readJson2(configPath);
216187
+ const config = readAppConfigAt(root);
216080
216188
  if (!config)
216081
216189
  throw new CliError("No app.json here — run `lecodes app init <ios|android>` first.");
216082
216190
  config.plugins ??= {};
@@ -216181,7 +216289,7 @@ var updateSdkPin = async (root, config, wanted, checkOnly, platform) => {
216181
216289
  };
216182
216290
  var runAppUpdate = async (opts = {}) => {
216183
216291
  const root = projectRoot();
216184
- const config = readJson2(join25(root, "app.json"));
216292
+ const config = readAppConfigAt(root);
216185
216293
  const platform = mobilePlatform(opts.platform);
216186
216294
  const { hasIos, hasAndroid } = shellsOf(root, config);
216187
216295
  if (platform === "ios" && !hasIos)
@@ -216339,7 +216447,7 @@ var doctor = defineCommand({
216339
216447
  examples: ["lecodes app doctor"],
216340
216448
  run: async () => {
216341
216449
  const root = projectRoot();
216342
- const config = readJson2(join25(root, "app.json"));
216450
+ const config = readAppConfigAt(root);
216343
216451
  const { hasIos, hasAndroid } = shellsOf(root, config);
216344
216452
  if (!hasIos && !hasAndroid)
216345
216453
  throw new CliError(NO_IOS_SHELL);
@@ -216386,7 +216494,7 @@ var exportOptionsPlist = (method, teamId, destination) => `<?xml version="1.0" e
216386
216494
  `;
216387
216495
  var requireShell = () => {
216388
216496
  const root = projectRoot();
216389
- const config = readJson2(join25(root, "app.json"));
216497
+ const config = readAppConfigAt(root);
216390
216498
  if (!config?.ios || !existsSync23(join25(root, "ios")))
216391
216499
  throw new CliError(NO_IOS_SHELL);
216392
216500
  return { root, config };
@@ -216499,7 +216607,7 @@ var iosUpload = defineCommand({
216499
216607
  "-allowProvisioningUpdates"
216500
216608
  ]);
216501
216609
  const configPath = join25(root, "app.json");
216502
- const fresh = readJson2(configPath);
216610
+ const fresh = readAppConfigAt(root);
216503
216611
  fresh.ios.buildNumber = (fresh.ios.buildNumber ?? 1) + 1;
216504
216612
  writeJson(configPath, fresh);
216505
216613
  success(`Uploaded build ${(fresh.ios.buildNumber ?? 2) - 1} — check App Store Connect / TestFlight once processing finishes.`);
@@ -216745,7 +216853,7 @@ lecodes render --desktop --view iso --png iso.png # 3D: inspection camera fitt
216745
216853
  # { "view": "iso", "screenshot": "iso.png" } then { "view": "off" }
216746
216854
  lecodes render --desktop --script <file> # the same scenario on the NATIVE host (real GPU, the
216747
216855
  # real hit-test): the same steps, targets and expects as
216748
- # headless (only expect.audio / the json output are headless-only)
216856
+ # headless (only expect.audio / expect.toast / the json output are headless-only)
216749
216857
  lecodes render --sim --script <file> # the WINDOWLESS native host (lecodes-server): the real
216750
216858
  # engines, no GPU, no pixels, no UI hit-test — expect.log /
216751
216859
  # expect.paint only; runs on CI and beside an open desktop window
@@ -216791,11 +216899,12 @@ Three hosts run the same file, at a fixed 16 ms clock on all of them:
216791
216899
  |---|---|---|---|---|
216792
216900
  | headless | (none) | Bun: the runtime + UI renderer + a recording 3D core (no GPU) | names or \`{ x, y }\` | all |
216793
216901
  | 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\` |
216902
+ | desktop | \`--desktop\` | the hidden desktop window: pixels, GL materials, the real hit-test | names or \`{ x, y }\` | all but \`audio\` / \`toast\` |
216795
216903
 
216796
216904
  One flow file, the same verdict on headless and desktop: names resolve the same way (\`name\` →
216797
216905
  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
216906
+ \`clip\` included) and the semantic expects read the same tree. Only \`expect.audio\`,
216907
+ \`expect.toast\` (the last \`toast()\` message — a toast is not in the tree) and the
216799
216908
  \`json\` output are the headless renderer's. On the desktop \`type\` taps the input and types into
216800
216909
  it (appends); the sim has no UI, so it takes coordinates and \`expect.log\` / \`expect.paint\` only.
216801
216910
 
@@ -219979,8 +220088,8 @@ var lightmap_default = defineCommand({
219979
220088
  // src/commands/link.ts
219980
220089
  init_cli();
219981
220090
  init_api();
219982
- import { existsSync as existsSync39 } from "node:fs";
219983
- import { basename as basename21, join as join44, resolve as resolve21 } from "node:path";
220091
+ import { existsSync as existsSync40 } from "node:fs";
220092
+ import { basename as basename21, join as join44, resolve as resolve22 } from "node:path";
219984
220093
  init_ignore();
219985
220094
  init_localFiles();
219986
220095
  init_manifest();
@@ -219990,14 +220099,148 @@ init_types4();
219990
220099
  // src/commands/push.ts
219991
220100
  init_cli();
219992
220101
  init_api();
219993
- import { readFileSync as readFileSync31 } from "node:fs";
220102
+ import { readFileSync as readFileSync32 } from "node:fs";
219994
220103
  import { basename as basename20 } from "node:path";
219995
220104
  init_localFiles();
219996
220105
  init_manifest();
220106
+
220107
+ // src/commands/releases.ts
220108
+ init_cli();
220109
+ init_api();
220110
+ init_localFiles();
220111
+ import { existsSync as existsSync39, readFileSync as readFileSync31 } from "node:fs";
220112
+ import { resolve as resolve21 } from "node:path";
220113
+ init_shared();
220114
+ var compareVersions = (a, b) => {
220115
+ const pa = a.split("."), pb = b.split(".");
220116
+ for (let i = 0;i < Math.max(pa.length, pb.length); i++) {
220117
+ const d = (parseInt(pa[i] ?? "0", 10) || 0) - (parseInt(pb[i] ?? "0", 10) || 0);
220118
+ if (d)
220119
+ return d;
220120
+ }
220121
+ return 0;
220122
+ };
220123
+ var formatSize = (bytes) => bytes < 1024 ? `${bytes} B` : bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1024 / 1024).toFixed(1)} MB`;
220124
+ var qrUrl = (apiUrl, uuid) => `${apiUrl}/qr/${uuid}`;
220125
+ var reportRelease = (release, qr, verb = "Published") => {
220126
+ const parts = [release.unchanged ? `Nothing changed — ${release.version} is current` : `${verb} ${release.version}`, release.url.replace(/\/+$/, "")];
220127
+ if (!release.unchanged) {
220128
+ if (typeof release.size === "number")
220129
+ parts.push(formatSize(release.size));
220130
+ if (release.sdk)
220131
+ parts.push(`SDK ${release.sdk}`);
220132
+ }
220133
+ success(parts.join(" · "));
220134
+ info(` ${c.dim("QR")} ${qr}`);
220135
+ };
220136
+ var withSchemaConfirm = async (call, opts) => {
220137
+ try {
220138
+ return await call(false);
220139
+ } catch (e) {
220140
+ if (!(e instanceof SchemaChangeError))
220141
+ throw e;
220142
+ if (e.incompatible !== null) {
220143
+ throw new CliError(`The production database cannot migrate to this schema in place${e.incompatible ? `: ${e.incompatible}` : ""}. Nothing changed.`, 409);
220144
+ }
220145
+ warn("The schema change deletes data in the production database:");
220146
+ for (const op of e.destructive)
220147
+ log(c.red(` ${op}`));
220148
+ if (!opts.allowDestructive) {
220149
+ if (!process.stdin.isTTY)
220150
+ throw new CliError(`Nothing changed. Run \`${opts.retry} --allow-destructive\` to apply it.`, 409);
220151
+ if (!await confirm("Delete that data and go on?", false)) {
220152
+ info(c.dim("Cancelled — nothing changed."));
220153
+ return null;
220154
+ }
220155
+ }
220156
+ return await call(true);
220157
+ }
220158
+ };
220159
+ var allowDestructiveFlag = { "allow-destructive": bool("apply a schema change that deletes production data without asking") };
220160
+ var use = defineCommand({
220161
+ name: "use",
220162
+ usage: "<version>",
220163
+ summary: "Make a release current again — a rollback, no compile",
220164
+ 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).",
220165
+ flags: allowDestructiveFlag,
220166
+ examples: ["lecodes releases use 1.2.3"],
220167
+ run: async ({ args, flags }) => {
220168
+ const version = args[0]?.replace(/^v/, "");
220169
+ if (!version)
220170
+ throw new UsageError("Which release? `lecodes releases` lists them.");
220171
+ const { apiUrl, token, manifest } = linkedProject();
220172
+ const releases = await listReleases(apiUrl, token, manifest.uuid);
220173
+ const target = releases.find((r) => r.version === version);
220174
+ if (!target)
220175
+ throw new CliError(`${manifest.name} has no release ${version} — \`lecodes releases\` lists them.`);
220176
+ const previous = releases.find((r) => r.current);
220177
+ const res = await withSchemaConfirm((allowDestructive) => useRelease(apiUrl, token, manifest.uuid, version, { allowDestructive }), { allowDestructive: flags["allow-destructive"], retry: `lecodes releases use ${version}` });
220178
+ if (!res)
220179
+ return;
220180
+ const back = previous !== undefined && compareVersions(res.version, previous.version) < 0;
220181
+ reportRelease({ ...target, ...res }, qrUrl(apiUrl, manifest.uuid), back ? "Rolled back to" : "Published");
220182
+ }
220183
+ });
220184
+ var upload2 = defineCommand({
220185
+ name: "upload",
220186
+ usage: "<app.js>",
220187
+ summary: "Make a bundle compiled here the current release — no build on the platform",
220188
+ 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).",
220189
+ flags: { message: str('checkpoint message when there are local changes (default "Release")', { alias: "m", value: "<text>" }) },
220190
+ examples: ["lecodes releases upload app.js", 'lecodes releases upload build/app.js -m "Kungur for the 2.1 app"'],
220191
+ run: async ({ args, flags }) => {
220192
+ const file = args[0];
220193
+ if (!file)
220194
+ throw new UsageError("Which bundle? `lecodes releases upload <app.js>`.");
220195
+ const path = resolve21(file);
220196
+ if (!existsSync39(path))
220197
+ throw new CliError(`No such file: ${file}`);
220198
+ const code = readFileSync31(path, "utf8");
220199
+ const sdk = /^\/\/ sdk: (\S+)/m.exec(code)?.[1];
220200
+ if (!sdk)
220201
+ throw new CliError(`${file} is not a LeCodes bundle: no \`// sdk:\` header line. \`lecodes compile --remote-assets -o app.js\` writes one.`);
220202
+ const map = existsSync39(`${path}.map`) ? readFileSync31(`${path}.map`, "utf8") : null;
220203
+ const { root, manifest, apiUrl, token } = linkedProject();
220204
+ if (hasChanges(diffLocal(manifest, scanProject(root, manifest))))
220205
+ await runPush({ message: flags.message || "Release" });
220206
+ const release = await uploadReleaseBundle(apiUrl, token, manifest.uuid, { code, map });
220207
+ reportRelease(release, qrUrl(apiUrl, manifest.uuid), "Uploaded");
220208
+ }
220209
+ });
220210
+ var releases_default = defineCommand({
220211
+ name: "releases",
220212
+ summary: "List the project's releases (● = the current one); `releases use <version>` rolls back, `releases upload <app.js>` releases a bundle compiled here",
220213
+ 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.",
220214
+ commands: [use, upload2],
220215
+ examples: ["lecodes releases", "lecodes releases use 1.2.3", "lecodes releases upload app.js"],
220216
+ run: async () => {
220217
+ const { apiUrl, token, manifest } = linkedProject();
220218
+ const releases = await listReleases(apiUrl, token, manifest.uuid);
220219
+ if (releases.length === 0) {
220220
+ info(`${manifest.name} has no releases yet — \`lecodes publish\` makes the first.`);
220221
+ return;
220222
+ }
220223
+ table([
220224
+ ["", c.dim("VERSION"), c.dim("DATE"), c.dim("SIZE"), c.dim("SDK")],
220225
+ ...releases.map((r) => [
220226
+ r.current ? c.green("●") : "",
220227
+ r.current ? c.bold(r.version) : r.version,
220228
+ relativeTime(r.createdAt),
220229
+ formatSize(r.size),
220230
+ r.sdk
220231
+ ])
220232
+ ]);
220233
+ if (!releases.some((r) => r.current))
220234
+ info(c.dim(" Unpublished: no release is current."));
220235
+ }
220236
+ });
220237
+
220238
+ // src/commands/push.ts
219997
220239
  init_shared();
219998
220240
  var pushFlags = {
219999
220241
  message: str("checkpoint message (asked for when omitted)", { alias: "m", value: "<text>" }),
220000
- publish: bool("also deploy the project live (compile) with the push"),
220242
+ publish: bool("also release the project with the push (= `lecodes publish`)"),
220243
+ "force-publish": bool("--publish, and release even when nothing changed — a new SDK on the build service (= `lecodes publish --force`)"),
220001
220244
  force: bool("push even if the server moved on (shows + confirms which server files get overwritten)"),
220002
220245
  yes: bool("skip the --force overwrite confirmation", { alias: "y" })
220003
220246
  };
@@ -220005,18 +220248,21 @@ var push_default = defineCommand({
220005
220248
  name: "push",
220006
220249
  summary: "Push local changes as one new checkpoint",
220007
220250
  flags: pushFlags,
220008
- examples: ['lecodes push -m "Tweak the menu"', "lecodes push --publish"],
220251
+ examples: ['lecodes push -m "Tweak the menu"', "lecodes push --publish", "lecodes push --force-publish"],
220009
220252
  run: ({ flags }) => runPush(flags)
220010
220253
  });
220011
220254
  var runPush = async (opts) => {
220012
220255
  const { root, manifest, apiUrl, token } = linkedProject();
220013
- const publish = !!opts.publish;
220256
+ const forcePublish = !!opts["force-publish"];
220257
+ const publish = !!opts.publish || forcePublish;
220014
220258
  const scan = scanProject(root, manifest);
220015
220259
  const diff = diffLocal(manifest, scan);
220016
220260
  if (!hasChanges(diff)) {
220017
220261
  if (publish) {
220018
- await publishProject(apiUrl, token, manifest.uuid);
220019
- success("No file changes — published the current project state.");
220262
+ info(c.dim("Working tree clean — publishing the project as it is."));
220263
+ const release = await withSchemaConfirm((allowDestructive) => publishProject(apiUrl, token, manifest.uuid, { allowDestructive, force: forcePublish }), { allowDestructive: false, retry: "lecodes publish" });
220264
+ if (release)
220265
+ reportRelease(release, qrUrl(apiUrl, manifest.uuid));
220020
220266
  } else
220021
220267
  info(c.dim("Working tree clean — nothing to push."));
220022
220268
  await flushComments();
@@ -220058,9 +220304,9 @@ var runPush = async (opts) => {
220058
220304
  if (source)
220059
220305
  entry.assetId = source.id;
220060
220306
  if (isTextKind(kind))
220061
- entry.text = readFileSync31(file.absPath, "utf8");
220307
+ entry.text = readFileSync32(file.absPath, "utf8");
220062
220308
  else if (!source || source.sha !== file.sha) {
220063
- const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename20(file.path), readFileSync31(file.absPath));
220309
+ const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename20(file.path), readFileSync32(file.absPath));
220064
220310
  entry.blobFileId = blob.id;
220065
220311
  uploaded++;
220066
220312
  }
@@ -220069,11 +220315,11 @@ var runPush = async (opts) => {
220069
220315
  const folders = scan.dirs.map((path) => ({ path }));
220070
220316
  if (uploaded > 0)
220071
220317
  info(c.dim(`Uploaded ${uploaded} binary file${uploaded === 1 ? "" : "s"}.`));
220072
- const res = await sync(apiUrl, token, manifest.uuid, { message, files, folders, publish });
220318
+ const res = await sync(apiUrl, token, manifest.uuid, { message, files, folders, publish, ...forcePublish ? { force: true } : {} });
220073
220319
  if (!res.created) {
220074
220320
  info(c.dim("Server reported no changes."));
220075
220321
  if (publish)
220076
- reportPublish(res);
220322
+ reportPublish(res, qrUrl(apiUrl, manifest.uuid));
220077
220323
  return;
220078
220324
  }
220079
220325
  const shaByPath = new Map(scan.files.map((f) => [f.path, f.sha]));
@@ -220089,7 +220335,7 @@ var runPush = async (opts) => {
220089
220335
  ].filter(Boolean).join(", ");
220090
220336
  success(`Pushed checkpoint${res.seq ? ` #${res.seq}` : ""}${counts ? ` (${counts})` : ""}: ${message}`);
220091
220337
  if (publish)
220092
- reportPublish(res);
220338
+ reportPublish(res, qrUrl(apiUrl, manifest.uuid));
220093
220339
  await flushComments();
220094
220340
  };
220095
220341
  var flushComments = async () => {
@@ -220112,11 +220358,11 @@ var serverPathDrift = (project, manifest) => {
220112
220358
  const parts = [added.length ? describe("added", added) : "", removed.length ? describe("removed", removed) : ""].filter(Boolean);
220113
220359
  return parts.length ? parts.join("; ") : null;
220114
220360
  };
220115
- var reportPublish = (res) => {
220116
- if (res.published)
220117
- success("Published — the project is now live.");
220361
+ var reportPublish = (res, qr) => {
220362
+ if (res.published && res.release)
220363
+ reportRelease(res.release, qr);
220118
220364
  else
220119
- warn(`Push landed, but publish failed: ${res.publishError ?? "unknown error"}`);
220365
+ warn(`Push landed, but publish failed: ${res.publishError ?? "unknown error"} — \`lecodes publish\` tries again.`);
220120
220366
  };
220121
220367
 
220122
220368
  // src/commands/link.ts
@@ -220171,10 +220417,10 @@ var link_default = defineCommand({
220171
220417
  run: async ({ args, flags }) => {
220172
220418
  const { apiUrl, token } = session();
220173
220419
  const dirArg = args[0];
220174
- const root = dirArg ? resolve21(dirArg) : process.cwd();
220175
- if (!existsSync39(root))
220420
+ const root = dirArg ? resolve22(dirArg) : process.cwd();
220421
+ if (!existsSync40(root))
220176
220422
  throw new CliError(`No such directory: ${root}`);
220177
- if (existsSync39(join44(root, LECODES_DIR, "manifest.json")) && !flags.force) {
220423
+ if (existsSync40(join44(root, LECODES_DIR, "manifest.json")) && !flags.force) {
220178
220424
  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
220425
  }
220180
220426
  let enclosing = null;
@@ -220260,9 +220506,9 @@ var login_default = defineCommand({
220260
220506
  });
220261
220507
 
220262
220508
  // 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";
220509
+ 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
220510
  import { tmpdir as tmpdir5 } from "node:os";
220265
- import { basename as basename22, join as join45, relative as relative7, resolve as resolve22 } from "node:path";
220511
+ import { basename as basename22, join as join45, relative as relative7, resolve as resolve23 } from "node:path";
220266
220512
  init_cli();
220267
220513
  init_shared();
220268
220514
 
@@ -221059,7 +221305,7 @@ var range2 = (name, value, fallback, min, max) => {
221059
221305
  var guessName = (root) => {
221060
221306
  const dirs = [join45(root, "src"), root];
221061
221307
  for (const dir of dirs) {
221062
- if (!existsSync40(dir))
221308
+ if (!existsSync41(dir))
221063
221309
  continue;
221064
221310
  let entries;
221065
221311
  try {
@@ -221071,7 +221317,7 @@ var guessName = (root) => {
221071
221317
  if (!f.endsWith(".scene.ts"))
221072
221318
  continue;
221073
221319
  try {
221074
- if (/\bnavmesh\s*:/.test(readFileSync32(join45(dir, f), "utf8")))
221320
+ if (/\bnavmesh\s*:/.test(readFileSync33(join45(dir, f), "utf8")))
221075
221321
  return f.slice(0, -".scene.ts".length);
221076
221322
  } catch {}
221077
221323
  }
@@ -221137,9 +221383,9 @@ var bake2 = defineCommand({
221137
221383
  examples: ["lecodes navmesh bake", "lecodes navmesh bake --name yard --cell 0.2 --obj --logs"],
221138
221384
  run: async ({ flags }) => {
221139
221385
  const { root } = localProject();
221140
- const outDir = resolve22(root, flags.out);
221386
+ const outDir = resolve23(root, flags.out);
221141
221387
  const entryFile = flags.entry;
221142
- if (!existsSync40(resolve22(root, entryFile))) {
221388
+ if (!existsSync41(resolve23(root, entryFile))) {
221143
221389
  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
221390
  }
221145
221391
  const name = flags.name ?? guessName(root) ?? "scene";
@@ -221150,7 +221396,7 @@ var bake2 = defineCommand({
221150
221396
  const logs = flags.logs;
221151
221397
  mkdirSync25(outDir, { recursive: true });
221152
221398
  const navmeshPath = join45(outDir, `${name}.navmesh`);
221153
- if (!existsSync40(navmeshPath))
221399
+ if (!existsSync41(navmeshPath))
221154
221400
  writeFileSync27(navmeshPath, placeholderLnav());
221155
221401
  const importPath = "./" + entryFile.replace(/\\/g, "/").replace(/\.tsx?$/, "");
221156
221402
  const entryPath = "/.navmesh-entry.ts";
@@ -221219,9 +221465,9 @@ import ${JSON.stringify(importPath)}
221219
221465
  if (!config)
221220
221466
  throw new CliError("The host reported a dump but no `[navmesh] config` line — SDK/host mismatch?");
221221
221467
  const geoPath = join45(outDir, `${name}.navgeo`);
221222
- if (!existsSync40(geoPath))
221468
+ if (!existsSync41(geoPath))
221223
221469
  throw new CliError(`The host reported a dump but ${geoPath} is missing.`);
221224
- const geo = readNgeo(new Uint8Array(readFileSync32(geoPath)));
221470
+ const geo = readNgeo(new Uint8Array(readFileSync33(geoPath)));
221225
221471
  const params = {
221226
221472
  agentRadius: range2("radius", flags.radius, config.agent.radius, 0.05, 10),
221227
221473
  agentHeight: range2("height", flags.height, config.agent.height, 0.1, 20),
@@ -221295,25 +221541,44 @@ init_cli();
221295
221541
 
221296
221542
  // src/plugin/generate.ts
221297
221543
  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";
221544
+ import { existsSync as existsSync42, mkdirSync as mkdirSync26, readFileSync as readFileSync34, readdirSync as readdirSync18, statSync as statSync16, writeFileSync as writeFileSync28 } from "node:fs";
221299
221545
  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";
221546
+ import { basename as basename23, join as join46, relative as relative8, resolve as resolve24, sep as sep3 } from "node:path";
221301
221547
 
221302
221548
  // src/plugin/permissions.ts
221303
221549
  var PERMISSION_KINDS = ["camera", "microphone", "location", "speech"];
221304
221550
  var isPermissionKind = (s) => PERMISSION_KINDS.includes(s);
221305
221551
  var PERMISSION_NEEDS = {
221306
- camera: { infoPlist: ["NSCameraUsageDescription"], android: [["android.permission.CAMERA"]] },
221307
- microphone: { infoPlist: ["NSMicrophoneUsageDescription"], android: [["android.permission.RECORD_AUDIO"]] },
221552
+ camera: { infoPlist: ["NSCameraUsageDescription"], android: [["android.permission.CAMERA"]], androidFeatures: ["android.hardware.camera"] },
221553
+ microphone: {
221554
+ infoPlist: ["NSMicrophoneUsageDescription"],
221555
+ android: [["android.permission.RECORD_AUDIO"]],
221556
+ androidFeatures: ["android.hardware.microphone"]
221557
+ },
221308
221558
  location: {
221309
221559
  infoPlist: ["NSLocationWhenInUseUsageDescription"],
221310
- android: [["android.permission.ACCESS_FINE_LOCATION", "android.permission.ACCESS_COARSE_LOCATION"]]
221560
+ android: [["android.permission.ACCESS_FINE_LOCATION", "android.permission.ACCESS_COARSE_LOCATION"]],
221561
+ androidFeatures: ["android.hardware.location"]
221311
221562
  },
221312
- speech: { infoPlist: ["NSSpeechRecognitionUsageDescription"], android: [["android.permission.RECORD_AUDIO"]] }
221563
+ speech: {
221564
+ infoPlist: ["NSSpeechRecognitionUsageDescription"],
221565
+ android: [["android.permission.RECORD_AUDIO"]],
221566
+ androidFeatures: ["android.hardware.microphone"]
221567
+ }
221313
221568
  };
221314
221569
  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: [] };
221570
+ var usesFeatures = (xml) => {
221571
+ const out = [];
221572
+ for (const m of xml.replace(/<!--[\s\S]*?-->/g, "").matchAll(/<uses-feature\b[^>]*>/g)) {
221573
+ const attr = (re) => re.exec(m[0])?.[2];
221574
+ const name = attr(/\bandroid:name\s*=\s*(["'])(.*?)\1/);
221575
+ if (name)
221576
+ out.push({ name, required: attr(/\bandroid:required\s*=\s*(["'])(.*?)\1/)?.trim() !== "false" });
221577
+ }
221578
+ return out;
221579
+ };
221580
+ var missingDeclarations = (kinds, infoPlist, androidNames, androidFeatures) => {
221581
+ const out = { infoPlist: [], android: [], androidFeatures: [] };
221317
221582
  for (const kind of kinds) {
221318
221583
  const needs = PERMISSION_NEEDS[kind];
221319
221584
  if (infoPlist)
@@ -221326,8 +221591,14 @@ var missingDeclarations = (kinds, infoPlist, androidNames) => {
221326
221591
  if (!group.some((name) => androidNames.includes(name)))
221327
221592
  out.android.push(`${group.join(" or ")} (${kind})`);
221328
221593
  }
221594
+ if (androidFeatures)
221595
+ for (const feature of needs.androidFeatures) {
221596
+ const declared = androidFeatures.filter((f) => f.name === feature);
221597
+ if (declared.length === 0 || declared.some((f) => f.required))
221598
+ out.androidFeatures.push(`${feature} (${kind})`);
221599
+ }
221329
221600
  }
221330
- return { infoPlist: [...new Set(out.infoPlist)], android: [...new Set(out.android)] };
221601
+ return { infoPlist: [...new Set(out.infoPlist)], android: [...new Set(out.android)], androidFeatures: [...new Set(out.androidFeatures)] };
221331
221602
  };
221332
221603
 
221333
221604
  // src/plugin/contract.ts
@@ -224135,13 +224406,13 @@ const wire = {
224135
224406
  var shown = (from, to) => relative8(from, to).split(sep3).join("/");
224136
224407
  var CONTRACT_FILE = "contract.d.ts";
224137
224408
  var findPlugins = (path) => {
224138
- const root = resolve23(path);
224139
- if (existsSync41(join46(root, CONTRACT_FILE)))
224409
+ const root = resolve24(path);
224410
+ if (existsSync42(join46(root, CONTRACT_FILE)))
224140
224411
  return [{ id: basename23(root), dir: root }];
224141
224412
  const holder = join46(root, "plugins");
224142
- if (!existsSync41(holder))
224413
+ if (!existsSync42(holder))
224143
224414
  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)));
224415
+ return readdirSync18(holder).sort().map((id) => ({ id, dir: join46(holder, id) })).filter((p) => statSync16(p.dir).isDirectory() && existsSync42(join46(p.dir, CONTRACT_FILE)));
224145
224416
  };
224146
224417
  var loadTypescript = (dir) => {
224147
224418
  for (const from of [join46(dir, "package.json"), import.meta.url]) {
@@ -224156,25 +224427,25 @@ var loadTypescript = (dir) => {
224156
224427
  };
224157
224428
  var generatePlugin = (ts, plugin) => {
224158
224429
  const file = join46(plugin.dir, CONTRACT_FILE);
224159
- const contract = readContract(ts, file, readFileSync33(file, "utf8"));
224430
+ const contract = readContract(ts, file, readFileSync34(file, "utf8"));
224160
224431
  const manifest = readManifestIfAny(plugin.dir, plugin.id);
224161
224432
  checkPermissions(plugin, contract, manifest);
224162
- const handWritten = existsSync41(join46(plugin.dir, "sdk", `${plugin.id}.ts`));
224433
+ const handWritten = existsSync42(join46(plugin.dir, "sdk", `${plugin.id}.ts`));
224163
224434
  const single = contract.channels.length === 1;
224164
224435
  const files = contract.channels.map((channel) => ({
224165
224436
  file: join46(plugin.dir, "sdk", single ? `${plugin.id}.gen.ts` : `${plugin.id}.${channel.iface}.gen.ts`),
224166
224437
  text: emitSdk(contract, channel, { id: plugin.id, handWritten })
224167
224438
  }));
224168
- if (existsSync41(join46(plugin.dir, "ios"))) {
224439
+ if (existsSync42(join46(plugin.dir, "ios"))) {
224169
224440
  const name = single ? contract.channels[0].iface : contract.channels[0].iface + "Plugin";
224170
224441
  files.push({ file: join46(plugin.dir, "ios", `${name}.gen.swift`), text: emitSwift(lower3(contract), { id: plugin.id }) });
224171
224442
  }
224172
- if (existsSync41(join46(plugin.dir, "android"))) {
224443
+ if (existsSync42(join46(plugin.dir, "android"))) {
224173
224444
  const name = single ? contract.channels[0].iface : contract.channels[0].iface + "Plugin";
224174
224445
  const pkg = kotlinPackage(join46(plugin.dir, "android"));
224175
224446
  files.push({ file: join46(pkg.dir, `${name}.gen.kt`), text: emitKotlin(lower3(contract), { id: plugin.id, package: pkg.name }) });
224176
224447
  }
224177
- if (existsSync41(join46(plugin.dir, "web"))) {
224448
+ if (existsSync42(join46(plugin.dir, "web"))) {
224178
224449
  files.push({ file: join46(plugin.dir, "web", `${plugin.id}.gen.ts`), text: emitWeb(lower3(contract), { id: plugin.id }) });
224179
224450
  const foreign = hostImports(join46(plugin.dir, "web"));
224180
224451
  if (foreign.length)
@@ -224196,13 +224467,15 @@ var checkPermissions = (plugin, contract, manifest) => {
224196
224467
  if (kinds.length === 0)
224197
224468
  return;
224198
224469
  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);
224470
+ const androidXml = manifest.android ? existsSync42(androidManifest) ? readFileSync34(androidManifest, "utf8") : "" : null;
224471
+ const missing = missingDeclarations(kinds, manifest.ios ? manifest.ios.infoPlist ?? {} : null, androidXml === null ? null : usesPermissions(androidXml), androidXml === null ? null : usesFeatures(androidXml));
224201
224472
  const problems = [];
224202
224473
  if (missing.infoPlist.length)
224203
224474
  problems.push(`ios.infoPlist: the contract's @permission needs ${missing.infoPlist.join(", ")} — the usage string the dialog shows`);
224204
224475
  if (missing.android.length)
224205
224476
  problems.push(`android/src/main/AndroidManifest.xml: the contract's @permission needs <uses-permission android:name="…"/> for ${missing.android.join(", ")}`);
224477
+ if (missing.androidFeatures.length)
224478
+ 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
224479
  if (problems.length)
224207
224480
  throw new ManifestError(join46(plugin.dir, MANIFEST_FILE), problems);
224208
224481
  };
@@ -224218,7 +224491,7 @@ var hostImports = (web) => {
224218
224491
  }
224219
224492
  if (!/\.(ts|tsx|js|mjs)$/.test(entry))
224220
224493
  continue;
224221
- readFileSync33(path, "utf8").split(`
224494
+ readFileSync34(path, "utf8").split(`
224222
224495
  `).forEach((line, i) => {
224223
224496
  const m = /(?:from\s+|import\s*\(\s*|import\s+)["'](lecodes-[^"']*)["']/.exec(line);
224224
224497
  if (m && !/^\s*\/\//.test(line))
@@ -224232,7 +224505,7 @@ var hostImports = (web) => {
224232
224505
  var kotlinPackage = (android) => {
224233
224506
  const root = join46(android, "src", "main", "java");
224234
224507
  let dir = root;
224235
- while (existsSync41(dir)) {
224508
+ while (existsSync42(dir)) {
224236
224509
  const entries = readdirSync18(dir).filter((e) => !e.startsWith("."));
224237
224510
  const dirs = entries.filter((e) => statSync16(join46(dir, e)).isDirectory());
224238
224511
  if (dirs.length !== 1 || entries.length !== 1)
@@ -224246,10 +224519,10 @@ var kotlinPackage = (android) => {
224246
224519
  };
224247
224520
  var repoSdkPath = (root) => {
224248
224521
  const file = join46(root, "package.json");
224249
- if (!existsSync41(file))
224522
+ if (!existsSync42(file))
224250
224523
  return DEFAULT_SDK_PATH;
224251
224524
  try {
224252
- const pkg = JSON.parse(readFileSync33(file, "utf8"));
224525
+ const pkg = JSON.parse(readFileSync34(file, "utf8"));
224253
224526
  const path = pkg.lecodes?.iosSdkPath;
224254
224527
  if (path !== undefined && typeof path !== "string")
224255
224528
  throw new PackageError(`package.json lecodes.iosSdkPath: a path, as a string`);
@@ -224263,13 +224536,13 @@ var repoSdkPath = (root) => {
224263
224536
  var generate = (path, opts = {}) => {
224264
224537
  const plugins = findPlugins(path);
224265
224538
  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).`);
224539
+ 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
224540
  const ts = loadTypescript(plugins[0].dir);
224268
- const root = resolve23(path);
224541
+ const root = resolve24(path);
224269
224542
  const report = { plugins: [], repo: { written: [], stale: [], fresh: [] }, failures: [] };
224270
224543
  const place = (f, entry) => {
224271
224544
  const name = shown(root, f.file) || basename23(f.file);
224272
- const current = existsSync41(f.file) ? readFileSync33(f.file, "utf8") : null;
224545
+ const current = existsSync42(f.file) ? readFileSync34(f.file, "utf8") : null;
224273
224546
  if (current === f.text) {
224274
224547
  entry.fresh.push(name);
224275
224548
  return;
@@ -224306,7 +224579,7 @@ ${e.problems.map((p) => ` ${p}`).join(`
224306
224579
  throw e;
224307
224580
  }
224308
224581
  }
224309
- const isRepo = !existsSync41(join46(root, CONTRACT_FILE));
224582
+ const isRepo = !existsSync42(join46(root, CONTRACT_FILE));
224310
224583
  if (isRepo && report.failures.length === 0 && manifests.some((m) => m.manifest.ios)) {
224311
224584
  try {
224312
224585
  place({ file: join46(root, PACKAGE_FILE), text: emitPackageSwift(manifests, repoSdkPath(root)) }, report.repo);
@@ -224582,12 +224855,37 @@ var pn_default = defineCommand({
224582
224855
  commands: [send, devices, keys]
224583
224856
  });
224584
224857
 
224858
+ // src/commands/publish.ts
224859
+ init_cli();
224860
+ init_api();
224861
+ init_localFiles();
224862
+ init_shared();
224863
+ var publish_default = defineCommand({
224864
+ name: "publish",
224865
+ summary: "Release the project: push local changes, build, make it the live version",
224866
+ 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.',
224867
+ flags: {
224868
+ message: str('checkpoint message when there are local changes (default "Release")', { alias: "m", value: "<text>" }),
224869
+ force: bool("rebuild even when nothing changed — a new SDK on the build service"),
224870
+ "allow-destructive": bool("apply a schema change that deletes production data without asking")
224871
+ },
224872
+ examples: ["lecodes publish", 'lecodes publish -m "Checkout v2"', "lecodes publish --force", "lecodes publish --allow-destructive"],
224873
+ run: async ({ flags }) => {
224874
+ const { root, manifest, apiUrl, token } = linkedProject();
224875
+ if (hasChanges(diffLocal(manifest, scanProject(root, manifest))))
224876
+ await runPush({ message: flags.message || "Release" });
224877
+ 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" });
224878
+ if (release)
224879
+ reportRelease(release, qrUrl(apiUrl, manifest.uuid));
224880
+ }
224881
+ });
224882
+
224585
224883
  // src/commands/pull.ts
224586
224884
  init_cli();
224587
224885
  init_api();
224588
224886
  init_localFiles();
224589
224887
  init_manifest();
224590
- import { existsSync as existsSync42, mkdirSync as mkdirSync27, rmSync as rmSync13, writeFileSync as writeFileSync29 } from "node:fs";
224888
+ import { existsSync as existsSync43, mkdirSync as mkdirSync27, rmSync as rmSync13, writeFileSync as writeFileSync29 } from "node:fs";
224591
224889
  import { dirname as dirname21, join as join47 } from "node:path";
224592
224890
  init_paths();
224593
224891
  init_types4();
@@ -224618,7 +224916,7 @@ var pull_default = defineCommand({
224618
224916
  if (remotePaths.has(path))
224619
224917
  continue;
224620
224918
  const abs = join47(root, ...path.replace(/^\//, "").split("/"));
224621
- if (existsSync42(abs))
224919
+ if (existsSync43(abs))
224622
224920
  rmSync13(abs);
224623
224921
  }
224624
224922
  const files = await materializeAssets(root, project, apiUrl, token, progress);
@@ -224679,8 +224977,8 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
224679
224977
  };
224680
224978
 
224681
224979
  // 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";
224980
+ import { mkdirSync as mkdirSync29, readFileSync as readFileSync36, writeFileSync as writeFileSync31 } from "node:fs";
224981
+ import { dirname as dirname22, resolve as resolve28 } from "node:path";
224684
224982
 
224685
224983
  // src/compile/headlessBundle.ts
224686
224984
  init_api();
@@ -224767,22 +225065,22 @@ var compileHeadlessBundle = async (opts = {}) => {
224767
225065
  init_designMeta();
224768
225066
  init_cli();
224769
225067
  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";
225068
+ import { existsSync as existsSync44, readFileSync as readFileSync35, statSync as statSync17 } from "node:fs";
225069
+ import { isAbsolute, join as join48, relative as relative9, resolve as resolve25 } from "node:path";
224772
225070
  var SCREEN_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs"];
224773
225071
  var SCREEN_ENTRY_PATH = "/.screen-entry.ts";
224774
225072
  var DEFAULT_DESIGN_DIR = "design";
224775
225073
  var hasSeparator = (file) => file.includes("/") || file.includes("\\");
224776
225074
  var findScreenFile = (root, file, dirFlag) => {
224777
- const candidates = [resolve24(process.cwd(), file)];
225075
+ const candidates = [resolve25(process.cwd(), file)];
224778
225076
  if (!hasSeparator(file)) {
224779
225077
  candidates.push(join48(root, (dirFlag ?? DEFAULT_DESIGN_DIR).replace(/[/\\]+$/, ""), "screens", file));
224780
225078
  }
224781
225079
  for (const base of candidates) {
224782
- if (existsSync43(base) && statSync17(base).isFile())
225080
+ if (existsSync44(base) && statSync17(base).isFile())
224783
225081
  return base;
224784
225082
  for (const ext of SCREEN_EXTS)
224785
- if (existsSync43(base + ext))
225083
+ if (existsSync44(base + ext))
224786
225084
  return base + ext;
224787
225085
  }
224788
225086
  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 +225090,7 @@ var designDirOf = (root, key) => {
224792
225090
  if (!m)
224793
225091
  return null;
224794
225092
  const dir = join48(root, m[1]);
224795
- return existsSync43(join48(dir, "meta.json")) ? { dir, id: m[2] } : null;
225093
+ return existsSync44(join48(dir, "meta.json")) ? { dir, id: m[2] } : null;
224796
225094
  };
224797
225095
  var resolveScreenTarget = (root, file, opts = {}) => {
224798
225096
  const abs = findScreenFile(root, file, opts.dir);
@@ -224801,7 +225099,7 @@ var resolveScreenTarget = (root, file, opts = {}) => {
224801
225099
  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
225100
  }
224803
225101
  const key = toKey(rel);
224804
- if (!/^\s*export\s+default\b/m.test(readFileSync34(abs, "utf8"))) {
225102
+ if (!/^\s*export\s+default\b/m.test(readFileSync35(abs, "utf8"))) {
224805
225103
  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
225104
  }
224807
225105
  const invoke = opts.state !== undefined ? `screen(${JSON.stringify(opts.state)})` : "screen()";
@@ -224837,9 +225135,9 @@ init_manifest();
224837
225135
 
224838
225136
  // src/hosts/flowPick.ts
224839
225137
  init_cli();
224840
- import { existsSync as existsSync44, mkdirSync as mkdirSync28, mkdtempSync as mkdtempSync6, rmSync as rmSync14, writeFileSync as writeFileSync30 } from "node:fs";
225138
+ import { existsSync as existsSync45, mkdirSync as mkdirSync28, mkdtempSync as mkdtempSync6, rmSync as rmSync14, writeFileSync as writeFileSync30 } from "node:fs";
224841
225139
  import { tmpdir as tmpdir6 } from "node:os";
224842
- import { join as join49, resolve as resolve25 } from "node:path";
225140
+ import { join as join49, resolve as resolve26 } from "node:path";
224843
225141
  import { deflateSync as deflateSync2 } from "node:zlib";
224844
225142
  var MAX_SIDE = 4096;
224845
225143
  var crcTable = (() => {
@@ -224908,8 +225206,8 @@ var write = (name, bytes) => {
224908
225206
  };
224909
225207
  var fileOf = (at, value, baseDir) => {
224910
225208
  if (typeof value === "string") {
224911
- const path = resolve25(baseDir, value);
224912
- if (!existsSync44(path))
225209
+ const path = resolve26(baseDir, value);
225210
+ if (!existsSync45(path))
224913
225211
  throw new CliError(`${at}: "pick": no file "${value}" (looked for ${path}).`);
224914
225212
  return path;
224915
225213
  }
@@ -224949,7 +225247,7 @@ var resolvePicks = (steps, baseDir) => {
224949
225247
 
224950
225248
  // src/hosts/desktopScript.ts
224951
225249
  init_cli();
224952
- import { resolve as resolve26 } from "node:path";
225250
+ import { resolve as resolve27 } from "node:path";
224953
225251
 
224954
225252
  // ../../runtime/headless/src/headless/paintDump.ts
224955
225253
  var FLAG_INTERACTIVE = 1;
@@ -225132,7 +225430,7 @@ var FRAME_MS = 16;
225132
225430
  var SETTLE_FRAMES = 640;
225133
225431
  var ACTIONS = ["tap", "type", "fill", "scroll", "wait", "frames", "drag", "hold", "hover", "key", "keyDown", "keyUp", "look", "mouseTo", "wheel", "stick", "gamepad", "camera", "view"];
225134
225432
  var SEMANTIC_EXPECT = ["text", "notText", "node", "notNode", "screen", "value"];
225135
- var UNSUPPORTED_EXPECT = ["audio"];
225433
+ var UNSUPPORTED_EXPECT = ["audio", "toast"];
225136
225434
  var VIEW_NAMES = ["iso", "top", "front", "side", "back"];
225137
225435
  var isVec3 = (v) => Array.isArray(v) && v.length === 3 && v.every((n) => typeof n === "number");
225138
225436
  var pt = (p) => typeof p === "string" ? p : `(${p.x},${p.y})`;
@@ -225394,7 +225692,7 @@ var translateScenario = (rawSteps, opts) => {
225394
225692
  }
225395
225693
  }
225396
225694
  if (needDump) {
225397
- plan.paintDump = resolve26(opts.outDir, `.dump-${i}.json`);
225695
+ plan.paintDump = resolve27(opts.outDir, `.dump-${i}.json`);
225398
225696
  plan.lines.push(emit(`frames 1`));
225399
225697
  plan.lines.push(emit(`dump ${plan.paintDump}`));
225400
225698
  budgetMs += 2 * FRAME_MS;
@@ -225409,7 +225707,7 @@ var translateScenario = (rawSteps, opts) => {
225409
225707
  throw new CliError(`${at}: "screenshot" must be a path.`);
225410
225708
  if (opts.sim)
225411
225709
  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);
225710
+ plan.screenshot = resolve27(opts.outDir, s.screenshot);
225413
225711
  const clip = s.clip;
225414
225712
  if (clip !== undefined) {
225415
225713
  if (typeof clip === "string") {
@@ -225435,7 +225733,7 @@ var translateScenario = (rawSteps, opts) => {
225435
225733
  if (s.dump !== undefined) {
225436
225734
  if (typeof s.dump !== "string")
225437
225735
  throw new CliError(`${at}: "dump" must be a path.`);
225438
- plan.dump = resolve26(opts.outDir, s.dump);
225736
+ plan.dump = resolve27(opts.outDir, s.dump);
225439
225737
  plan.lines.push(emit(`frames 1`));
225440
225738
  plan.lines.push(emit(`dump ${plan.dump}`));
225441
225739
  budgetMs += 2 * FRAME_MS;
@@ -225480,7 +225778,7 @@ var evaluateDesktopRun = (plan, output, readFile = () => null) => {
225480
225778
  let ok = true;
225481
225779
  for (const st of plan.steps) {
225482
225780
  const skipped = [
225483
- ...st.skippedExpect.map((k) => `expect.${k} (the headless audio recorder's — not on a native host)`),
225781
+ ...st.skippedExpect.map((k) => `expect.${k} (the headless renderer's record — not on a native host)`),
225484
225782
  ...st.skippedOutputs.map((k) => `${k} output (the headless renderer's shape — use "dump" on a native host)`)
225485
225783
  ];
225486
225784
  let error;
@@ -225586,7 +225884,7 @@ var parseNativeViews = (raw) => {
225586
225884
  var requireRenderer = async () => loadPeer("lecodes-headless", "headless", { for: "lecodes render" });
225587
225885
  var fileScenarioIO = (dir) => ({
225588
225886
  write: (relPath, data) => {
225589
- const abs = resolve27(dir, relPath);
225887
+ const abs = resolve28(dir, relPath);
225590
225888
  mkdirSync29(dirname22(abs), { recursive: true });
225591
225889
  writeFileSync31(abs, data);
225592
225890
  }
@@ -225596,9 +225894,9 @@ var resolveFixtures = (value, baseDir) => {
225596
225894
  return;
225597
225895
  let map = value;
225598
225896
  if (typeof value === "string") {
225599
- const abs = resolve27(baseDir, value);
225897
+ const abs = resolve28(baseDir, value);
225600
225898
  try {
225601
- map = JSON.parse(readFileSync35(abs, "utf8"));
225899
+ map = JSON.parse(readFileSync36(abs, "utf8"));
225602
225900
  } catch (e) {
225603
225901
  throw new CliError(`Can't read fixtures "${value}": ${e instanceof Error ? e.message : String(e)}`);
225604
225902
  }
@@ -225611,18 +225909,18 @@ var resolveFixtures = (value, baseDir) => {
225611
225909
  var readScenarioFile = (path) => {
225612
225910
  let raw;
225613
225911
  try {
225614
- raw = JSON.parse(readFileSync35(resolve27(path), "utf8"));
225912
+ raw = JSON.parse(readFileSync36(resolve28(path), "utf8"));
225615
225913
  } catch (e) {
225616
225914
  throw new CliError(`Can't read scenario "${path}": ${e instanceof Error ? e.message : String(e)}`);
225617
225915
  }
225618
225916
  if (Array.isArray(raw))
225619
- return { steps: resolvePicks(raw, dirname22(resolve27(path))) };
225917
+ return { steps: resolvePicks(raw, dirname22(resolve28(path))) };
225620
225918
  if (typeof raw !== "object" || raw === null)
225621
225919
  throw new CliError(`Scenario "${path}" must be a JSON object with "steps" (or a bare step array).`);
225622
225920
  const file = raw;
225623
225921
  if (file.steps === undefined)
225624
225922
  throw new CliError(`Scenario "${path}" has no "steps".`);
225625
- return { ...file, steps: resolvePicks(file.steps, dirname22(resolve27(path))) };
225923
+ return { ...file, steps: resolvePicks(file.steps, dirname22(resolve28(path))) };
225626
225924
  };
225627
225925
  var parseClip = (raw) => {
225628
225926
  if (raw === undefined)
@@ -225747,7 +226045,7 @@ var renderDesktop = async (flags, file) => {
225747
226045
  entryOverride: screen?.entryPath
225748
226046
  });
225749
226047
  if (!sim)
225750
- mkdirSync29(dirname22(resolve27(pngFlag)), { recursive: true });
226048
+ mkdirSync29(dirname22(resolve28(pngFlag)), { recursive: true });
225751
226049
  if (scenario) {
225752
226050
  const outDir = flags["out-dir"];
225753
226051
  const plan = translateScenario(scenario.steps, { outDir, headroomFrames: frames, sim });
@@ -225843,7 +226141,7 @@ var renderHeadless = async (flags, file) => {
225843
226141
  const width = flags.width || scenario?.width || screen?.frame?.[0] || projectViewport?.width || 390;
225844
226142
  const height = flags.height || scenario?.height || screen?.frame?.[1] || projectViewport?.height || 844;
225845
226143
  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)) : ".");
226144
+ const envelopeFixtures = resolveFixtures(scenario?.fixtures, flags.script !== undefined ? dirname22(resolve28(flags.script)) : ".");
225847
226145
  const flagFixtures = resolveFixtures(flags.fixtures, ".");
225848
226146
  const fixtures = envelopeFixtures || flagFixtures ? { ...envelopeFixtures, ...flagFixtures } : undefined;
225849
226147
  if (fixtures) {
@@ -225884,7 +226182,7 @@ var renderHeadless = async (flags, file) => {
225884
226182
  ${c.red(st.error ?? "failed")}`);
225885
226183
  }
225886
226184
  if (!result.ok && result.failureArtifacts.length > 0) {
225887
- logErr(c.dim(` failing frame: ${result.failureArtifacts.map((p) => resolve27(outDir, p)).join(", ")}`));
226185
+ logErr(c.dim(` failing frame: ${result.failureArtifacts.map((p) => resolve28(outDir, p)).join(", ")}`));
225888
226186
  }
225889
226187
  logErr(result.ok ? c.green(`✓ ${result.steps.length}/${steps.length} steps passed`) : c.red(`✗ failed at step ${result.steps.length} of ${steps.length}`));
225890
226188
  return done((cb) => cb(), result.ok ? 0 : 1);
@@ -225905,10 +226203,10 @@ var renderHeadless = async (flags, file) => {
225905
226203
  for (const w of [...warnings, ...mapWarnings])
225906
226204
  warnErr(w);
225907
226205
  const pngPath = mapFlag.endsWith(".png") ? mapFlag : mapFlag + ".png";
225908
- mkdirSync29(dirname22(resolve27(pngPath)), { recursive: true });
225909
- writeFileSync31(resolve27(pngPath), png);
226206
+ mkdirSync29(dirname22(resolve28(pngPath)), { recursive: true });
226207
+ writeFileSync31(resolve28(pngPath), png);
225910
226208
  const jsonPath = pngPath.slice(0, -4) + ".json";
225911
- writeFileSync31(resolve27(jsonPath), JSON.stringify(data, null, 2));
226209
+ writeFileSync31(resolve28(jsonPath), JSON.stringify(data, null, 2));
225912
226210
  note(`Wrote ${pngPath} + ${jsonPath}`);
225913
226211
  return done((cb) => cb());
225914
226212
  }
@@ -225928,8 +226226,8 @@ var renderHeadless = async (flags, file) => {
225928
226226
  if (isNullSink(pngFlag)) {
225929
226227
  note(`Discarded PNG (${pngFlag})`);
225930
226228
  } else {
225931
- mkdirSync29(dirname22(resolve27(pngFlag)), { recursive: true });
225932
- writeFileSync31(resolve27(pngFlag), png);
226229
+ mkdirSync29(dirname22(resolve28(pngFlag)), { recursive: true });
226230
+ writeFileSync31(resolve28(pngFlag), png);
225933
226231
  note(`Wrote ${pngFlag} (${png.length} bytes, ${outW}x${outH}${clip !== undefined ? ", clipped" : ""})`);
225934
226232
  }
225935
226233
  return done((cb) => cb());
@@ -225944,7 +226242,7 @@ var renderHeadless = async (flags, file) => {
225944
226242
  return done((cb) => cb());
225945
226243
  }
225946
226244
  if (out) {
225947
- writeFileSync31(resolve27(out), result.text);
226245
+ writeFileSync31(resolve28(out), result.text);
225948
226246
  note(`Wrote ${out} (${result.text.length} bytes)`);
225949
226247
  return done((cb) => cb());
225950
226248
  }
@@ -225956,8 +226254,8 @@ var renderHeadless = async (flags, file) => {
225956
226254
 
225957
226255
  // src/commands/scene.ts
225958
226256
  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";
226257
+ import { existsSync as existsSync47, readFileSync as readFileSync38, readdirSync as readdirSync19, statSync as statSync18, writeFileSync as writeFileSync33 } from "node:fs";
226258
+ import { basename as basename24, dirname as dirname23, join as join51, relative as relative10, resolve as resolve29 } from "node:path";
225961
226259
 
225962
226260
  // src/compile/sceneCompile.ts
225963
226261
  init_localFiles();
@@ -225965,7 +226263,7 @@ init_ignore();
225965
226263
  init_cli();
225966
226264
  init_projectCompile();
225967
226265
  import { createHash as createHash8 } from "node:crypto";
225968
- import { existsSync as existsSync45, mkdirSync as mkdirSync30, readFileSync as readFileSync36, writeFileSync as writeFileSync32 } from "node:fs";
226266
+ import { existsSync as existsSync46, mkdirSync as mkdirSync30, readFileSync as readFileSync37, writeFileSync as writeFileSync32 } from "node:fs";
225969
226267
  import { join as join50 } from "node:path";
225970
226268
  var TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i;
225971
226269
  var rel = (key) => key.replace(/^\//, "");
@@ -225977,9 +226275,9 @@ var compileSceneEditor = async (opts) => {
225977
226275
  for (const file of scan.files) {
225978
226276
  const path = "./" + rel(file.path);
225979
226277
  if (TEXT_EXT.test(path)) {
225980
- entries.push({ path, type: "text", text: readFileSync36(file.absPath, "utf8") });
226278
+ entries.push({ path, type: "text", text: readFileSync37(file.absPath, "utf8") });
225981
226279
  } else if (/\.mat$/i.test(path)) {
225982
- entries.push({ path, type: "shader", text: readFileSync36(file.absPath, "utf8"), absPath: file.absPath });
226280
+ entries.push({ path, type: "shader", text: readFileSync37(file.absPath, "utf8"), absPath: file.absPath });
225983
226281
  } else {
225984
226282
  entries.push({ path, type: "resource", fileSrc: "/project/" + rel(file.path) });
225985
226283
  }
@@ -225997,7 +226295,7 @@ var compileSceneEditor = async (opts) => {
225997
226295
  const dir = shadersCacheDir(root);
225998
226296
  mkdirSync30(dir, { recursive: true });
225999
226297
  const liteName = `${hash}_webgl-lite.filamat`;
226000
- if (!existsSync45(join50(dir, liteName)))
226298
+ if (!existsSync46(join50(dir, liteName)))
226001
226299
  writeFileSync32(join50(dir, liteName), JSON.stringify(buildLiteMaterialArtifact(e.text ?? "")));
226002
226300
  const src = `/project/.lecodes/shaders/${hash}_opengl.filamat`;
226003
226301
  e.shaders = ["opengl", "webgl-lite"].map((platform) => ({ platform, src }));
@@ -226035,7 +226333,7 @@ var DEFAULT_PORT3 = 4488;
226035
226333
  var findAppRoot = (start) => {
226036
226334
  let dir = start;
226037
226335
  while (true) {
226038
- if (existsSync46(join51(dir, "app.json")))
226336
+ if (existsSync47(join51(dir, "app.json")))
226039
226337
  return dir;
226040
226338
  const parent = dirname23(dir);
226041
226339
  if (parent === dir)
@@ -226060,10 +226358,10 @@ var getContext2 = (pathArg) => {
226060
226358
  root = process.cwd();
226061
226359
  manifest = null;
226062
226360
  if (pathArg) {
226063
- const abs = resolve28(process.cwd(), pathArg);
226064
- if (existsSync46(abs) && statSync18(abs).isDirectory())
226361
+ const abs = resolve29(process.cwd(), pathArg);
226362
+ if (existsSync47(abs) && statSync18(abs).isDirectory())
226065
226363
  root = abs;
226066
- else if (abs.endsWith(".scene.ts") && existsSync46(abs))
226364
+ else if (abs.endsWith(".scene.ts") && existsSync47(abs))
226067
226365
  root = findAppRoot(dirname23(abs)) ?? dirname23(abs);
226068
226366
  else
226069
226367
  throw new CliError(`No such folder or .scene.ts: ${pathArg}`);
@@ -226073,9 +226371,9 @@ var getContext2 = (pathArg) => {
226073
226371
  }
226074
226372
  let scene = null;
226075
226373
  if (pathArg?.endsWith(".scene.ts")) {
226076
- const abs = resolve28(process.cwd(), pathArg);
226374
+ const abs = resolve29(process.cwd(), pathArg);
226077
226375
  const rel = relative10(root, abs).replace(/\\/g, "/");
226078
- if (existsSync46(abs) && !rel.startsWith(".."))
226376
+ if (existsSync47(abs) && !rel.startsWith(".."))
226079
226377
  scene = "./" + rel;
226080
226378
  }
226081
226379
  return { root, manifest, name: manifest?.name ?? basename24(root), scene };
@@ -226084,9 +226382,9 @@ var writeMcpConfig2 = (root, port) => {
226084
226382
  const path = join51(root, ".mcp.json");
226085
226383
  const entry = { type: "http", url: `http://localhost:${port}/mcp` };
226086
226384
  let config = {};
226087
- if (existsSync46(path)) {
226385
+ if (existsSync47(path)) {
226088
226386
  try {
226089
- config = JSON.parse(readFileSync37(path, "utf8"));
226387
+ config = JSON.parse(readFileSync38(path, "utf8"));
226090
226388
  } catch {
226091
226389
  warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-scene server by hand.");
226092
226390
  return;
@@ -226201,7 +226499,7 @@ var serve_default = defineCommand({
226201
226499
  });
226202
226500
 
226203
226501
  // src/commands/shaders/index.ts
226204
- import { readFileSync as readFileSync39 } from "node:fs";
226502
+ import { readFileSync as readFileSync40 } from "node:fs";
226205
226503
  import { relative as relative12 } from "node:path";
226206
226504
  init_cli();
226207
226505
  init_shared();
@@ -226210,15 +226508,15 @@ init_shared();
226210
226508
  init_compile();
226211
226509
  init_distRoot();
226212
226510
  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";
226511
+ import { existsSync as existsSync48, mkdirSync as mkdirSync31, readdirSync as readdirSync20, readFileSync as readFileSync39, writeFileSync as writeFileSync34 } from "node:fs";
226512
+ import { basename as basename26, join as join52, relative as relative11, resolve as resolve30 } from "node:path";
226215
226513
  var engineMaterialsDir = () => {
226216
226514
  const candidates = [
226217
226515
  join52(distRoot, "runtime", "materials"),
226218
226516
  join52(distRoot, "..", "..", "engines", "gl", "materials", "src")
226219
226517
  ];
226220
226518
  for (const c of candidates)
226221
- if (existsSync47(join52(c, "unlit.mat")))
226519
+ if (existsSync48(join52(c, "unlit.mat")))
226222
226520
  return c;
226223
226521
  return null;
226224
226522
  };
@@ -226300,11 +226598,11 @@ var writeNewShader = (o) => {
226300
226598
  const name = o.name.endsWith(".mat") ? o.name.slice(0, -4) : o.name;
226301
226599
  if (!NAME_RE.test(name))
226302
226600
  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 ?? ".");
226601
+ const dir = resolve30(o.cwd ?? process.cwd(), o.dir ?? ".");
226304
226602
  const matPath = join52(dir, `${name}.mat`), assetPath = join52(dir, `${name}.material.ts`);
226305
226603
  if (!o.force) {
226306
226604
  for (const p of [matPath, assetPath])
226307
- if (existsSync47(p))
226605
+ if (existsSync48(p))
226308
226606
  throw new CliError(`${relative11(o.cwd ?? process.cwd(), p) || basename26(p)} exists — pick another name or pass --force to overwrite`);
226309
226607
  }
226310
226608
  let mat;
@@ -226317,7 +226615,7 @@ var writeNewShader = (o) => {
226317
226615
  const known = engineMaterialNames();
226318
226616
  if (!known.includes(ref))
226319
226617
  throw new CliError(`no engine material "${ref}". Known: ${known.join(", ")}`);
226320
- mat = refMat(name, ref, readFileSync38(join52(srcDir, `${ref}.mat`), "utf8"));
226618
+ mat = refMat(name, ref, readFileSync39(join52(srcDir, `${ref}.mat`), "utf8"));
226321
226619
  } else
226322
226620
  mat = starterMat(name);
226323
226621
  mkdirSync31(dir, { recursive: true });
@@ -226349,7 +226647,7 @@ var status2 = async () => {
226349
226647
  if (matcId === null)
226350
226648
  state = c.dim("unknown (matc not downloaded)");
226351
226649
  else {
226352
- const text = readFileSync39(shader.absPath, "utf8");
226650
+ const text = readFileSync40(shader.absPath, "utf8");
226353
226651
  if (shaderIsCompiled(root, text, matcId))
226354
226652
  state = "compiled";
226355
226653
  else {
@@ -226358,7 +226656,7 @@ var status2 = async () => {
226358
226656
  }
226359
226657
  }
226360
226658
  log(` ${shader.path.padEnd(width)} ${state}`);
226361
- const schema = parseShaderSchema(readFileSync39(shader.absPath, "utf8"));
226659
+ const schema = parseShaderSchema(readFileSync40(shader.absPath, "utf8"));
226362
226660
  for (const p of schema.params) {
226363
226661
  const range = p.min !== undefined ? ` [${p.min}..${p.max}]` : "";
226364
226662
  const def = p.default !== null && p.default !== undefined ? ` = ${JSON.stringify(p.default)}` : "";
@@ -226399,7 +226697,7 @@ var compileShaders = async (platforms) => {
226399
226697
  }
226400
226698
  const matcExe = await resolveMatcExe();
226401
226699
  const matcId = matcIdentity(matcExe);
226402
- const wasCompiled = new Map(shaders.map((s) => [s.path, shaderIsCompiled(root, readFileSync39(s.absPath, "utf8"), matcId)]));
226700
+ const wasCompiled = new Map(shaders.map((s) => [s.path, shaderIsCompiled(root, readFileSync40(s.absPath, "utf8"), matcId)]));
226403
226701
  const errors = [];
226404
226702
  for (const shader of shaders) {
226405
226703
  try {
@@ -226501,14 +226799,14 @@ var status_default = defineCommand({
226501
226799
  });
226502
226800
 
226503
226801
  // 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";
226802
+ import { existsSync as existsSync50, mkdirSync as mkdirSync33, readFileSync as readFileSync41, readdirSync as readdirSync21 } from "node:fs";
226803
+ import { dirname as dirname25, resolve as resolve31 } from "node:path";
226506
226804
  init_manifest();
226507
226805
 
226508
226806
  // src/hosts/liteRunner.ts
226509
226807
  init_cli();
226510
226808
  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";
226809
+ import { existsSync as existsSync49, mkdirSync as mkdirSync32, rmSync as rmSync15, writeFileSync as writeFileSync35 } from "node:fs";
226512
226810
  import { createServer as createServer3 } from "node:net";
226513
226811
  import { dirname as dirname24, join as join53 } from "node:path";
226514
226812
  import { createInterface as createInterface2 } from "node:readline";
@@ -226521,7 +226819,7 @@ var NO_NODE = "--lite needs Node on the PATH: the browser is driven from a Node
226521
226819
  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
226820
  var startDriver = async () => {
226523
226821
  const script = join53(distRoot, "src", "hosts", "liteBrowser.mjs");
226524
- if (!existsSync48(script))
226822
+ if (!existsSync49(script))
226525
226823
  throw new CliError("--lite is not available in this build of the CLI (its browser driver ships with the npm package).");
226526
226824
  const child = spawn7("node", [script], { stdio: ["pipe", "pipe", "inherit"] });
226527
226825
  const waiting = new Map;
@@ -227018,7 +227316,7 @@ var slug = (s) => s.replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "").sli
227018
227316
  var readFlowFile = (abs, displayName) => {
227019
227317
  let raw;
227020
227318
  try {
227021
- raw = JSON.parse(readFileSync40(abs, "utf8"));
227319
+ raw = JSON.parse(readFileSync41(abs, "utf8"));
227022
227320
  } catch (e) {
227023
227321
  throw new CliError(`Can't read "${displayName}": ${e instanceof Error ? e.message : String(e)}`);
227024
227322
  }
@@ -227112,39 +227410,39 @@ var runTests = async (args, flags) => {
227112
227410
  if (nativeFixedDt < 0 || nativeFixedDt > 1000)
227113
227411
  throw new CliError(`--fixed-dt wants a number of ms (0 = wall clock, max 1000), got "${nativeFixedDt}".`);
227114
227412
  const projectViewport = desktopDefaultViewport(root);
227115
- const testsDir = resolve30(root, flags.dir);
227116
- const designMetaPath = resolve30(root, flags["design-dir"], "meta.json");
227413
+ const testsDir = resolve31(root, flags.dir);
227414
+ const designMetaPath = resolve31(root, flags["design-dir"], "meta.json");
227117
227415
  const asJson = flags.json;
227118
227416
  const out = asJson ? logErr : log;
227119
227417
  const cases = [];
227120
227418
  if (args.length > 0) {
227121
227419
  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));
227420
+ const candidates = [resolve31(given), resolve31(testsDir, given), resolve31(testsDir, `${given}.flow.json`)];
227421
+ const abs = candidates.find((p) => existsSync50(p));
227124
227422
  if (!abs)
227125
227423
  throw new CliError(`No such test: "${given}" (looked in ${testsDir}).`);
227126
227424
  cases.push(readFlowFile(abs, given));
227127
227425
  }
227128
- } else if (existsSync49(testsDir)) {
227426
+ } else if (existsSync50(testsDir)) {
227129
227427
  for (const f of readdirSync21(testsDir).filter((f) => f.endsWith(".flow.json")).sort()) {
227130
- cases.push(readFlowFile(resolve30(testsDir, f), f));
227428
+ cases.push(readFlowFile(resolve31(testsDir, f), f));
227131
227429
  }
227132
227430
  }
227133
- const defaultPack = resolve30(testsDir, "fixtures.json");
227134
- const flowFixtures = flags.fixtures !== undefined ? resolveFixtures(flags.fixtures, ".") : existsSync49(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
227431
+ const defaultPack = resolve31(testsDir, "fixtures.json");
227432
+ const flowFixtures = flags.fixtures !== undefined ? resolveFixtures(flags.fixtures, ".") : existsSync50(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
227135
227433
  const flowsOnly = flags.flows;
227136
227434
  const wantFlows = !flags["no-flows"] && args.length === 0;
227137
227435
  if (flowsOnly)
227138
227436
  cases.length = 0;
227139
227437
  let skipped = [];
227140
227438
  let entryScreen = null;
227141
- if ((desktop || lite) && wantFlows && existsSync49(designMetaPath)) {
227439
+ if ((desktop || lite) && wantFlows && existsSync50(designMetaPath)) {
227142
227440
  note("Design-derived flows skipped with --desktop / --lite (they read expect.screen and fill inputs; run `lecodes test` headless for them).");
227143
227441
  }
227144
- if (!desktop && !lite && (wantFlows || flowsOnly) && existsSync49(designMetaPath)) {
227442
+ if (!desktop && !lite && (wantFlows || flowsOnly) && existsSync50(designMetaPath)) {
227145
227443
  let meta;
227146
227444
  try {
227147
- meta = JSON.parse(readFileSync40(designMetaPath, "utf8"));
227445
+ meta = JSON.parse(readFileSync41(designMetaPath, "utf8"));
227148
227446
  } catch (e) {
227149
227447
  throw new CliError(`Can't read ${designMetaPath}: ${e instanceof Error ? e.message : String(e)}`);
227150
227448
  }
@@ -227177,7 +227475,7 @@ var runTests = async (args, flags) => {
227177
227475
  }, timeoutMs);
227178
227476
  killer.unref?.();
227179
227477
  const onConsole = flags.logs ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
227180
- const artifactsRoot = resolve30(root, flags.artifacts ?? resolve30(testsDir, ".artifacts"));
227478
+ const artifactsRoot = resolve31(root, flags.artifacts ?? resolve31(testsDir, ".artifacts"));
227181
227479
  const results = [];
227182
227480
  const reportCase = (result) => {
227183
227481
  const skippedCount = result.steps.reduce((n, s) => n + (s.skipped?.length ?? 0), 0);
@@ -227197,13 +227495,13 @@ var runTests = async (args, flags) => {
227197
227495
  }
227198
227496
  };
227199
227497
  const runNativeCase = async (kase) => {
227200
- const caseDir = resolve30(artifactsRoot, slug(kase.name));
227498
+ const caseDir = resolve31(artifactsRoot, slug(kase.name));
227201
227499
  const plan = translateScenario(kase.steps, { outDir: caseDir, headroomFrames: nativeFrames, sim });
227202
227500
  for (const st of plan.steps)
227203
227501
  if (st.screenshot)
227204
227502
  mkdirSync33(dirname25(st.screenshot), { recursive: true });
227205
227503
  mkdirSync33(caseDir, { recursive: true });
227206
- const finalPng = resolve30(caseDir, "final.png");
227504
+ const finalPng = resolve31(caseDir, "final.png");
227207
227505
  const { output } = await runDesktopRender(native.js, native.resources, {
227208
227506
  renderer: desktopRenderer,
227209
227507
  host: sim ? "server" : "desktop",
@@ -227217,7 +227515,7 @@ var runTests = async (args, flags) => {
227217
227515
  script: plan.text,
227218
227516
  fixedDtMs: nativeFixedDt
227219
227517
  });
227220
- const r = evaluateDesktopRun(plan, output, (path) => existsSync49(path) ? readFileSync40(path, "utf8") : null);
227518
+ const r = evaluateDesktopRun(plan, output, (path) => existsSync50(path) ? readFileSync41(path, "utf8") : null);
227221
227519
  if (!r.loaded)
227222
227520
  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
227521
  return {
@@ -227248,7 +227546,7 @@ var runTests = async (args, flags) => {
227248
227546
  }
227249
227547
  }
227250
227548
  const runLiteCase = async (kase) => {
227251
- const caseDir = resolve30(artifactsRoot, slug(kase.name));
227549
+ const caseDir = resolve31(artifactsRoot, slug(kase.name));
227252
227550
  const plan = translateScenario(kase.steps, { outDir: caseDir, headroomFrames: 0 });
227253
227551
  mkdirSync33(caseDir, { recursive: true });
227254
227552
  const output = await liteSession.run(plan, {
@@ -227256,7 +227554,7 @@ var runTests = async (args, flags) => {
227256
227554
  height: flags.height || kase.height || projectViewport?.height || 844,
227257
227555
  onConsole
227258
227556
  });
227259
- const r = evaluateDesktopRun(plan, output, (path) => existsSync49(path) ? readFileSync40(path, "utf8") : null);
227557
+ const r = evaluateDesktopRun(plan, output, (path) => existsSync50(path) ? readFileSync41(path, "utf8") : null);
227260
227558
  return {
227261
227559
  name: kase.name,
227262
227560
  kind: kase.kind,
@@ -227307,14 +227605,14 @@ var runTests = async (args, flags) => {
227307
227605
  onConsole
227308
227606
  });
227309
227607
  try {
227310
- const io = fileScenarioIO(resolve30(artifactsRoot, slug(kase.name)));
227608
+ const io = fileScenarioIO(resolve31(artifactsRoot, slug(kase.name)));
227311
227609
  const outcome = await renderer.runScenario(session, steps, io);
227312
227610
  result = {
227313
227611
  name: kase.name,
227314
227612
  kind: kase.kind,
227315
227613
  ok: outcome.ok,
227316
227614
  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))
227615
+ artifacts: outcome.failureArtifacts.map((p) => resolve31(artifactsRoot, slug(kase.name), p))
227318
227616
  };
227319
227617
  } finally {
227320
227618
  session.dispose();
@@ -227365,9 +227663,9 @@ var runTests = async (args, flags) => {
227365
227663
  init_peerInstall();
227366
227664
  import { createRequire as createRequire8 } from "node:module";
227367
227665
  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";
227666
+ import { existsSync as existsSync51, mkdtempSync as mkdtempSync7, readdirSync as readdirSync22, rmSync as rmSync16, statSync as statSync19 } from "node:fs";
227369
227667
  import { tmpdir as tmpdir7 } from "node:os";
227370
- import { extname, join as join54, relative as relative13, resolve as resolve31 } from "node:path";
227668
+ import { extname, join as join54, relative as relative13, resolve as resolve32 } from "node:path";
227371
227669
  init_cli();
227372
227670
  init_paths();
227373
227671
  var DEFAULT_SIZE = 256;
@@ -227385,7 +227683,7 @@ var findKtx = () => {
227385
227683
  "/opt/homebrew/bin/ktx"
227386
227684
  ].filter((p) => !!p);
227387
227685
  for (const path of candidates) {
227388
- if (existsSync50(path))
227686
+ if (existsSync51(path))
227389
227687
  return path;
227390
227688
  }
227391
227689
  try {
@@ -227417,7 +227715,7 @@ var ktxToPng = (ktx, file, out) => {
227417
227715
  run(["extract", "--transcode", "rgba8", "--level", "0", file, out]);
227418
227716
  } catch (e) {
227419
227717
  run(["extract", "--level", "0", file, out]);
227420
- if (!existsSync50(out))
227718
+ if (!existsSync51(out))
227421
227719
  throw e;
227422
227720
  }
227423
227721
  };
@@ -227452,8 +227750,8 @@ var thumbs = async (targets, size, force) => {
227452
227750
  const rendered = [];
227453
227751
  if (targets.length > 0) {
227454
227752
  for (const arg of targets) {
227455
- const abs = resolve31(process.cwd(), arg);
227456
- if (!existsSync50(abs))
227753
+ const abs = resolve32(process.cwd(), arg);
227754
+ if (!existsSync51(abs))
227457
227755
  throw new CliError(`No such file or folder: ${arg}`);
227458
227756
  if (statSync19(abs).isDirectory())
227459
227757
  files.push(...collect(abs));
@@ -227532,9 +227830,9 @@ init_peerInstall();
227532
227830
  init_archives();
227533
227831
  init_packageManager();
227534
227832
  import { spawnSync as spawnSync11 } from "node:child_process";
227535
- import { existsSync as existsSync51, readdirSync as readdirSync23 } from "node:fs";
227833
+ import { existsSync as existsSync52, readdirSync as readdirSync23 } from "node:fs";
227536
227834
  import { homedir as homedir14 } from "node:os";
227537
- import { dirname as dirname26, join as join55, resolve as resolve33 } from "node:path";
227835
+ import { dirname as dirname26, join as join55, resolve as resolve34 } from "node:path";
227538
227836
 
227539
227837
  // src/commands/update/types.ts
227540
227838
  init_api();
@@ -227542,10 +227840,10 @@ init_config();
227542
227840
  init_manifest();
227543
227841
  init_types4();
227544
227842
  init_cli();
227545
- import { resolve as resolve32 } from "node:path";
227843
+ import { resolve as resolve33 } from "node:path";
227546
227844
  var label = (bundle) => `${Object.keys(bundle).length} file(s)`;
227547
227845
  var refreshTypes = async (opts) => {
227548
- const root = opts.dir ? resolve32(opts.dir) : (() => {
227846
+ const root = opts.dir ? resolve33(opts.dir) : (() => {
227549
227847
  try {
227550
227848
  return findProjectRoot(process.cwd());
227551
227849
  } catch {
@@ -227603,10 +227901,10 @@ var refreshTypes = async (opts) => {
227603
227901
 
227604
227902
  // src/commands/update/index.ts
227605
227903
  var findProjectDir = (start) => {
227606
- const home = resolve33(homedir14());
227607
- let dir = resolve33(start);
227904
+ const home = resolve34(homedir14());
227905
+ let dir = resolve34(start);
227608
227906
  while (true) {
227609
- if (dir !== home && (existsSync51(join55(dir, "app.json")) || existsSync51(join55(dir, ".lecodes", "manifest.json")) || existsSync51(join55(dir, ".lecodes", "types"))))
227907
+ if (dir !== home && (existsSync52(join55(dir, "app.json")) || existsSync52(join55(dir, ".lecodes", "manifest.json")) || existsSync52(join55(dir, ".lecodes", "types"))))
227610
227908
  return dir;
227611
227909
  const parent = dirname26(dir);
227612
227910
  if (parent === dir)
@@ -227643,7 +227941,7 @@ var semverNewer2 = (a, b) => {
227643
227941
  return 0;
227644
227942
  };
227645
227943
  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"));
227944
+ var STANDALONE = existsSync52(join55(dirname26(process.execPath), "runtime")) && existsSync52(join55(dirname26(process.execPath), process.platform === "win32" ? "chisel.exe" : "chisel"));
227647
227945
  var update_default = defineCommand({
227648
227946
  name: "update",
227649
227947
  summary: "Move everything the project depends on to its newest published state",
@@ -227703,7 +228001,7 @@ ${c.bold(name)} ${c.dim(what)}`);
227703
228001
  return ["skipped", "not inside a project"];
227704
228002
  if (flags["no-shells"])
227705
228003
  return ["skipped", "--no-shells"];
227706
- const present = ["ios", "android"].filter((d) => existsSync51(join55(root, d)) && readdirSync23(join55(root, d)).length > 0);
228004
+ const present = ["ios", "android"].filter((d) => existsSync52(join55(root, d)) && readdirSync23(join55(root, d)).length > 0);
227707
228005
  if (present.length === 0)
227708
228006
  return ["skipped", "no ios/ or android/ shell in this project (`lecodes app init ios|android`)"];
227709
228007
  await runSubcommand(app_default, "update");
@@ -227781,8 +228079,8 @@ init_cli();
227781
228079
 
227782
228080
  // src/web/build.ts
227783
228081
  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";
228082
+ 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";
228083
+ import { dirname as dirname27, extname as extname2, join as join56, resolve as resolve35 } from "node:path";
227786
228084
  init_fonts();
227787
228085
  init_assetIcons();
227788
228086
  init_projectCompile();
@@ -227806,8 +228104,8 @@ var walk = (dir, prefix = "") => {
227806
228104
  };
227807
228105
  var buildWeb = async (options) => {
227808
228106
  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 + "/")) {
228107
+ const out = resolve35(root, options.out);
228108
+ if (out === resolve35(root) || resolve35(root).startsWith(out + "\\") || resolve35(root).startsWith(out + "/")) {
227811
228109
  throw new CliError(`The output folder is cleaned before a build: "${options.out}" holds the project itself.`);
227812
228110
  }
227813
228111
  const config = readAppConfigAt(root)?.web ?? {};
@@ -227817,7 +228115,7 @@ var buildWeb = async (options) => {
227817
228115
  const assetUrl = (path) => "assets" + encodeURI(path);
227818
228116
  const collected = collectLocalEntries(root, manifest, "assets");
227819
228117
  const entries = collected.entries.filter((e) => {
227820
- const abs = e.absPath ? resolve34(e.absPath) : null;
228118
+ const abs = e.absPath ? resolve35(e.absPath) : null;
227821
228119
  return !(abs && (abs === out || abs.startsWith(out + "\\") || abs.startsWith(out + "/")));
227822
228120
  });
227823
228121
  for (const w of collected.warnings)
@@ -227859,8 +228157,8 @@ var buildWeb = async (options) => {
227859
228157
  mkdirSync34(out, { recursive: true });
227860
228158
  cpSync6(lite, out, { recursive: true });
227861
228159
  const table = join56(out, "features.json");
227862
- if (existsSync52(table)) {
227863
- const { features, fonts } = JSON.parse(readFileSync41(table, "utf8"));
228160
+ if (existsSync53(table)) {
228161
+ const { features, fonts } = JSON.parse(readFileSync42(table, "utf8"));
227864
228162
  if (config.font === "system")
227865
228163
  for (const file of fonts ?? [])
227866
228164
  rmSync17(join56(out, file), { force: true });
@@ -227873,9 +228171,9 @@ var buildWeb = async (options) => {
227873
228171
  rmSync17(table);
227874
228172
  }
227875
228173
  const pluginsTable = join56(out, "plugins.json");
227876
- if (existsSync52(pluginsTable)) {
228174
+ if (existsSync53(pluginsTable)) {
227877
228175
  const listed = new Set((/^\/\/ plugins: (.+)$/m.exec(header)?.[1] ?? "").split(";").map((id) => id.trim()).filter(Boolean));
227878
- const { plugins } = JSON.parse(readFileSync41(pluginsTable, "utf8"));
228176
+ const { plugins } = JSON.parse(readFileSync42(pluginsTable, "utf8"));
227879
228177
  const kept = {};
227880
228178
  for (const id of Object.keys(plugins)) {
227881
228179
  if (listed.has(id))
@@ -227901,7 +228199,7 @@ var buildWeb = async (options) => {
227901
228199
  if (!js.includes(entry.fileSrc))
227902
228200
  continue;
227903
228201
  const to = join56(out, "assets", ...entry.path.split("/").filter(Boolean));
227904
- if (existsSync52(to)) {
228202
+ if (existsSync53(to)) {
227905
228203
  warnings.push(`${entry.path}: a file of the page has this name in the built folder — the project's file was not copied`);
227906
228204
  continue;
227907
228205
  }
@@ -227909,12 +228207,12 @@ var buildWeb = async (options) => {
227909
228207
  copyFileSync2(entry.absPath, to);
227910
228208
  }
227911
228209
  const app = readAppConfigAt(root);
227912
- let html = readFileSync41(join56(lite, "index.html"), "utf8");
228210
+ let html = readFileSync42(join56(lite, "index.html"), "utf8");
227913
228211
  const head = [];
227914
228212
  head.push(`<meta name="theme-color" content="${escapeHtml(config.themeColor ?? "#000000")}" />`);
227915
228213
  if (app?.icon) {
227916
- const icon = resolve34(root, app.icon);
227917
- if (existsSync52(icon)) {
228214
+ const icon = resolve35(root, app.icon);
228215
+ if (existsSync53(icon)) {
227918
228216
  const file = `icon${extname2(icon).toLowerCase() || ".png"}`;
227919
228217
  copyFileSync2(icon, join56(out, file));
227920
228218
  head.push(`<link rel="icon" href="${file}" />`, `<link rel="apple-touch-icon" href="${file}" />`);
@@ -227949,12 +228247,12 @@ var buildWeb = async (options) => {
227949
228247
  };
227950
228248
 
227951
228249
  // src/web/preview.ts
227952
- import { createReadStream as createReadStream3, existsSync as existsSync53, statSync as statSync21 } from "node:fs";
228250
+ import { createReadStream as createReadStream3, existsSync as existsSync54, statSync as statSync21 } from "node:fs";
227953
228251
  import { createServer as createServer4 } from "node:http";
227954
- import { join as join57, resolve as resolve35 } from "node:path";
228252
+ import { join as join57, resolve as resolve36 } from "node:path";
227955
228253
  var startPreview = async (dir, port) => {
227956
- const root = resolve35(dir);
227957
- const fallback = existsSync53(join57(root, "404.html")) ? join57(root, "index.html") : null;
228254
+ const root = resolve36(dir);
228255
+ const fallback = existsSync54(join57(root, "404.html")) ? join57(root, "index.html") : null;
227958
228256
  const server2 = createServer4((req, res) => {
227959
228257
  let path;
227960
228258
  try {
@@ -227971,10 +228269,10 @@ var startPreview = async (dir, port) => {
227971
228269
  return;
227972
228270
  }
227973
228271
  let file = join57(root, ...parts);
227974
- if (existsSync53(file) && statSync21(file).isDirectory())
228272
+ if (existsSync54(file) && statSync21(file).isDirectory())
227975
228273
  file = join57(file, "index.html");
227976
228274
  let status = 200;
227977
- if (!existsSync53(file) || !statSync21(file).isFile()) {
228275
+ if (!existsSync54(file) || !statSync21(file).isFile()) {
227978
228276
  if (!fallback || /\.[a-z0-9]+$/i.test(parts[parts.length - 1] ?? "")) {
227979
228277
  res.writeHead(404, { "content-type": "text/plain" });
227980
228278
  res.end("Not found");
@@ -228164,7 +228462,7 @@ var web_default = defineCommand({
228164
228462
 
228165
228463
  // src/commands/index.ts
228166
228464
  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] },
228465
+ { 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
228466
  { title: "Run & test", commands: [
228169
228467
  dev_default,
228170
228468
  web_default,
@@ -228201,7 +228499,7 @@ init_peers();
228201
228499
  var STANDALONE2 = (() => {
228202
228500
  const dir = dirname28(process.execPath);
228203
228501
  const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
228204
- return existsSync55(join59(dir, "runtime")) && existsSync55(join59(dir, chisel));
228502
+ return existsSync56(join59(dir, "runtime")) && existsSync56(join59(dir, chisel));
228205
228503
  })();
228206
228504
  var INSTALLED = STANDALONE2 && (() => {
228207
228505
  const exe = process.platform === "win32" ? "lecodes.exe" : "lecodes";
@@ -228217,7 +228515,7 @@ var INSTALLED = STANDALONE2 && (() => {
228217
228515
  return false;
228218
228516
  try {
228219
228517
  const cand = join59(d, exe);
228220
- return existsSync55(cand) && realpathSync2(cand) === self2;
228518
+ return existsSync56(cand) && realpathSync2(cand) === self2;
228221
228519
  } catch {
228222
228520
  return false;
228223
228521
  }