lecodes-cli 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +3 -3
  2. package/dist/index.js +2941 -745
  3. package/package.json +4 -4
  4. package/runtime/scene-harness.json +1 -1
  5. package/runtime/sdk/canvas/Canvas.ts +16 -0
  6. package/runtime/sdk/gl/Material.ts +4 -0
  7. package/runtime/sdk/inject.ts +8 -5
  8. package/runtime/sdk/plugins/service.ts +47 -0
  9. package/runtime/sdk/runtime/app.ts +21 -1
  10. package/runtime/sdk/runtime/appEvents.ts +25 -0
  11. package/runtime/sdk/runtime/service.ts +3 -2
  12. package/runtime/sdk/ui/NativeView.ts +2 -1
  13. package/runtime/sdk/ui/UI.ts +3 -1
  14. package/runtime/sdk/ui/UIButton.ts +10 -5
  15. package/runtime/sdk/ui/UIContainer.ts +16 -9
  16. package/runtime/sdk/ui/UIImage.ts +5 -2
  17. package/runtime/sdk/ui/UIInput.ts +75 -8
  18. package/runtime/sdk/ui/UIModal.ts +149 -0
  19. package/runtime/sdk/ui/UINode.ts +126 -55
  20. package/runtime/sdk/ui/UIPager.ts +297 -0
  21. package/runtime/sdk/ui/UIPopover.ts +106 -0
  22. package/runtime/sdk/ui/UIScreen.ts +11 -10
  23. package/runtime/sdk/ui/UIScrollable.ts +2 -1
  24. package/runtime/sdk/ui/UISpacer.ts +1 -1
  25. package/runtime/sdk/ui/UIText.ts +2 -1
  26. package/runtime/sdk/ui/UIVideo.ts +2 -1
  27. package/runtime/sdk/ui/UIVirtualizedList.ts +2 -1
  28. package/runtime/sdk/ui/UIWidget.ts +19 -22
  29. package/runtime/sdk/ui/theme.ts +66 -0
  30. package/runtime/sdk-types.json +1 -1
  31. package/runtime/sdk/compile/aspectMacro.ts +0 -42
  32. package/runtime/sdk/compile/assetMacro.ts +0 -45
  33. package/runtime/sdk/compile/bundler.ts +0 -205
  34. package/runtime/sdk/compile/compileProject.ts +0 -108
  35. package/runtime/sdk/compile/header.ts +0 -46
  36. package/runtime/sdk/compile/index.ts +0 -18
  37. package/runtime/sdk/compile/libraryImports.ts +0 -47
  38. package/runtime/sdk/compile/sceneEditor.ts +0 -78
  39. package/runtime/sdk/compile/sourcemap.ts +0 -25
  40. package/runtime/sdk/kit/UITabs.ts +0 -105
  41. package/runtime/sdk/ui/UIScreenHost.ts +0 -213
package/dist/index.js CHANGED
@@ -109,12 +109,15 @@ var hasImportStatement = (src) => src.includes("import") || src.includes("from")
109
109
  if (isRelativeOrAbsolute(spec))
110
110
  return spec;
111
111
  const slash = spec.indexOf("/");
112
- const slug3 = slash === -1 ? spec : spec.slice(0, slash);
113
- if (slugs.has(slug3)) {
112
+ const slug = slash === -1 ? spec : spec.slice(0, slash);
113
+ if (slugs.has(slug)) {
114
114
  const sub = slash === -1 ? "/index" : spec.slice(slash);
115
- return `/__lib/${slug3}${sub}`;
115
+ return `/__lib/${slug}${sub}`;
116
116
  }
117
- throw new Error(`Unknown library "${slug3}" — add it in Settings → Libraries, or use a relative path like "./${slug3}".`);
117
+ if (slug === "ui" || slug === "creator" || slug === "utils") {
118
+ throw new Error(`import from "${slug}": the SDK needs no imports — UIScreen, Scene, etc. are globals; remove this import line.`);
119
+ }
120
+ throw new Error(`Unknown library "${slug}" — add it in Settings → Libraries, or use a relative path like "./${slug}".`);
118
121
  };
119
122
  let out = src.replace(IMPORT_FROM_RE, (_m, boundary, head, quote, spec) => `${boundary}${head}${quote}${rewriteSpec(spec)}${quote}`);
120
123
  out = out.replace(SIDE_EFFECT_RE, (m, boundary, head, quote, spec) => isRelativeOrAbsolute(spec) ? m : `${boundary}${head}${quote}${rewriteSpec(spec)}${quote}`);
@@ -126,18 +129,18 @@ var init_libraryImports = __esm(() => {
126
129
  });
127
130
 
128
131
  // ../sdk/src/compile/bundler.ts
129
- import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync6, statSync } from "node:fs";
132
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync7, statSync } from "node:fs";
130
133
  import { spawnSync } from "node:child_process";
131
134
  import { createRequire as createRequire2 } from "node:module";
132
- import { dirname as dirname5, join as join9, relative as relative2 } from "node:path";
135
+ import { dirname as dirname4, join as join7, relative as relative2 } from "node:path";
133
136
  import { fileURLToPath as fileURLToPath2 } from "node:url";
134
137
  var resolveInject = () => {
135
138
  const override = process.env.LECODES_SDK_INJECT;
136
- if (override && existsSync7(override))
139
+ if (override && existsSync4(override))
137
140
  return override;
138
- const here = dirname5(fileURLToPath2(import.meta.url));
139
- const src = join9(here, "..", "inject.ts");
140
- if (existsSync7(src))
141
+ const here = dirname4(fileURLToPath2(import.meta.url));
142
+ const src = join7(here, "..", "inject.ts");
143
+ if (existsSync4(src))
141
144
  return src;
142
145
  throw new Error("sdk: could not locate the inject source (LECODES_SDK_INJECT / src/inject.ts)");
143
146
  }, normalize = (path) => {
@@ -156,19 +159,19 @@ var resolveInject = () => {
156
159
  if (!inject.endsWith("inject.ts")) {
157
160
  throw new Error(`chisel needs the SDK source tree (src/inject.ts); got ${inject}. Set LECODES_SDK_INJECT to the source.`);
158
161
  }
159
- return dirname5(inject);
162
+ return dirname4(inject);
160
163
  }, isSdkSource = (path) => /\.[tj]s$/.test(path) && !path.endsWith(".d.ts") && !path.includes("/compile/") && !path.includes("/tests/") && !path.includes("/__tests__/"), sdkFilesCache, collectSdkFiles = (sdkSrc) => {
161
164
  const cached = sdkFilesCache.get(sdkSrc);
162
165
  if (cached)
163
166
  return cached;
164
167
  const out = {};
165
168
  const walk = (dir) => {
166
- for (const name of readdirSync4(dir)) {
167
- const p = join9(dir, name);
169
+ for (const name of readdirSync2(dir)) {
170
+ const p = join7(dir, name);
168
171
  if (statSync(p).isDirectory())
169
172
  walk(p);
170
173
  else if (isSdkSource(p))
171
- out["/__sdk/" + relative2(sdkSrc, p).split("\\").join("/")] = readFileSync6(p, "utf8");
174
+ out["/__sdk/" + relative2(sdkSrc, p).split("\\").join("/")] = readFileSync7(p, "utf8");
172
175
  }
173
176
  };
174
177
  walk(sdkSrc);
@@ -177,13 +180,13 @@ var resolveInject = () => {
177
180
  }, resolveChiselBin = () => {
178
181
  const override = process.env.LECODES_CHISEL_BIN;
179
182
  if (override) {
180
- if (existsSync7(override))
183
+ if (existsSync4(override))
181
184
  return override;
182
185
  throw new Error(`LECODES_CHISEL_BIN points at a missing file: ${override}`);
183
186
  }
184
187
  try {
185
188
  const bin = createRequire2(import.meta.url)("@letary/chisel").binaryPath();
186
- if (bin && existsSync7(bin))
189
+ if (bin && existsSync4(bin))
187
190
  return bin;
188
191
  } catch {}
189
192
  throw new Error("chisel binary not found. Install @letary/chisel, or set LECODES_CHISEL_BIN.");
@@ -206,9 +209,9 @@ var resolveInject = () => {
206
209
  }
207
210
  if (!projFiles[entry])
208
211
  throw new Error(`Entrypoint not found: ${entry}`);
209
- for (const [slug3, libFiles] of Object.entries(libraries ?? {})) {
212
+ for (const [slug, libFiles] of Object.entries(libraries ?? {})) {
210
213
  for (const [k, v] of Object.entries(libFiles)) {
211
- const path = normalize(`/__lib/${slug3}/${k}`);
214
+ const path = normalize(`/__lib/${slug}/${k}`);
212
215
  let src = v;
213
216
  if (/\.[tj]sx?$/.test(path)) {
214
217
  if (hasAssetMacro(src))
@@ -249,7 +252,7 @@ var resolveInject = () => {
249
252
  if (parsed.code == null)
250
253
  throw new Error("chisel produced no code");
251
254
  return { code: parsed.code, map: parsed.map ?? null };
252
- }, bundleProject = async (entrypoint, files, format = "esm") => (await bundleProjectChisel(entrypoint, files, format, false)).code, bundleProjectWithMap = async (entrypoint, files, format = "esm", minify, editor, libraries) => bundleProjectChisel(entrypoint, files, format, true, minify, editor, libraries);
255
+ }, bundleProject = async (entrypoint, files, format = "esm") => (await bundleProjectChisel(entrypoint, files, format, false)).code, bundleProjectWithMap = async (entrypoint, files, format = "esm", minify, editor, libraries, sourcemap = true) => bundleProjectChisel(entrypoint, files, format, sourcemap, minify, editor, libraries);
253
256
  var init_bundler = __esm(() => {
254
257
  init_assetMacro();
255
258
  init_aspectMacro();
@@ -290,6 +293,9 @@ var buildHeader = (name, code, opts = {}) => {
290
293
  localAssets.add("capture");
291
294
  if (localAssets.size > 0)
292
295
  header += `// preload: ${[...localAssets].join(";")}
296
+ `;
297
+ if (opts.fonts)
298
+ header += `// fonts: ${opts.fonts}
293
299
  `;
294
300
  if (!code.includes("_creator.createScene") && !code.includes("_creator.createEntity"))
295
301
  header += `// gl: disable
@@ -339,7 +345,7 @@ var detectEntry = (entries) => {
339
345
  const files = {};
340
346
  for (const e of opts.entries) {
341
347
  if (e.type === "resource") {
342
- if (opts.localAssets) {
348
+ if (opts.localAssets && !e.remote) {
343
349
  const name = e.path.split(/[\\/]/).pop() ?? e.path;
344
350
  files[e.path] = `export default ("id:" + _creatorUtils.fetchLocal(${JSON.stringify(name)}))`;
345
351
  } else {
@@ -360,12 +366,12 @@ var detectEntry = (entries) => {
360
366
  files[e.path] = e.text ?? "";
361
367
  }
362
368
  }
363
- const { code, map } = await bundleProjectWithMap(entry, files, opts.format ?? "esm", opts.minify, opts.editor, opts.libraries);
369
+ const { code, map } = await bundleProjectWithMap(entry, files, opts.format ?? "esm", opts.minify, opts.editor, opts.libraries, opts.sourcemap ?? true);
364
370
  if (opts.header === false) {
365
371
  return map ? `${code}
366
372
  ${inlineSourceMapComment(map)}` : code;
367
373
  }
368
- const header = buildHeader(opts.name, code, { icon: opts.icon });
374
+ const header = buildHeader(opts.name, code, { icon: opts.icon, fonts: opts.fontsHeader });
369
375
  if (!map)
370
376
  return header + code;
371
377
  const headerLines = (header.match(/\n/g) ?? []).length;
@@ -377,17 +383,17 @@ var init_compileProject = __esm(() => {
377
383
  });
378
384
 
379
385
  // ../sdk/src/compile/sceneEditor.ts
380
- import { existsSync as existsSync8, readdirSync as readdirSync5, readFileSync as readFileSync7 } from "node:fs";
381
- import { join as join10 } from "node:path";
386
+ import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "node:fs";
387
+ import { join as join8 } from "node:path";
382
388
  var SCENE_HARNESS_DIR = "./.scene-editor", SCENE_ENTRY_PATH = "./.scene-entry.ts", loadSceneHarness = (fallbackDir) => {
383
389
  const override = process.env.LECODES_SCENE_HARNESS_DIR;
384
- const dir = override && existsSync8(override) ? override : fallbackDir && existsSync8(fallbackDir) ? fallbackDir : null;
390
+ const dir = override && existsSync5(override) ? override : fallbackDir && existsSync5(fallbackDir) ? fallbackDir : null;
385
391
  if (!dir)
386
392
  throw new Error("scene-editor harness sources not found (set LECODES_SCENE_HARNESS_DIR)");
387
393
  const out = {};
388
- for (const f of readdirSync5(dir)) {
394
+ for (const f of readdirSync3(dir)) {
389
395
  if (f.endsWith(".ts"))
390
- out[f] = readFileSync7(join10(dir, f), "utf8");
396
+ out[f] = readFileSync8(join8(dir, f), "utf8");
391
397
  }
392
398
  return out;
393
399
  }, sceneEditorEntries = (scenePath, harness, projectPaths = []) => {
@@ -417,25 +423,569 @@ export {}
417
423
  };
418
424
  var init_sceneEditor = () => {};
419
425
 
426
+ // ../sdk/src/compile/fontRegistry.ts
427
+ 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, ext2) => `${id}-${weight}${italic ? "-italic" : ""}.${ext2}`, LATIN, CYR, OFL = "OFL-1.1", f = (family, category, scripts, weights, italics, defaultWeights) => ({ family, category, scripts, weights, italics, defaultWeights, license: OFL }), FONT_REGISTRY;
428
+ var init_fontRegistry = __esm(() => {
429
+ LATIN = ["latin"];
430
+ CYR = ["latin", "cyrillic"];
431
+ FONT_REGISTRY = {
432
+ inter: f("Inter", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700, 800, 900], [], [400, 500, 700]),
433
+ manrope: f("Manrope", "sans-serif", CYR, [200, 300, 400, 500, 600, 700, 800], [], [400, 500, 700]),
434
+ "golos-text": f("Golos Text", "sans-serif", CYR, [400, 500, 600, 700, 800, 900], [], [400, 500, 700]),
435
+ onest: f("Onest", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700, 800, 900], [], [400, 500, 700]),
436
+ rubik: f("Rubik", "sans-serif", CYR, [300, 400, 500, 600, 700, 800, 900], [300, 400, 500, 600, 700, 800, 900], [400, 500, 700]),
437
+ nunito: f("Nunito", "sans-serif", CYR, [200, 300, 400, 500, 600, 700, 800, 900], [400, 700], [400, 600, 700]),
438
+ montserrat: f("Montserrat", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700, 800, 900], [400, 700], [400, 500, 700]),
439
+ "ibm-plex-sans": f("IBM Plex Sans", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700], [400, 700], [400, 500, 700]),
440
+ "pt-sans": f("PT Sans", "sans-serif", CYR, [400, 700], [400, 700], [400, 700]),
441
+ jost: f("Jost", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700, 800, 900], [400, 700], [400, 500, 700]),
442
+ "exo-2": f("Exo 2", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700, 800, 900], [400, 700], [400, 500, 700]),
443
+ unbounded: f("Unbounded", "display", CYR, [200, 300, 400, 500, 600, 700, 800, 900], [], [400, 700]),
444
+ "russo-one": f("Russo One", "display", CYR, [400], [], [400]),
445
+ oswald: f("Oswald", "display", CYR, [200, 300, 400, 500, 600, 700], [], [400, 700]),
446
+ comfortaa: f("Comfortaa", "display", CYR, [300, 400, 500, 600, 700], [], [400, 700]),
447
+ "space-grotesk": f("Space Grotesk", "display", LATIN, [300, 400, 500, 600, 700], [], [400, 700]),
448
+ "playfair-display": f("Playfair Display", "serif", CYR, [400, 500, 600, 700, 800, 900], [400, 700], [400, 700]),
449
+ lora: f("Lora", "serif", CYR, [400, 500, 600, 700], [400, 700], [400, 700]),
450
+ "pt-serif": f("PT Serif", "serif", CYR, [400, 700], [400, 700], [400, 700]),
451
+ cormorant: f("Cormorant", "serif", CYR, [300, 400, 500, 600, 700], [400, 700], [400, 700]),
452
+ literata: f("Literata", "serif", CYR, [200, 300, 400, 500, 600, 700, 800, 900], [400, 700], [400, 700]),
453
+ "jetbrains-mono": f("JetBrains Mono", "monospace", CYR, [100, 200, 300, 400, 500, 600, 700, 800], [400, 700], [400, 700]),
454
+ "fira-code": f("Fira Code", "monospace", CYR, [300, 400, 500, 600, 700], [], [400, 700]),
455
+ "ibm-plex-mono": f("IBM Plex Mono", "monospace", CYR, [100, 200, 300, 400, 500, 600, 700], [400, 700], [400, 700]),
456
+ caveat: f("Caveat", "handwriting", CYR, [400, 500, 600, 700], [], [400, 700]),
457
+ pacifico: f("Pacifico", "handwriting", CYR, [400], [], [400])
458
+ };
459
+ });
460
+
461
+ // ../sdk/src/compile/sfnt.ts
462
+ var TAG_TTF = 65536, TAG_OTTO = 1330926671, TAG_TTCF = 1953784678, TAG_WOFF = 2001684038, TAG_WOF2 = 2001684018, parseSfnt = (bytes) => {
463
+ const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
464
+ if (bytes.byteLength < 12)
465
+ throw new Error("not a font file (truncated)");
466
+ const version = dv.getUint32(0);
467
+ if (version === TAG_TTCF)
468
+ throw new Error("font collections (.ttc) aren't supported — export a single face");
469
+ if (version === TAG_WOFF || version === TAG_WOF2)
470
+ throw new Error("WOFF/WOFF2 project fonts aren't supported — use the .ttf/.otf");
471
+ if (version !== TAG_TTF && version !== TAG_OTTO)
472
+ throw new Error("not a TTF/OTF font file");
473
+ const numTables = dv.getUint16(4);
474
+ const tables = new Map;
475
+ for (let i = 0;i < numTables; i++) {
476
+ const rec = 12 + i * 16;
477
+ if (rec + 16 > bytes.byteLength)
478
+ throw new Error("not a font file (truncated table directory)");
479
+ const tag = String.fromCharCode(bytes[rec], bytes[rec + 1], bytes[rec + 2], bytes[rec + 3]);
480
+ tables.set(tag, { offset: dv.getUint32(rec + 8), length: dv.getUint32(rec + 12) });
481
+ }
482
+ const family = readFamily(dv, bytes, tables.get("name"));
483
+ if (!family)
484
+ throw new Error("no family name in the font's `name` table");
485
+ let weight = 400;
486
+ let italic = false;
487
+ const os2 = tables.get("OS/2");
488
+ if (os2 && os2.offset + 64 <= bytes.byteLength) {
489
+ weight = dv.getUint16(os2.offset + 4);
490
+ italic = (dv.getUint16(os2.offset + 62) & 1) === 1;
491
+ } else {
492
+ const head = tables.get("head");
493
+ if (head && head.offset + 46 <= bytes.byteLength) {
494
+ const macStyle = dv.getUint16(head.offset + 44);
495
+ if (macStyle & 1)
496
+ weight = 700;
497
+ italic = (macStyle & 2) === 2;
498
+ }
499
+ }
500
+ if (weight < 1 || weight > 1000)
501
+ weight = 400;
502
+ return { family, weight, italic };
503
+ }, readFamily = (dv, bytes, name) => {
504
+ if (!name || name.offset + 6 > bytes.byteLength)
505
+ return "";
506
+ const base = name.offset;
507
+ const count = dv.getUint16(base + 2);
508
+ const stringOffset = base + dv.getUint16(base + 4);
509
+ let best = "";
510
+ let bestScore = -1;
511
+ for (let i = 0;i < count; i++) {
512
+ const rec = base + 6 + i * 12;
513
+ if (rec + 12 > bytes.byteLength)
514
+ break;
515
+ const platformID = dv.getUint16(rec);
516
+ const languageID = dv.getUint16(rec + 4);
517
+ const nameID = dv.getUint16(rec + 6);
518
+ const length = dv.getUint16(rec + 8);
519
+ const offset = stringOffset + dv.getUint16(rec + 10);
520
+ if (nameID !== 16 && nameID !== 1)
521
+ continue;
522
+ if (offset + length > bytes.byteLength)
523
+ continue;
524
+ let score = nameID === 16 ? 100 : 0;
525
+ if (platformID === 3)
526
+ score += languageID === 1033 ? 30 : 20;
527
+ else if (platformID === 0)
528
+ score += 10;
529
+ if (score <= bestScore)
530
+ continue;
531
+ const text = platformID === 1 ? decodeLatin1(bytes, offset, length) : decodeUtf16be(bytes, offset, length);
532
+ if (!text.trim())
533
+ continue;
534
+ best = text.trim();
535
+ bestScore = score;
536
+ }
537
+ return best;
538
+ }, decodeUtf16be = (bytes, offset, length) => {
539
+ let out = "";
540
+ for (let i = 0;i + 1 < length; i += 2)
541
+ out += String.fromCharCode(bytes[offset + i] << 8 | bytes[offset + i + 1]);
542
+ return out;
543
+ }, decodeLatin1 = (bytes, offset, length) => {
544
+ let out = "";
545
+ for (let i = 0;i < length; i++)
546
+ out += String.fromCharCode(bytes[offset + i]);
547
+ return out;
548
+ };
549
+
550
+ // ../sdk/src/compile/fontMacro.ts
551
+ var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c2) => c2 >= "A" && c2 <= "Z" || c2 >= "a" && c2 <= "z" || c2 === "_" || c2 === "$", isIdentPart = (c2) => isIdentStart(c2) || c2 >= "0" && c2 <= "9", skipString = (src, start) => {
552
+ const quote = src[start];
553
+ const n = src.length;
554
+ let i = start + 1;
555
+ while (i < n) {
556
+ const c2 = src[i];
557
+ if (c2 === "\\") {
558
+ i += 2;
559
+ continue;
560
+ }
561
+ if (quote === "`" && c2 === "$" && src[i + 1] === "{") {
562
+ i = skipBraces(src, i + 1);
563
+ continue;
564
+ }
565
+ if (c2 === quote)
566
+ return i + 1;
567
+ i++;
568
+ }
569
+ return n;
570
+ }, skipBraces = (src, open) => {
571
+ const n = src.length;
572
+ let depth = 0;
573
+ let i = open;
574
+ while (i < n) {
575
+ const c2 = src[i];
576
+ if (c2 === '"' || c2 === "'" || c2 === "`") {
577
+ i = skipString(src, i);
578
+ continue;
579
+ }
580
+ if (c2 === "{")
581
+ depth++;
582
+ else if (c2 === "}") {
583
+ depth--;
584
+ if (depth === 0)
585
+ return i + 1;
586
+ }
587
+ i++;
588
+ }
589
+ return n;
590
+ }, matchParen = (src, open) => {
591
+ const n = src.length;
592
+ let depth = 0;
593
+ let i = open;
594
+ while (i < n) {
595
+ const c2 = src[i];
596
+ if (c2 === '"' || c2 === "'" || c2 === "`") {
597
+ i = skipString(src, i);
598
+ continue;
599
+ }
600
+ if (c2 === "/" && src[i + 1] === "/") {
601
+ i += 2;
602
+ while (i < n && src[i] !== `
603
+ `)
604
+ i++;
605
+ continue;
606
+ }
607
+ if (c2 === "/" && src[i + 1] === "*") {
608
+ i += 2;
609
+ while (i < n && !(src[i] === "*" && src[i + 1] === "/"))
610
+ i++;
611
+ i += 2;
612
+ continue;
613
+ }
614
+ if (c2 === "(")
615
+ depth++;
616
+ else if (c2 === ")") {
617
+ depth--;
618
+ if (depth === 0)
619
+ return i;
620
+ }
621
+ i++;
622
+ }
623
+ return -1;
624
+ }, splitTopLevel = (text, delim) => {
625
+ const parts = [];
626
+ const n = text.length;
627
+ let depth = 0;
628
+ let last = 0;
629
+ let i = 0;
630
+ while (i < n) {
631
+ const c2 = text[i];
632
+ if (c2 === '"' || c2 === "'" || c2 === "`") {
633
+ i = skipString(text, i);
634
+ continue;
635
+ }
636
+ if (c2 === "(" || c2 === "[" || c2 === "{")
637
+ depth++;
638
+ else if (c2 === ")" || c2 === "]" || c2 === "}")
639
+ depth = Math.max(0, depth - 1);
640
+ else if (depth === 0 && c2 === delim) {
641
+ parts.push(text.slice(last, i));
642
+ last = i + 1;
643
+ }
644
+ i++;
645
+ }
646
+ parts.push(text.slice(last));
647
+ return parts;
648
+ }, stringLiteralValue = (text) => {
649
+ const t = text.trim();
650
+ if (t.length < 2)
651
+ return null;
652
+ const q = t[0];
653
+ if (q !== '"' && q !== "'" && q !== "`" || t[t.length - 1] !== q)
654
+ return null;
655
+ const inner = t.slice(1, -1);
656
+ if (q === "`" && inner.includes("${"))
657
+ return null;
658
+ if (inner.includes(q) && !inner.includes("\\" + q))
659
+ return null;
660
+ return inner.replace(/\\(.)/g, "$1");
661
+ }, prevNonSpace = (src, i) => {
662
+ let k = i - 1;
663
+ while (k >= 0 && /\s/.test(src[k]))
664
+ k--;
665
+ return k >= 0 ? src[k] : "";
666
+ }, scanFontCalls = (source) => {
667
+ const calls = [];
668
+ const n = source.length;
669
+ let i = 0;
670
+ while (i < n) {
671
+ const c2 = source[i];
672
+ if (c2 === '"' || c2 === "'" || c2 === "`") {
673
+ i = skipString(source, i);
674
+ continue;
675
+ }
676
+ if (c2 === "/" && source[i + 1] === "/") {
677
+ i += 2;
678
+ while (i < n && source[i] !== `
679
+ `)
680
+ i++;
681
+ continue;
682
+ }
683
+ if (c2 === "/" && source[i + 1] === "*") {
684
+ i += 2;
685
+ while (i < n && !(source[i] === "*" && source[i + 1] === "/"))
686
+ i++;
687
+ i += 2;
688
+ continue;
689
+ }
690
+ if (isIdentStart(c2)) {
691
+ let j = i + 1;
692
+ while (j < n && isIdentPart(source[j]))
693
+ j++;
694
+ if (source.slice(i, j) === "font" && prevNonSpace(source, i) !== ".") {
695
+ const call = parseCall(source, i, j);
696
+ if (call) {
697
+ calls.push(call);
698
+ i = call.end;
699
+ continue;
700
+ }
701
+ }
702
+ i = j;
703
+ continue;
704
+ }
705
+ i++;
706
+ }
707
+ return calls;
708
+ }, parseCall = (src, start, identEnd) => {
709
+ const n = src.length;
710
+ let i = identEnd;
711
+ while (i < n && /\s/.test(src[i]))
712
+ i++;
713
+ if (src[i] !== "(")
714
+ return null;
715
+ const close = matchParen(src, i);
716
+ if (close === -1)
717
+ return null;
718
+ const call = { start, end: close + 1, arg: "", kind: "registry" };
719
+ const args = splitTopLevel(src.slice(i + 1, close), ",");
720
+ const arg = stringLiteralValue(args[0] ?? "");
721
+ if (arg === null) {
722
+ call.error = `font(...) needs a string-literal argument — a registry id ("manrope") or a project path ("./fonts/Brand.ttf"). Got \`${(args[0] ?? "").trim() || "()"}\`.`;
723
+ return call;
724
+ }
725
+ call.arg = arg;
726
+ if (arg.startsWith("./") || arg.startsWith("../")) {
727
+ call.kind = "file";
728
+ if (!/\.(ttf|otf)$/i.test(arg)) {
729
+ call.error = `font("${arg}"): project fonts must be .ttf/.otf files.`;
730
+ return call;
731
+ }
732
+ if (args.length > 1 && args.slice(1).some((a) => a.trim())) {
733
+ call.error = `font("${arg}"): a project font takes no options — weight and style come from the file itself.`;
734
+ return call;
735
+ }
736
+ return call;
737
+ }
738
+ if (arg === "serif" || arg === "sans-serif" || arg === "monospace" || arg === "monospaced") {
739
+ call.error = `font("${arg}"): system font families need no font() — pass the string straight to fontFamily, e.g. .style({ fontFamily: "${arg}" }).`;
740
+ return call;
741
+ }
742
+ if (!FONT_ID.test(arg)) {
743
+ call.error = `font("${arg}"): expected a registry id (lowercase [a-z0-9-], e.g. "manrope") or a relative path ("./fonts/Brand.ttf").`;
744
+ return call;
745
+ }
746
+ call.kind = "registry";
747
+ call.id = arg;
748
+ const optsText = args[1]?.trim();
749
+ if (optsText) {
750
+ const err = parseOptions(optsText, call);
751
+ if (err) {
752
+ call.error = err;
753
+ return call;
754
+ }
755
+ }
756
+ return call;
757
+ }, parseOptions = (optsText, call) => {
758
+ if (!optsText.startsWith("{") || !optsText.endsWith("}")) {
759
+ return `font("${call.arg}", opts): opts must be an object literal like { weights: [400, 700] }.`;
760
+ }
761
+ for (const prop of splitTopLevel(optsText.slice(1, -1), ",")) {
762
+ if (!prop.trim())
763
+ continue;
764
+ const kv = splitTopLevel(prop, ":");
765
+ if (kv.length < 2)
766
+ return `font("${call.arg}"): couldn't parse option \`${prop.trim()}\`.`;
767
+ const key = kv[0].trim().replace(/^["'`]|["'`]$/g, "");
768
+ const raw = kv.slice(1).join(":").trim();
769
+ if (key === "weights") {
770
+ const m = /^\[\s*(\d+(?:\s*,\s*\d+)*)\s*,?\s*\]$/.exec(raw);
771
+ if (!m)
772
+ return `font("${call.arg}"): weights must be a literal number array, e.g. { weights: [400, 700] }.`;
773
+ call.weights = m[1].split(",").map((s) => parseInt(s.trim(), 10));
774
+ } else if (key === "italic") {
775
+ if (raw !== "true" && raw !== "false")
776
+ return `font("${call.arg}"): italic must be a literal true/false.`;
777
+ call.italic = raw === "true";
778
+ } else {
779
+ return `font("${call.arg}"): unknown option "${key}" (did you mean weights/italic?).`;
780
+ }
781
+ }
782
+ return null;
783
+ }, norm = (p) => p.replace(/\\/g, "/").replace(/^\//, ""), dirOf = (p) => {
784
+ const n = norm(p);
785
+ const i = n.lastIndexOf("/");
786
+ return i === -1 ? "" : n.slice(0, i);
787
+ }, baseOf = (p) => {
788
+ const n = norm(p);
789
+ return n.slice(n.lastIndexOf("/") + 1);
790
+ }, resolveRel = (dir, rel) => {
791
+ const out = dir ? dir.split("/") : [];
792
+ for (const seg of rel.split("/")) {
793
+ if (!seg || seg === ".")
794
+ continue;
795
+ if (seg === "..") {
796
+ if (out.length === 0)
797
+ return "";
798
+ out.pop();
799
+ } else
800
+ out.push(seg);
801
+ }
802
+ return out.join("/");
803
+ }, relSpecifier = (fromDir, toPath) => {
804
+ const from = fromDir ? fromDir.split("/") : [];
805
+ const to = norm(toPath).split("/");
806
+ let common = 0;
807
+ while (common < from.length && common < to.length - 1 && from[common] === to[common])
808
+ common++;
809
+ const ups = from.length - common;
810
+ const rest = to.slice(common).join("/");
811
+ return ups === 0 ? `./${rest}` : `${"../".repeat(ups)}${rest}`;
812
+ }, faceKey = (family, weight, italic) => `${family}|${weight}|${italic ? "i" : "n"}`, applyFontMacro = async (entries, opts) => {
813
+ const scanned = entries.filter((e) => e.type === "text" && CODE_FILE.test(e.path) && typeof e.text === "string" && e.text.includes("font(")).filter((e) => !DECLARES_FONT.test(e.text)).map((e) => ({ entry: e, calls: scanFontCalls(e.text) })).filter((s) => s.calls.length > 0);
814
+ if (scanned.length === 0)
815
+ return [];
816
+ const errors = scanned.flatMap((s) => s.calls.filter((c2) => c2.error).map((c2) => `${s.entry.path}: ${c2.error}`));
817
+ if (errors.length)
818
+ throw new Error([...new Set(errors)].join(`
819
+ `));
820
+ const registryBase = (opts.registryBase ?? fontRegistryBase()).replace(/\/+$/, "");
821
+ const entryByPath = new Map(entries.map((e) => [norm(e.path), e]));
822
+ const faces = new Map;
823
+ const fileFaceCache = new Map;
824
+ const resolveCall = (entryPath, call) => {
825
+ if (call.kind === "file") {
826
+ const filePath = resolveRel(dirOf(entryPath), call.arg);
827
+ const target = filePath && entryByPath.get(filePath);
828
+ if (!target)
829
+ throw new Error(`${entryPath}: font("${call.arg}"): no such file in the project (resolved to "${filePath || call.arg}").`);
830
+ call.filePath = filePath;
831
+ let info2 = fileFaceCache.get(filePath);
832
+ if (!info2) {
833
+ if (!opts.readBytes)
834
+ throw new Error(`${entryPath}: font("${call.arg}"): project-font files aren't supported in this compile context.`);
835
+ const bytes = opts.readBytes(filePath);
836
+ if (!bytes)
837
+ throw new Error(`${entryPath}: font("${call.arg}"): couldn't read "${filePath}".`);
838
+ try {
839
+ info2 = parseSfnt(bytes);
840
+ } catch (e) {
841
+ throw new Error(`${entryPath}: font("${call.arg}"): ${e.message}`);
842
+ }
843
+ fileFaceCache.set(filePath, info2);
844
+ }
845
+ return { family: info2.family, callFaces: [{ family: info2.family, weight: info2.weight, italic: info2.italic }] };
846
+ }
847
+ const meta = FONT_REGISTRY[call.id];
848
+ if (!meta) {
849
+ const known = Object.keys(FONT_REGISTRY);
850
+ throw new Error(`${entryPath}: font("${call.id}"): unknown registry font. Available: ${known.slice(0, 10).join(", ")}${known.length > 10 ? ", …" : ""}`);
851
+ }
852
+ const weights = call.weights ?? meta.defaultWeights;
853
+ const badW = weights.filter((w) => !meta.weights.includes(w));
854
+ if (badW.length)
855
+ throw new Error(`${entryPath}: font("${call.id}"): no ${badW.join("/")} face — available weights: ${meta.weights.join(", ")}.`);
856
+ if (call.italic) {
857
+ const badI = weights.filter((w) => !meta.italics.includes(w));
858
+ if (badI.length) {
859
+ throw new Error(`${entryPath}: font("${call.id}"): no italic ${badI.join("/")} face${meta.italics.length ? ` — italics exist for: ${meta.italics.join(", ")}` : " — the family ships no italics"}.`);
860
+ }
861
+ }
862
+ const callFaces = weights.flatMap((w) => call.italic ? [{ id: call.id, family: meta.family, weight: w, italic: false }, { id: call.id, family: meta.family, weight: w, italic: true }] : [{ id: call.id, family: meta.family, weight: w, italic: false }]);
863
+ return { family: meta.family, callFaces };
864
+ };
865
+ const pendingRegistry = new Map;
866
+ const work = [];
867
+ for (const { entry, calls } of scanned) {
868
+ const fw = { entry, calls: [], faceKeys: new Set };
869
+ for (const call of calls) {
870
+ const { family, callFaces } = resolveCall(norm(entry.path), call);
871
+ fw.calls.push({ call, family });
872
+ for (const cf of callFaces) {
873
+ const key = faceKey(cf.family, cf.weight, cf.italic);
874
+ fw.faceKeys.add(key);
875
+ if (faces.has(key))
876
+ continue;
877
+ if (cf.id === undefined) {
878
+ faces.set(key, { family: cf.family, weight: cf.weight, italic: cf.italic, source: { kind: "entry", path: call.filePath } });
879
+ } else {
880
+ pendingRegistry.set(key, cf);
881
+ faces.set(key, { family: cf.family, weight: cf.weight, italic: cf.italic, source: { kind: "cdn", urlBase: "" } });
882
+ }
883
+ }
884
+ }
885
+ work.push(fw);
886
+ }
887
+ for (const [key, face] of pendingRegistry) {
888
+ const ref = faces.get(key);
889
+ if (opts.mode === "cdn") {
890
+ ref.source = { kind: "cdn", urlBase: `${registryBase}/${face.id}/${face.id}-${face.weight}${face.italic ? "-italic" : ""}.` };
891
+ } else {
892
+ if (!opts.vendorFace)
893
+ throw new Error(`font("${face.id}"): registry fonts need vendoring in this compile context, but no vendor hook was provided.`);
894
+ const path = await opts.vendorFace({ ...face, fileName: faceFileName(face.id, face.weight, face.italic, "ttf") });
895
+ ref.source = { kind: "entry", path: norm(path) };
896
+ }
897
+ }
898
+ for (const fw of work) {
899
+ const entryPath = norm(fw.entry.path);
900
+ const dir = dirOf(entryPath);
901
+ const modBase = `.${baseOf(entryPath)}.__fonts.ts`;
902
+ const modPath = dir ? `${dir}/${modBase}` : modBase;
903
+ const idents = new Map;
904
+ for (const { family } of fw.calls) {
905
+ if (!idents.has(family))
906
+ idents.set(family, `__font_${idents.size}`);
907
+ }
908
+ let text = fw.entry.text;
909
+ for (const { call, family } of [...fw.calls].sort((a, b) => b.call.start - a.call.start)) {
910
+ text = text.slice(0, call.start) + idents.get(family) + text.slice(call.end);
911
+ }
912
+ text += `
913
+ import { ${[...idents.values()].join(", ")} } from ${JSON.stringify(`./${modBase}`)}`;
914
+ fw.entry.text = text;
915
+ const lines = [
916
+ "// Generated by the font() compile macro: registers this file's fonts. Imports hoist, so the",
917
+ "// faces are declared before the importing module's body runs. Hosts dedupe repeated faces."
918
+ ];
919
+ const faceImports = new Map;
920
+ const body = [];
921
+ for (const key of [...fw.faceKeys].sort()) {
922
+ const ref = faces.get(key);
923
+ let urlExpr;
924
+ if (ref.source.kind === "cdn") {
925
+ urlExpr = `${JSON.stringify(ref.source.urlBase)} + (_creatorUI.fontFormat ?? "ttf")`;
926
+ } else {
927
+ let ident = faceImports.get(ref.source.path);
928
+ if (!ident) {
929
+ ident = `__face_${faceImports.size}`;
930
+ faceImports.set(ref.source.path, ident);
931
+ lines.push(`import ${ident} from ${JSON.stringify(relSpecifier(dir, ref.source.path))}`);
932
+ }
933
+ urlExpr = ident;
934
+ }
935
+ const optionsExpr = ref.italic ? `{ weight: ${ref.weight}, style: "italic" }` : `{ weight: ${ref.weight} }`;
936
+ body.push(`_creatorUI.registerFont(${JSON.stringify(ref.family)}, ${urlExpr}, ${optionsExpr}, () => {}, () => {})`);
937
+ }
938
+ for (const [family, ident] of idents)
939
+ lines.push(`export const ${ident} = ${JSON.stringify(family)}`);
940
+ lines.push(...body);
941
+ entries.push({ path: modPath, type: "text", text: lines.join(`
942
+ `) + `
943
+ ` });
944
+ }
945
+ return [...faces.values()];
946
+ }, buildFontsHeaderValue = (faces, entryUrl) => faces.map((f2) => {
947
+ const tag = `${f2.family}@${f2.weight}${f2.italic ? "i" : ""}`;
948
+ if (f2.source.kind === "cdn")
949
+ return `${tag}=${f2.source.urlBase}{fmt}`;
950
+ const url = entryUrl?.(f2.source.path);
951
+ return url ? `${tag}=${url}` : null;
952
+ }).filter((s) => !!s).join(";");
953
+ var init_fontMacro = __esm(() => {
954
+ init_fontRegistry();
955
+ FONT_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
956
+ CODE_FILE = /\.[tj]sx?$/;
957
+ DECLARES_FONT = /\b(?:function|const|let|var|class)\s+font\b|\bimport\s+(?:\{[^}]*\bfont\b[^}]*\}|font)\s+from\b/;
958
+ });
959
+
420
960
  // ../sdk/src/compile/index.ts
421
961
  var exports_compile = {};
422
962
  __export(exports_compile, {
423
963
  sceneEditorEntries: () => sceneEditorEntries,
964
+ scanFontCalls: () => scanFontCalls,
965
+ parseSfnt: () => parseSfnt,
424
966
  offsetSourceMap: () => offsetSourceMap,
425
967
  loadSceneHarness: () => loadSceneHarness,
426
968
  inlineSourceMapComment: () => inlineSourceMapComment,
969
+ fontRegistryBase: () => fontRegistryBase,
970
+ faceFileName: () => faceFileName,
427
971
  detectProjectKind: () => detectProjectKind,
428
972
  compileProject: () => compileProject,
429
973
  bundleProjectWithMap: () => bundleProjectWithMap,
430
974
  bundleProject: () => bundleProject,
431
975
  buildHeader: () => buildHeader,
976
+ buildFontsHeaderValue: () => buildFontsHeaderValue,
977
+ applyFontMacro: () => applyFontMacro,
432
978
  SCENE_HARNESS_DIR: () => SCENE_HARNESS_DIR,
433
- SCENE_ENTRY_PATH: () => SCENE_ENTRY_PATH
979
+ SCENE_ENTRY_PATH: () => SCENE_ENTRY_PATH,
980
+ FONT_REGISTRY: () => FONT_REGISTRY,
981
+ DEFAULT_FONT_REGISTRY_BASE: () => DEFAULT_FONT_REGISTRY_BASE
434
982
  });
435
983
  var init_compile = __esm(() => {
436
984
  init_bundler();
437
985
  init_compileProject();
438
986
  init_sceneEditor();
987
+ init_fontMacro();
988
+ init_fontRegistry();
439
989
  });
440
990
 
441
991
  // ../../node_modules/.bun/qrcode-terminal@0.12.0/node_modules/qrcode-terminal/vendor/QRCode/QRMode.js
@@ -1457,7 +2007,7 @@ var require_main = __commonJS((exports, module) => {
1457
2007
  // package.json
1458
2008
  var package_default = {
1459
2009
  name: "lecodes-cli",
1460
- version: "0.7.1",
2010
+ version: "0.8.0",
1461
2011
  description: "Clone, edit and push LeCodes projects from your machine.",
1462
2012
  type: "module",
1463
2013
  license: "MIT",
@@ -1490,8 +2040,8 @@ var package_default = {
1490
2040
  "@letary/chisel": "^0.4.1"
1491
2041
  },
1492
2042
  peerDependencies: {
1493
- "lecodes-design": "^0.3.1",
1494
- "lecodes-renderer": "^0.6.1",
2043
+ "lecodes-design": "^0.3.2",
2044
+ "lecodes-renderer": "^0.6.2",
1495
2045
  "lecodes-3d-editor": "^0.1.0"
1496
2046
  },
1497
2047
  peerDependenciesMeta: {
@@ -1507,16 +2057,22 @@ var package_default = {
1507
2057
  },
1508
2058
  devDependencies: {
1509
2059
  "lecodes-design": "workspace:*",
1510
- "lecodes-renderer": "^0.4.1",
2060
+ "lecodes-renderer": "workspace:*",
1511
2061
  "qrcode-terminal": "^0.12.0",
1512
2062
  "lecodes-3d-editor": "workspace:*",
1513
2063
  sdk: "workspace:*"
1514
2064
  }
1515
2065
  };
1516
2066
 
1517
- // src/commands/clone.ts
1518
- import { existsSync as existsSync5, mkdirSync as mkdirSync5, readdirSync } from "node:fs";
1519
- import { resolve } from "node:path";
2067
+ // src/commands/app.ts
2068
+ import { execFileSync } from "node:child_process";
2069
+ import { cpSync, existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync10, readdirSync as readdirSync4, rmSync, statSync as statSync2, writeFileSync as writeFileSync7 } from "node:fs";
2070
+ import { homedir as homedir2 } from "node:os";
2071
+ import { basename as basename4, dirname as dirname7, join as join11, resolve as resolve2 } from "node:path";
2072
+
2073
+ // src/manifest.ts
2074
+ import { dirname, join } from "node:path";
2075
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
1520
2076
 
1521
2077
  // src/util.ts
1522
2078
  import { createHash } from "node:crypto";
@@ -1621,17 +2177,145 @@ var flagStr = (args, ...keys) => {
1621
2177
  };
1622
2178
  var flagBool = (args, ...keys) => keys.some((key) => args.flags[key] === true || args.flags[key] === "true");
1623
2179
 
1624
- // src/api.ts
1625
- var errorMessage = (body, status) => {
1626
- if (typeof body === "string" && body)
1627
- return body;
1628
- if (body && typeof body === "object") {
1629
- const b = body;
1630
- if (typeof b.error === "string")
1631
- return b.error;
1632
- if (b.error && typeof b.error === "object") {
1633
- const first = Object.values(b.error)[0];
1634
- if (typeof first === "string")
2180
+ // src/manifest.ts
2181
+ var LECODES_DIR = ".lecodes";
2182
+ var manifestPath = (root) => join(root, LECODES_DIR, "manifest.json");
2183
+ var findProjectRoot = (start) => {
2184
+ let dir = start;
2185
+ while (true) {
2186
+ if (existsSync(manifestPath(dir)))
2187
+ return dir;
2188
+ const parent = dirname(dir);
2189
+ if (parent === dir)
2190
+ throw new CliError("Not inside a lecodes project (no .lecodes/manifest.json found).");
2191
+ dir = parent;
2192
+ }
2193
+ };
2194
+ var readManifest = (root) => {
2195
+ try {
2196
+ return JSON.parse(readFileSync(manifestPath(root), "utf8"));
2197
+ } catch {
2198
+ throw new CliError("Could not read .lecodes/manifest.json — is this a lecodes project?");
2199
+ }
2200
+ };
2201
+ var writeManifest = (root, manifest) => {
2202
+ mkdirSync(join(root, LECODES_DIR), { recursive: true });
2203
+ writeFileSync(manifestPath(root), JSON.stringify(manifest, null, 2));
2204
+ };
2205
+
2206
+ // src/ignore.ts
2207
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
2208
+ import { join as join2 } from "node:path";
2209
+ var BUILTIN_PATTERNS = ["node_modules/", ".git/", "_*", "*.log", ".DS_Store"];
2210
+ var DEFAULT_IGNORE_FILE = `# Files matched here are NOT pushed to LeCodes.
2211
+ # Only untracked files are affected — assets already in the project stay tracked
2212
+ # even if they match a pattern. Built-in defaults (node_modules, _*, *.log) always apply.
2213
+
2214
+ # Render / preview outputs (the default \`lecodes render --png\` target)
2215
+ screenshot.png
2216
+ shots/
2217
+
2218
+ # Image & other binary assets ARE pushed by default — they're real project resources.
2219
+ # Uncomment a pattern below only if you want to keep some untracked ones out of pushes.
2220
+ # *.png
2221
+
2222
+ # Local notes & dumps (uncomment to ignore)
2223
+ # *.json
2224
+ # notes.md
2225
+ `;
2226
+ var translate = (glob) => {
2227
+ let re = "";
2228
+ for (let i = 0;i < glob.length; i++) {
2229
+ const ch = glob[i];
2230
+ if (ch === "*") {
2231
+ if (glob[i + 1] === "*") {
2232
+ re += ".*";
2233
+ i++;
2234
+ if (glob[i + 1] === "/")
2235
+ i++;
2236
+ } else
2237
+ re += "[^/]*";
2238
+ } else if (ch === "?")
2239
+ re += "[^/]";
2240
+ else if (ch === "/")
2241
+ re += "/";
2242
+ else if ("\\^$.|+()[]{}".includes(ch))
2243
+ re += "\\" + ch;
2244
+ else
2245
+ re += ch;
2246
+ }
2247
+ return re;
2248
+ };
2249
+ var compile = (raw) => {
2250
+ let p = raw.trim();
2251
+ if (!p || p.startsWith("#"))
2252
+ return null;
2253
+ let negate = false;
2254
+ if (p.startsWith("!")) {
2255
+ negate = true;
2256
+ p = p.slice(1);
2257
+ }
2258
+ let dirOnly = false;
2259
+ if (p.endsWith("/")) {
2260
+ dirOnly = true;
2261
+ p = p.slice(0, -1);
2262
+ }
2263
+ const anchored = p.startsWith("/") || p.includes("/");
2264
+ if (p.startsWith("/"))
2265
+ p = p.slice(1);
2266
+ if (!p)
2267
+ return null;
2268
+ const body = translate(p);
2269
+ const re = anchored ? new RegExp("^/" + body + "(/.*)?$") : new RegExp("(^|/)" + body + "(/.*)?$");
2270
+ return { re, negate, dirOnly };
2271
+ };
2272
+ var buildMatcher = (rules) => (key, isDir) => {
2273
+ let ignored = false;
2274
+ for (const r of rules) {
2275
+ if (r.dirOnly && !isDir)
2276
+ continue;
2277
+ if (r.re.test(key))
2278
+ ignored = !r.negate;
2279
+ }
2280
+ return ignored;
2281
+ };
2282
+ var buildGlobMatcher = (patterns) => {
2283
+ const rules = patterns.map((p) => compile(p.trim().replace(/\/+$/, ""))).filter((r) => r !== null);
2284
+ const match = buildMatcher(rules);
2285
+ return (path) => match(path.startsWith("/") ? path : "/" + path, false);
2286
+ };
2287
+ var loadIgnore = (root) => {
2288
+ const lines = [...BUILTIN_PATTERNS];
2289
+ const file = join2(root, ".lecodesignore");
2290
+ if (existsSync2(file)) {
2291
+ try {
2292
+ lines.push(...readFileSync2(file, "utf8").split(/\r?\n/));
2293
+ } catch {}
2294
+ }
2295
+ const rules = lines.map(compile).filter((r) => r !== null);
2296
+ return buildMatcher(rules);
2297
+ };
2298
+ var writeDefaultIgnore = (root) => {
2299
+ const file = join2(root, ".lecodesignore");
2300
+ if (!existsSync2(file))
2301
+ writeFileSync2(file, DEFAULT_IGNORE_FILE);
2302
+ };
2303
+
2304
+ // src/commands/compile.ts
2305
+ import { writeFileSync as writeFileSync6 } from "node:fs";
2306
+ import { basename as basename3, resolve } from "node:path";
2307
+
2308
+ // src/api.ts
2309
+ var errorMessage = (body, status) => {
2310
+ if (typeof body === "string" && body)
2311
+ return body;
2312
+ if (body && typeof body === "object") {
2313
+ const b = body;
2314
+ if (typeof b.error === "string")
2315
+ return b.error;
2316
+ if (b.error && typeof b.error === "object") {
2317
+ const first = Object.values(b.error)[0];
2318
+ if (typeof first === "string")
1635
2319
  return first;
1636
2320
  if (first && typeof first === "object" && typeof first.message === "string") {
1637
2321
  return first.message;
@@ -1688,15 +2372,15 @@ var downloadBlob = async (apiUrl, src) => {
1688
2372
 
1689
2373
  // src/config.ts
1690
2374
  import { homedir } from "node:os";
1691
- import { join } from "node:path";
1692
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2375
+ import { join as join3 } from "node:path";
2376
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
1693
2377
  var DEFAULT_API = "https://le.codes";
1694
- var CONFIG_DIR = join(homedir(), ".lecodes");
1695
- var CONFIG_FILE = join(CONFIG_DIR, "config.json");
2378
+ var CONFIG_DIR = join3(homedir(), ".lecodes");
2379
+ var CONFIG_FILE = join3(CONFIG_DIR, "config.json");
1696
2380
  var loadConfig = () => {
1697
2381
  let stored = {};
1698
2382
  try {
1699
- stored = JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
2383
+ stored = JSON.parse(readFileSync3(CONFIG_FILE, "utf8"));
1700
2384
  } catch {}
1701
2385
  return {
1702
2386
  apiUrl: process.env.LECODES_API ?? stored.apiUrl,
@@ -1704,8 +2388,8 @@ var loadConfig = () => {
1704
2388
  };
1705
2389
  };
1706
2390
  var saveConfig = (config) => {
1707
- mkdirSync(CONFIG_DIR, { recursive: true });
1708
- writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
2391
+ mkdirSync2(CONFIG_DIR, { recursive: true });
2392
+ writeFileSync3(CONFIG_FILE, JSON.stringify(config, null, 2));
1709
2393
  };
1710
2394
  var normalizeApiUrl = (url) => url.trim().replace(/\/+$/, "");
1711
2395
  var requireToken = (config) => {
@@ -1719,39 +2403,124 @@ var requireApiUrl = (config) => {
1719
2403
  return normalizeApiUrl(config.apiUrl);
1720
2404
  };
1721
2405
 
1722
- // src/manifest.ts
1723
- import { dirname, join as join2 } from "node:path";
1724
- import { existsSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "node:fs";
1725
- var LECODES_DIR = ".lecodes";
1726
- var manifestPath = (root) => join2(root, LECODES_DIR, "manifest.json");
1727
- var findProjectRoot = (start) => {
1728
- let dir = start;
1729
- while (true) {
1730
- if (existsSync(manifestPath(dir)))
1731
- return dir;
1732
- const parent = dirname(dir);
1733
- if (parent === dir)
1734
- throw new CliError("Not inside a lecodes project (no .lecodes/manifest.json found).");
1735
- dir = parent;
1736
- }
2406
+ // src/compile/collect.ts
2407
+ import { readFileSync as readFileSync5 } from "node:fs";
2408
+ import { basename } from "node:path";
2409
+
2410
+ // src/localFiles.ts
2411
+ import { readdirSync, readFileSync as readFileSync4 } from "node:fs";
2412
+ import { join as join4, relative } from "node:path";
2413
+ var SKIP_DIRS = new Set([".lecodes", ".git", "node_modules"]);
2414
+ var SKIP_FILES = new Set(["tsconfig.json", "jsconfig.json", ".gitignore", ".lecodesignore", ".DS_Store"]);
2415
+ var TEXT_EXTS = new Set([
2416
+ "ts",
2417
+ "tsx",
2418
+ "js",
2419
+ "jsx",
2420
+ "mjs",
2421
+ "cjs",
2422
+ "json",
2423
+ "svg",
2424
+ "glsl",
2425
+ "vert",
2426
+ "frag",
2427
+ "txt",
2428
+ "md",
2429
+ "css",
2430
+ "html",
2431
+ "xml",
2432
+ "yml",
2433
+ "yaml",
2434
+ "csv"
2435
+ ]);
2436
+ var ext = (name) => {
2437
+ const i = name.lastIndexOf(".");
2438
+ return i < 0 ? "" : name.slice(i + 1).toLowerCase();
1737
2439
  };
1738
- var readManifest = (root) => {
1739
- try {
1740
- return JSON.parse(readFileSync2(manifestPath(root), "utf8"));
1741
- } catch {
1742
- throw new CliError("Could not read .lecodes/manifest.json — is this a lecodes project?");
1743
- }
2440
+ var classifyKind = (name) => {
2441
+ const e = ext(name);
2442
+ if (e === "mat")
2443
+ return "shader";
2444
+ if (e === "scene")
2445
+ return "scene";
2446
+ return TEXT_EXTS.has(e) ? "text" : "resource";
1744
2447
  };
1745
- var writeManifest = (root, manifest) => {
1746
- mkdirSync2(join2(root, LECODES_DIR), { recursive: true });
1747
- writeFileSync2(manifestPath(root), JSON.stringify(manifest, null, 2));
2448
+ var isTextKind = (kind) => kind !== "resource";
2449
+ var scanLocal = (root, opts = {}) => {
2450
+ const { ignore, tracked } = opts;
2451
+ const files = [];
2452
+ const dirs = [];
2453
+ const ignored = [];
2454
+ const isTracked = (key) => tracked?.has(key) ?? false;
2455
+ const hasTrackedUnder = (key) => {
2456
+ if (!tracked)
2457
+ return false;
2458
+ const prefix = key + "/";
2459
+ for (const t of tracked)
2460
+ if (t.startsWith(prefix))
2461
+ return true;
2462
+ return false;
2463
+ };
2464
+ const walk = (dir, parentIgnored) => {
2465
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
2466
+ const abs = join4(dir, entry.name);
2467
+ const key = toKey(relative(root, abs));
2468
+ if (entry.isDirectory()) {
2469
+ if (SKIP_DIRS.has(entry.name))
2470
+ continue;
2471
+ const dirIgnored = parentIgnored || !!ignore && ignore(key, true);
2472
+ if (dirIgnored && !isTracked(key) && !hasTrackedUnder(key)) {
2473
+ ignored.push(key + "/");
2474
+ continue;
2475
+ }
2476
+ dirs.push(key);
2477
+ walk(abs, dirIgnored);
2478
+ } else if (entry.isFile()) {
2479
+ if (SKIP_FILES.has(entry.name))
2480
+ continue;
2481
+ const fileIgnored = parentIgnored || !!ignore && ignore(key, false);
2482
+ if (fileIgnored && !isTracked(key)) {
2483
+ ignored.push(key);
2484
+ continue;
2485
+ }
2486
+ files.push({ path: key, absPath: abs, sha: sha256(readFileSync4(abs)) });
2487
+ }
2488
+ }
2489
+ };
2490
+ walk(root, false);
2491
+ return { files, dirs, ignored };
2492
+ };
2493
+ var scanProject = (root, manifest) => scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest.files)) });
2494
+ var diffLocal = (manifest, scan) => {
2495
+ const added = [];
2496
+ const modified = [];
2497
+ const localByPath = new Map(scan.files.map((f) => [f.path, f]));
2498
+ for (const file of scan.files) {
2499
+ const known = manifest.files[file.path];
2500
+ if (!known)
2501
+ added.push(file);
2502
+ else if (known.sha !== file.sha)
2503
+ modified.push(file);
2504
+ }
2505
+ const deleted = Object.keys(manifest.files).filter((path) => !localByPath.has(path));
2506
+ const moves = new Map;
2507
+ const claimed = new Set;
2508
+ for (const file of added) {
2509
+ const from = deleted.find((path) => !claimed.has(path) && manifest.files[path].sha === file.sha);
2510
+ if (from) {
2511
+ moves.set(file.path, from);
2512
+ claimed.add(from);
2513
+ }
2514
+ }
2515
+ return { added, modified, deleted, moves };
1748
2516
  };
2517
+ var hasChanges = (diff) => diff.added.length + diff.modified.length + diff.deleted.length > 0;
1749
2518
 
1750
2519
  // src/project.ts
1751
- import { dirname as dirname2, join as join3 } from "node:path";
1752
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
2520
+ import { dirname as dirname2, join as join5 } from "node:path";
2521
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "node:fs";
1753
2522
  var kindOf = (type) => type === "file" ? "resource" : type;
1754
- var absOf = (root, key) => join3(root, ...key.replace(/^\//, "").split("/"));
2523
+ var absOf = (root, key) => join5(root, ...key.replace(/^\//, "").split("/"));
1755
2524
  var buildPaths = (assets) => {
1756
2525
  const pathById = new Map;
1757
2526
  for (const asset of assets) {
@@ -1778,11 +2547,11 @@ var materializeAssets = async (root, project, apiUrl, token, onProgress) => {
1778
2547
  if (!asset.file?.src)
1779
2548
  continue;
1780
2549
  const bytes = await downloadBlob(apiUrl, asset.file.src);
1781
- writeFileSync3(abs, bytes);
2550
+ writeFileSync4(abs, bytes);
1782
2551
  sha = sha256(bytes);
1783
2552
  } else {
1784
2553
  const text = asset.text ?? "";
1785
- writeFileSync3(abs, text);
2554
+ writeFileSync4(abs, text);
1786
2555
  sha = sha256(text);
1787
2556
  }
1788
2557
  files[key] = { id: asset.id, type: kindOf(asset.type), sha };
@@ -1791,27 +2560,1538 @@ var materializeAssets = async (root, project, apiUrl, token, onProgress) => {
1791
2560
  return files;
1792
2561
  };
1793
2562
 
1794
- // src/types.ts
1795
- import { dirname as dirname4, join as join5 } from "node:path";
1796
- import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
1797
-
1798
- // src/distRoot.ts
1799
- import { existsSync as existsSync2 } from "node:fs";
1800
- import { dirname as dirname3, join as join4 } from "node:path";
1801
- import { fileURLToPath } from "node:url";
1802
- var distRoot = (() => {
1803
- const candidates = [
1804
- join4(dirname3(fileURLToPath(import.meta.url)), ".."),
1805
- dirname3(process.execPath)
1806
- ];
1807
- for (const c2 of candidates)
1808
- if (existsSync2(join4(c2, "runtime")))
1809
- return c2;
1810
- return candidates[0];
1811
- })();
1812
-
1813
- // src/types.ts
1814
- var TSCONFIG = {
2563
+ // src/compile/collect.ts
2564
+ var collectEntries = (root, manifest, project, opts = {}) => {
2565
+ const pathById = buildPaths(project.assets);
2566
+ const serverByPath = new Map;
2567
+ for (const asset of project.assets) {
2568
+ if (asset.type === "folder")
2569
+ continue;
2570
+ serverByPath.set(pathById.get(asset.id), { fileSrc: asset.file?.src ?? undefined, shaders: asset.shaders ?? [] });
2571
+ }
2572
+ const entries = [];
2573
+ const warnings = [];
2574
+ const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest.files)) });
2575
+ for (const file of scan.files) {
2576
+ const type = manifest.files[file.path]?.type ?? classifyKind(basename(file.path));
2577
+ if (type === "resource") {
2578
+ const src = serverByPath.get(file.path)?.fileSrc;
2579
+ if (!src && (opts.warnMissingUrl ?? true))
2580
+ warnings.push(`No server URL for ${file.path} — push it so its URL resolves.`);
2581
+ entries.push({ path: file.path, type, fileSrc: src, absPath: file.absPath });
2582
+ } else if (type === "shader") {
2583
+ const shaders = serverByPath.get(file.path)?.shaders;
2584
+ if (!shaders?.length)
2585
+ warnings.push(`No compiled shader for ${file.path} — push it to compile the shader.`);
2586
+ entries.push({ path: file.path, type, shaders });
2587
+ } else {
2588
+ entries.push({ path: file.path, type, text: readFileSync5(file.absPath, "utf8") });
2589
+ }
2590
+ }
2591
+ return { entries, warnings };
2592
+ };
2593
+
2594
+ // src/compile/collectLocal.ts
2595
+ import { readFileSync as readFileSync6 } from "node:fs";
2596
+ import { basename as basename2 } from "node:path";
2597
+ var collectLocalEntries = (root, manifest, assetBase) => {
2598
+ const entries = [];
2599
+ const warnings = [];
2600
+ const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest?.files ?? {})) });
2601
+ for (const file of scan.files) {
2602
+ const type = manifest?.files[file.path]?.type ?? classifyKind(basename2(file.path));
2603
+ if (type === "resource") {
2604
+ entries.push({ path: file.path, type, fileSrc: assetBase + encodeURI(file.path), absPath: file.absPath });
2605
+ } else if (type === "shader") {
2606
+ warnings.push(`${file.path}: shaders compile on the server — push the project once for shaders to work in dev mode.`);
2607
+ entries.push({ path: file.path, type, shaders: [] });
2608
+ } else {
2609
+ entries.push({ path: file.path, type, text: readFileSync6(file.absPath, "utf8") });
2610
+ }
2611
+ }
2612
+ return { entries, warnings };
2613
+ };
2614
+
2615
+ // src/compile/projectCompile.ts
2616
+ import { existsSync as existsSync6 } from "node:fs";
2617
+ import { createRequire as createRequire3 } from "node:module";
2618
+ import { dirname as dirname5, join as join9 } from "node:path";
2619
+
2620
+ // src/distRoot.ts
2621
+ import { existsSync as existsSync3 } from "node:fs";
2622
+ import { dirname as dirname3, join as join6 } from "node:path";
2623
+ import { fileURLToPath } from "node:url";
2624
+ var distRoot = (() => {
2625
+ const candidates = [
2626
+ join6(dirname3(fileURLToPath(import.meta.url)), ".."),
2627
+ dirname3(process.execPath)
2628
+ ];
2629
+ for (const c2 of candidates)
2630
+ if (existsSync3(join6(c2, "runtime")))
2631
+ return c2;
2632
+ return candidates[0];
2633
+ })();
2634
+
2635
+ // src/compile/projectCompile.ts
2636
+ init_compile();
2637
+ var vendoredInject = join9(distRoot, "runtime", "sdk", "inject.ts");
2638
+ if (existsSync6(vendoredInject))
2639
+ process.env.LECODES_SDK_INJECT = vendoredInject;
2640
+ var resolveChiselBin2 = () => {
2641
+ if (process.env.LECODES_CHISEL_BIN)
2642
+ return process.env.LECODES_CHISEL_BIN;
2643
+ try {
2644
+ const fromPkg = createRequire3(import.meta.url)("@letary/chisel").binaryPath();
2645
+ if (fromPkg)
2646
+ return fromPkg;
2647
+ } catch {}
2648
+ const local = join9(dirname5(process.execPath), process.platform === "win32" ? "chisel.exe" : "chisel");
2649
+ return existsSync6(local) ? local : null;
2650
+ };
2651
+ var chiselBin = resolveChiselBin2();
2652
+ if (chiselBin)
2653
+ process.env.LECODES_CHISEL_BIN = chiselBin;
2654
+ var compileProject2 = (opts) => compileProject({
2655
+ entries: opts.entries.map((e) => ({ path: e.path, type: e.type, text: e.text, fileSrc: e.fileSrc, remote: e.remote, shaders: e.shaders })),
2656
+ name: opts.name,
2657
+ publicUrl: opts.publicUrl,
2658
+ entryOverride: opts.entryOverride,
2659
+ header: opts.header,
2660
+ fontsHeader: opts.fontsHeader,
2661
+ format: opts.format,
2662
+ localAssets: opts.localAssets,
2663
+ minify: opts.minify,
2664
+ sourcemap: opts.sourcemap,
2665
+ editor: opts.editor
2666
+ });
2667
+
2668
+ // src/compile/fonts.ts
2669
+ init_compile();
2670
+ import { createHash as createHash2 } from "node:crypto";
2671
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
2672
+ import { dirname as dirname6, join as join10 } from "node:path";
2673
+ var applyProjectFonts = async (entries, opts) => {
2674
+ const registry = (opts.registry ?? fontRegistryBase()).replace(/\/+$/, "");
2675
+ const byPath = () => new Map(entries.map((e) => [e.path.replace(/^\//, ""), e]));
2676
+ const faces = await applyFontMacro(entries, {
2677
+ mode: opts.mode,
2678
+ registryBase: registry,
2679
+ readBytes: (entryPath) => {
2680
+ const entry = byPath().get(entryPath);
2681
+ const abs = entry?.absPath ?? join10(opts.root, entryPath);
2682
+ try {
2683
+ return new Uint8Array(readFileSync9(abs));
2684
+ } catch {
2685
+ return null;
2686
+ }
2687
+ },
2688
+ vendorFace: opts.mode === "vendor" ? async (face) => {
2689
+ const vendorDir = opts.vendorDir ?? opts.root;
2690
+ const rel = `assets/fonts/${face.fileName}`;
2691
+ const abs = join10(vendorDir, rel);
2692
+ const entryPath = `${opts.vendorPrefix ?? ""}/${rel}`;
2693
+ if (!existsSync7(abs))
2694
+ await downloadFace(registry, vendorDir, face, rel, abs);
2695
+ if (!byPath().has(entryPath.replace(/^\//, ""))) {
2696
+ entries.push(opts.makeResourceEntry?.(entryPath, abs) ?? { path: entryPath, type: "resource", absPath: abs });
2697
+ }
2698
+ return entryPath;
2699
+ } : undefined
2700
+ });
2701
+ if (faces.length === 0)
2702
+ return;
2703
+ return buildFontsHeaderValue(faces, (path) => {
2704
+ if (!opts.publicUrl || !/^https?:/i.test(opts.publicUrl))
2705
+ return null;
2706
+ const fileSrc = byPath().get(path.replace(/^\//, ""))?.fileSrc;
2707
+ return fileSrc ? opts.publicUrl + fileSrc : null;
2708
+ }) || undefined;
2709
+ };
2710
+ var isHttp = (s) => /^https?:/i.test(s);
2711
+ var sha12 = (b) => createHash2("sha256").update(b).digest("hex").slice(0, 12);
2712
+ var registryHome = (registry) => registry.replace(/\/v\d+$/, "");
2713
+ var readRegistry = async (registry, rel) => {
2714
+ if (isHttp(registry)) {
2715
+ const res = await fetch(`${registry}/${rel}`);
2716
+ return res.ok ? new Uint8Array(await res.arrayBuffer()) : null;
2717
+ }
2718
+ try {
2719
+ return new Uint8Array(readFileSync9(join10(registry, rel)));
2720
+ } catch {
2721
+ return null;
2722
+ }
2723
+ };
2724
+ var readLock = (path) => {
2725
+ try {
2726
+ const lock = JSON.parse(readFileSync9(path, "utf8"));
2727
+ if (lock && typeof lock === "object" && lock.faces && lock.families)
2728
+ return lock;
2729
+ } catch {}
2730
+ return null;
2731
+ };
2732
+ var serializeLock = (lock) => {
2733
+ const sortKeys = (rec) => Object.fromEntries(Object.keys(rec).sort().map((k) => [k, rec[k]]));
2734
+ return JSON.stringify({ registry: lock.registry, faces: sortKeys(lock.faces), families: sortKeys(lock.families) }, null, 2) + `
2735
+ `;
2736
+ };
2737
+ var renderNotices = (lock) => {
2738
+ const ids = [...new Set(Object.values(lock.faces).map((f2) => f2.id))].sort();
2739
+ const lines = [
2740
+ "# Font attribution & notices",
2741
+ "",
2742
+ "Fonts vendored from the font registry via `font(...)`. Keep this file in your repo / product.",
2743
+ ""
2744
+ ];
2745
+ for (const id of ids) {
2746
+ const l = lock.families[id];
2747
+ if (!l)
2748
+ continue;
2749
+ let note2 = `- **${l.title || id}** — ${l.spdx}${l.url ? ` — ${l.url}` : ""}`;
2750
+ if (l.attribution)
2751
+ note2 += `
2752
+ - **Attribution required:** ${l.attribution}`;
2753
+ lines.push(note2);
2754
+ }
2755
+ lines.push("");
2756
+ return lines.join(`
2757
+ `);
2758
+ };
2759
+ var writeIfChanged = (path, text) => {
2760
+ try {
2761
+ if (readFileSync9(path, "utf8") === text)
2762
+ return;
2763
+ } catch {}
2764
+ writeFileSync5(path, text);
2765
+ };
2766
+ var downloadFace = async (registry, vendorDir, face, rel, abs) => {
2767
+ const licRaw = await readRegistry(registry, "licenses.json");
2768
+ if (!licRaw)
2769
+ throw new Error(`font: can't reach the font registry (${registry}). Check your connection, or set LECODES_FONT_REGISTRY.`);
2770
+ const licenses = JSON.parse(new TextDecoder().decode(licRaw));
2771
+ const lic = licenses[face.id];
2772
+ if (!lic)
2773
+ throw new Error(`font("${face.id}"): the registry has no license entry for this family — it may not be published yet. Browse ${registryHome(registry)}.`);
2774
+ const bytes = await readRegistry(registry, `${face.id}/${face.fileName}`);
2775
+ if (!bytes)
2776
+ throw new Error(`font("${face.id}"): face "${face.fileName}" not found in the registry (${registryHome(registry)}).`);
2777
+ mkdirSync4(dirname6(abs), { recursive: true });
2778
+ writeFileSync5(abs, bytes);
2779
+ const lockPath = join10(vendorDir, "fonts.lock.json");
2780
+ const lock = readLock(lockPath) ?? { registry, faces: {}, families: {} };
2781
+ lock.registry = registry;
2782
+ lock.faces[`${face.id}@${face.weight}${face.italic ? "i" : ""}`] = { id: face.id, family: face.family, weight: face.weight, italic: face.italic, hash: sha12(bytes), spdx: lic.spdx, path: rel };
2783
+ lock.families[face.id] = lic;
2784
+ writeIfChanged(lockPath, serializeLock(lock));
2785
+ writeIfChanged(join10(vendorDir, "FONT-NOTICES.md"), renderNotices(lock));
2786
+ };
2787
+
2788
+ // src/commands/compile.ts
2789
+ var compile2 = async (args) => {
2790
+ await compileImpl(args);
2791
+ };
2792
+ var compileShell = (args, shell) => compileImpl(args, shell);
2793
+ var compileImpl = async (args, shell) => {
2794
+ let root;
2795
+ let manifest;
2796
+ try {
2797
+ root = findProjectRoot(process.cwd());
2798
+ manifest = readManifest(root);
2799
+ } catch {
2800
+ root = process.cwd();
2801
+ manifest = null;
2802
+ }
2803
+ let entries;
2804
+ let warnings;
2805
+ let name;
2806
+ let publicUrl;
2807
+ let localAssets = false;
2808
+ if (manifest) {
2809
+ const config = loadConfig();
2810
+ const apiUrl = requireApiUrl(config);
2811
+ const token = requireToken(config);
2812
+ note("Resolving project assets…");
2813
+ const project = await getProject(apiUrl, token, manifest.uuid);
2814
+ ({ entries, warnings } = collectEntries(root, manifest, project, { warnMissingUrl: !shell }));
2815
+ name = manifest.name;
2816
+ publicUrl = flagStr(args, "public-url") ?? apiUrl;
2817
+ localAssets = !!shell;
2818
+ if (shell && shell.remotePatterns.length > 0) {
2819
+ const isRemote = buildGlobMatcher(shell.remotePatterns);
2820
+ for (const e of entries) {
2821
+ if (e.type !== "resource" || !isRemote(e.path))
2822
+ continue;
2823
+ if (!e.fileSrc) {
2824
+ warnErr(`${e.path} matches resources.remote but has no server URL yet — bundling it (push, then re-sync).`);
2825
+ continue;
2826
+ }
2827
+ e.remote = true;
2828
+ }
2829
+ }
2830
+ } else {
2831
+ const publicUrlFlag = flagStr(args, "public-url");
2832
+ localAssets = shell ? true : publicUrlFlag === undefined;
2833
+ publicUrl = publicUrlFlag ?? "";
2834
+ ({ entries, warnings } = collectLocalEntries(root, null, ""));
2835
+ name = basename3(root);
2836
+ if (shell && shell.remotePatterns.length > 0) {
2837
+ warnErr("resources.remote needs a server-backed project (a streamed asset needs a server URL) — bundling everything.");
2838
+ }
2839
+ }
2840
+ const fontsHeader = await applyProjectFonts(entries, manifest && !shell ? { mode: "cdn", root, publicUrl } : {
2841
+ mode: "vendor",
2842
+ root,
2843
+ vendorDir: root,
2844
+ vendorPrefix: "",
2845
+ publicUrl: shell ? "" : publicUrl,
2846
+ makeResourceEntry: (path, absPath) => ({ path, type: "resource", fileSrc: encodeURI(path), absPath })
2847
+ });
2848
+ note("Compiling…");
2849
+ const js = await compileProject2({
2850
+ entries,
2851
+ name,
2852
+ publicUrl,
2853
+ entryOverride: flagStr(args, "entry"),
2854
+ header: !flagBool(args, "no-header"),
2855
+ localAssets,
2856
+ fontsHeader,
2857
+ minify: flagBool(args, "no-minify") ? false : undefined,
2858
+ sourcemap: flagBool(args, "no-sourcemap", "no-source-map") ? false : undefined
2859
+ });
2860
+ for (const w of warnings)
2861
+ warnErr(w);
2862
+ const out = flagStr(args, "o", "out");
2863
+ if (out && isNullSink(out)) {
2864
+ note(`Discarded output (${out})`);
2865
+ } else if (out) {
2866
+ writeFileSync6(resolve(out), js);
2867
+ note(`Wrote ${out} (${js.length} bytes)`);
2868
+ } else {
2869
+ process.stdout.write(js);
2870
+ if (!js.endsWith(`
2871
+ `))
2872
+ process.stdout.write(`
2873
+ `);
2874
+ }
2875
+ return entries.filter((e) => e.type === "resource").map((e) => ({
2876
+ path: e.path.replace(/^\//, ""),
2877
+ name: e.path.split(/[\\/]/).pop() ?? e.path,
2878
+ absPath: e.absPath,
2879
+ remote: !!e.remote
2880
+ }));
2881
+ };
2882
+ var previewLegacy = async (args) => {
2883
+ warnErr("'lecodes preview' is now 'lecodes compile' — the old name still works.");
2884
+ await compile2(args);
2885
+ };
2886
+
2887
+ // src/commands/appTemplates.ts
2888
+ var pbxStr = (s) => s.replace(/[\\"]/g, (m) => "\\" + m);
2889
+ var pbxprojTemplate = ({ displayName, bundleId, teamId, version, buildNumber }) => {
2890
+ const target = `
2891
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
2892
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
2893
+ CODE_SIGN_STYLE = Automatic;
2894
+ CURRENT_PROJECT_VERSION = ${buildNumber};
2895
+ DEVELOPMENT_TEAM = "${pbxStr(teamId)}";
2896
+ GENERATE_INFOPLIST_FILE = YES;
2897
+ INFOPLIST_FILE = "App/Info.plist";
2898
+ INFOPLIST_KEY_CFBundleDisplayName = "${pbxStr(displayName)}";
2899
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
2900
+ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
2901
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
2902
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
2903
+ IPHONEOS_DEPLOYMENT_TARGET = 15.6;
2904
+ LD_RUNPATH_SEARCH_PATHS = (
2905
+ "$(inherited)",
2906
+ "@executable_path/Frameworks",
2907
+ );
2908
+ MARKETING_VERSION = ${version};
2909
+ PRODUCT_BUNDLE_IDENTIFIER = "${pbxStr(bundleId)}";
2910
+ PRODUCT_NAME = "$(TARGET_NAME)";
2911
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
2912
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
2913
+ SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
2914
+ SWIFT_EMIT_LOC_STRINGS = YES;
2915
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
2916
+ SWIFT_VERSION = 5.0;
2917
+ TARGETED_DEVICE_FAMILY = "1,2";`;
2918
+ const project = (debug) => `
2919
+ ALWAYS_SEARCH_USER_PATHS = NO;
2920
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
2921
+ CLANG_ANALYZER_NONNULL = YES;
2922
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
2923
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
2924
+ CLANG_ENABLE_MODULES = YES;
2925
+ CLANG_ENABLE_OBJC_ARC = YES;
2926
+ CLANG_ENABLE_OBJC_WEAK = YES;
2927
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
2928
+ CLANG_WARN_BOOL_CONVERSION = YES;
2929
+ CLANG_WARN_COMMA = YES;
2930
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
2931
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
2932
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
2933
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
2934
+ CLANG_WARN_EMPTY_BODY = YES;
2935
+ CLANG_WARN_ENUM_CONVERSION = YES;
2936
+ CLANG_WARN_INFINITE_RECURSION = YES;
2937
+ CLANG_WARN_INT_CONVERSION = YES;
2938
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
2939
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
2940
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
2941
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
2942
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
2943
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
2944
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
2945
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
2946
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
2947
+ CLANG_WARN_UNREACHABLE_CODE = YES;
2948
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
2949
+ COPY_PHASE_STRIP = NO;
2950
+ DEBUG_INFORMATION_FORMAT = ${debug ? "dwarf" : '"dwarf-with-dsym"'};
2951
+ DEVELOPMENT_TEAM = "${pbxStr(teamId)}";${debug ? "" : `
2952
+ ENABLE_NS_ASSERTIONS = NO;`}
2953
+ ENABLE_STRICT_OBJC_MSGSEND = YES;${debug ? `
2954
+ ENABLE_TESTABILITY = YES;` : ""}
2955
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
2956
+ GCC_C_LANGUAGE_STANDARD = gnu17;${debug ? `
2957
+ GCC_DYNAMIC_NO_PIC = NO;` : ""}
2958
+ GCC_NO_COMMON_BLOCKS = YES;${debug ? `
2959
+ GCC_OPTIMIZATION_LEVEL = 0;
2960
+ GCC_PREPROCESSOR_DEFINITIONS = (
2961
+ "DEBUG=1",
2962
+ "$(inherited)",
2963
+ );` : ""}
2964
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
2965
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
2966
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
2967
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
2968
+ GCC_WARN_UNUSED_FUNCTION = YES;
2969
+ GCC_WARN_UNUSED_VARIABLE = YES;
2970
+ IPHONEOS_DEPLOYMENT_TARGET = 15.6;
2971
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
2972
+ MTL_ENABLE_DEBUG_INFO = ${debug ? "INCLUDE_SOURCE" : "NO"};
2973
+ MTL_FAST_MATH = YES;${debug ? `
2974
+ ONLY_ACTIVE_ARCH = YES;` : ""}
2975
+ SDKROOT = iphoneos;${debug ? `
2976
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
2977
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";` : `
2978
+ SWIFT_COMPILATION_MODE = wholemodule;
2979
+ VALIDATE_PRODUCT = YES;`}`;
2980
+ return `// !$*UTF8*$!
2981
+ {
2982
+ archiveVersion = 1;
2983
+ classes = {
2984
+ };
2985
+ objectVersion = 77;
2986
+ objects = {
2987
+
2988
+ /* Begin PBXBuildFile section */
2989
+ B73550812FEBAC710005B4CC /* LeCodesRuntime in Frameworks */ = {isa = PBXBuildFile; productRef = B73550802FEBAC710005B4CC /* LeCodesRuntime */; };
2990
+ /* End PBXBuildFile section */
2991
+
2992
+ /* Begin PBXFileReference section */
2993
+ B73550672FEBAC0F0005B4CC /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
2994
+ /* End PBXFileReference section */
2995
+
2996
+ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
2997
+ B73550792FEBAC100005B4CC /* Exceptions for "App" folder in "App" target */ = {
2998
+ isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
2999
+ membershipExceptions = (
3000
+ Info.plist,
3001
+ );
3002
+ target = B73550662FEBAC0F0005B4CC /* App */;
3003
+ };
3004
+ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
3005
+
3006
+ /* Begin PBXFileSystemSynchronizedRootGroup section */
3007
+ B73550692FEBAC0F0005B4CC /* App */ = {
3008
+ isa = PBXFileSystemSynchronizedRootGroup;
3009
+ exceptions = (
3010
+ B73550792FEBAC100005B4CC /* Exceptions for "App" folder in "App" target */,
3011
+ );
3012
+ path = App;
3013
+ sourceTree = "<group>";
3014
+ };
3015
+ /* End PBXFileSystemSynchronizedRootGroup section */
3016
+
3017
+ /* Begin PBXFrameworksBuildPhase section */
3018
+ B73550642FEBAC0F0005B4CC /* Frameworks */ = {
3019
+ isa = PBXFrameworksBuildPhase;
3020
+ buildActionMask = 2147483647;
3021
+ files = (
3022
+ B73550812FEBAC710005B4CC /* LeCodesRuntime in Frameworks */,
3023
+ );
3024
+ runOnlyForDeploymentPostprocessing = 0;
3025
+ };
3026
+ /* End PBXFrameworksBuildPhase section */
3027
+
3028
+ /* Begin PBXGroup section */
3029
+ B735505E2FEBAC0F0005B4CC = {
3030
+ isa = PBXGroup;
3031
+ children = (
3032
+ B73550692FEBAC0F0005B4CC /* App */,
3033
+ B73550682FEBAC0F0005B4CC /* Products */,
3034
+ );
3035
+ sourceTree = "<group>";
3036
+ };
3037
+ B73550682FEBAC0F0005B4CC /* Products */ = {
3038
+ isa = PBXGroup;
3039
+ children = (
3040
+ B73550672FEBAC0F0005B4CC /* App.app */,
3041
+ );
3042
+ name = Products;
3043
+ sourceTree = "<group>";
3044
+ };
3045
+ /* End PBXGroup section */
3046
+
3047
+ /* Begin PBXNativeTarget section */
3048
+ B73550662FEBAC0F0005B4CC /* App */ = {
3049
+ isa = PBXNativeTarget;
3050
+ buildConfigurationList = B735507A2FEBAC100005B4CC /* Build configuration list for PBXNativeTarget "App" */;
3051
+ buildPhases = (
3052
+ B73550632FEBAC0F0005B4CC /* Sources */,
3053
+ B73550642FEBAC0F0005B4CC /* Frameworks */,
3054
+ B73550652FEBAC0F0005B4CC /* Resources */,
3055
+ );
3056
+ buildRules = (
3057
+ );
3058
+ dependencies = (
3059
+ );
3060
+ fileSystemSynchronizedGroups = (
3061
+ B73550692FEBAC0F0005B4CC /* App */,
3062
+ );
3063
+ name = App;
3064
+ packageProductDependencies = (
3065
+ B73550802FEBAC710005B4CC /* LeCodesRuntime */,
3066
+ );
3067
+ productName = App;
3068
+ productReference = B73550672FEBAC0F0005B4CC /* App.app */;
3069
+ productType = "com.apple.product-type.application";
3070
+ };
3071
+ /* End PBXNativeTarget section */
3072
+
3073
+ /* Begin PBXProject section */
3074
+ B735505F2FEBAC0F0005B4CC /* Project object */ = {
3075
+ isa = PBXProject;
3076
+ attributes = {
3077
+ BuildIndependentTargetsInParallel = 1;
3078
+ LastSwiftUpdateCheck = 2650;
3079
+ LastUpgradeCheck = 2650;
3080
+ TargetAttributes = {
3081
+ B73550662FEBAC0F0005B4CC = {
3082
+ CreatedOnToolsVersion = 26.5;
3083
+ };
3084
+ };
3085
+ };
3086
+ buildConfigurationList = B73550622FEBAC0F0005B4CC /* Build configuration list for PBXProject "App" */;
3087
+ developmentRegion = en;
3088
+ hasScannedForEncodings = 0;
3089
+ knownRegions = (
3090
+ en,
3091
+ Base,
3092
+ );
3093
+ mainGroup = B735505E2FEBAC0F0005B4CC;
3094
+ minimizedProjectReferenceProxies = 1;
3095
+ packageReferences = (
3096
+ B73550822FEBAC720005B4CC /* XCLocalSwiftPackageReference "LeCodesRuntime" */,
3097
+ );
3098
+ preferredProjectObjectVersion = 77;
3099
+ productRefGroup = B73550682FEBAC0F0005B4CC /* Products */;
3100
+ projectDirPath = "";
3101
+ projectRoot = "";
3102
+ targets = (
3103
+ B73550662FEBAC0F0005B4CC /* App */,
3104
+ );
3105
+ };
3106
+ /* End PBXProject section */
3107
+
3108
+ /* Begin PBXResourcesBuildPhase section */
3109
+ B73550652FEBAC0F0005B4CC /* Resources */ = {
3110
+ isa = PBXResourcesBuildPhase;
3111
+ buildActionMask = 2147483647;
3112
+ files = (
3113
+ );
3114
+ runOnlyForDeploymentPostprocessing = 0;
3115
+ };
3116
+ /* End PBXResourcesBuildPhase section */
3117
+
3118
+ /* Begin PBXSourcesBuildPhase section */
3119
+ B73550632FEBAC0F0005B4CC /* Sources */ = {
3120
+ isa = PBXSourcesBuildPhase;
3121
+ buildActionMask = 2147483647;
3122
+ files = (
3123
+ );
3124
+ runOnlyForDeploymentPostprocessing = 0;
3125
+ };
3126
+ /* End PBXSourcesBuildPhase section */
3127
+
3128
+ /* Begin XCBuildConfiguration section */
3129
+ B735507B2FEBAC100005B4CC /* Debug */ = {
3130
+ isa = XCBuildConfiguration;
3131
+ buildSettings = {${target}
3132
+ };
3133
+ name = Debug;
3134
+ };
3135
+ B735507C2FEBAC100005B4CC /* Release */ = {
3136
+ isa = XCBuildConfiguration;
3137
+ buildSettings = {${target}
3138
+ };
3139
+ name = Release;
3140
+ };
3141
+ B735507D2FEBAC100005B4CC /* Debug */ = {
3142
+ isa = XCBuildConfiguration;
3143
+ buildSettings = {${project(true)}
3144
+ };
3145
+ name = Debug;
3146
+ };
3147
+ B735507E2FEBAC100005B4CC /* Release */ = {
3148
+ isa = XCBuildConfiguration;
3149
+ buildSettings = {${project(false)}
3150
+ };
3151
+ name = Release;
3152
+ };
3153
+ /* End XCBuildConfiguration section */
3154
+
3155
+ /* Begin XCConfigurationList section */
3156
+ B73550622FEBAC0F0005B4CC /* Build configuration list for PBXProject "App" */ = {
3157
+ isa = XCConfigurationList;
3158
+ buildConfigurations = (
3159
+ B735507D2FEBAC100005B4CC /* Debug */,
3160
+ B735507E2FEBAC100005B4CC /* Release */,
3161
+ );
3162
+ defaultConfigurationIsVisible = 0;
3163
+ defaultConfigurationName = Release;
3164
+ };
3165
+ B735507A2FEBAC100005B4CC /* Build configuration list for PBXNativeTarget "App" */ = {
3166
+ isa = XCConfigurationList;
3167
+ buildConfigurations = (
3168
+ B735507B2FEBAC100005B4CC /* Debug */,
3169
+ B735507C2FEBAC100005B4CC /* Release */,
3170
+ );
3171
+ defaultConfigurationIsVisible = 0;
3172
+ defaultConfigurationName = Release;
3173
+ };
3174
+ /* End XCConfigurationList section */
3175
+
3176
+ /* Begin XCLocalSwiftPackageReference section */
3177
+ B73550822FEBAC720005B4CC /* XCLocalSwiftPackageReference "LeCodesRuntime" */ = {
3178
+ isa = XCLocalSwiftPackageReference;
3179
+ relativePath = LeCodesRuntime;
3180
+ };
3181
+ /* End XCLocalSwiftPackageReference section */
3182
+
3183
+ /* Begin XCSwiftPackageProductDependency section */
3184
+ B73550802FEBAC710005B4CC /* LeCodesRuntime */ = {
3185
+ isa = XCSwiftPackageProductDependency;
3186
+ productName = LeCodesRuntime;
3187
+ };
3188
+ /* End XCSwiftPackageProductDependency section */
3189
+ };
3190
+ rootObject = B735505F2FEBAC0F0005B4CC /* Project object */;
3191
+ }
3192
+ `;
3193
+ };
3194
+ var SCHEME_XML = `<?xml version="1.0" encoding="UTF-8"?>
3195
+ <Scheme
3196
+ LastUpgradeVersion = "2650"
3197
+ version = "1.7">
3198
+ <BuildAction
3199
+ parallelizeBuildables = "YES"
3200
+ buildImplicitDependencies = "YES">
3201
+ <BuildActionEntries>
3202
+ <BuildActionEntry
3203
+ buildForTesting = "YES"
3204
+ buildForRunning = "YES"
3205
+ buildForProfiling = "YES"
3206
+ buildForArchiving = "YES"
3207
+ buildForAnalyzing = "YES">
3208
+ <BuildableReference
3209
+ BuildableIdentifier = "primary"
3210
+ BlueprintIdentifier = "B73550662FEBAC0F0005B4CC"
3211
+ BuildableName = "App.app"
3212
+ BlueprintName = "App"
3213
+ ReferencedContainer = "container:App.xcodeproj">
3214
+ </BuildableReference>
3215
+ </BuildActionEntry>
3216
+ </BuildActionEntries>
3217
+ </BuildAction>
3218
+ <TestAction
3219
+ buildConfiguration = "Debug"
3220
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
3221
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
3222
+ shouldUseLaunchSchemeArgsEnv = "YES">
3223
+ <Testables>
3224
+ </Testables>
3225
+ </TestAction>
3226
+ <LaunchAction
3227
+ buildConfiguration = "Debug"
3228
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
3229
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
3230
+ launchStyle = "0"
3231
+ useCustomWorkingDirectory = "NO"
3232
+ ignoresPersistentStateOnLaunch = "NO"
3233
+ debugDocumentVersioning = "YES"
3234
+ debugServiceExtension = "internal"
3235
+ allowLocationSimulation = "YES">
3236
+ <BuildableProductRunnable
3237
+ runnableDebuggingMode = "0">
3238
+ <BuildableReference
3239
+ BuildableIdentifier = "primary"
3240
+ BlueprintIdentifier = "B73550662FEBAC0F0005B4CC"
3241
+ BuildableName = "App.app"
3242
+ BlueprintName = "App"
3243
+ ReferencedContainer = "container:App.xcodeproj">
3244
+ </BuildableReference>
3245
+ </BuildableProductRunnable>
3246
+ </LaunchAction>
3247
+ <ProfileAction
3248
+ buildConfiguration = "Release"
3249
+ shouldUseLaunchSchemeArgsEnv = "YES"
3250
+ savedToolIdentifier = ""
3251
+ useCustomWorkingDirectory = "NO"
3252
+ debugDocumentVersioning = "YES">
3253
+ <BuildableProductRunnable
3254
+ runnableDebuggingMode = "0">
3255
+ <BuildableReference
3256
+ BuildableIdentifier = "primary"
3257
+ BlueprintIdentifier = "B73550662FEBAC0F0005B4CC"
3258
+ BuildableName = "App.app"
3259
+ BlueprintName = "App"
3260
+ ReferencedContainer = "container:App.xcodeproj">
3261
+ </BuildableReference>
3262
+ </BuildableProductRunnable>
3263
+ </ProfileAction>
3264
+ <AnalyzeAction
3265
+ buildConfiguration = "Debug">
3266
+ </AnalyzeAction>
3267
+ <ArchiveAction
3268
+ buildConfiguration = "Release"
3269
+ revealArchiveInOrganizer = "YES">
3270
+ </ArchiveAction>
3271
+ </Scheme>
3272
+ `;
3273
+ var APP_DELEGATE = `//
3274
+ // AppDelegate.swift
3275
+ // Generated by lecodes app init — yours to edit.
3276
+ //
3277
+
3278
+ import UIKit
3279
+
3280
+ @main
3281
+ class AppDelegate: UIResponder, UIApplicationDelegate {
3282
+
3283
+ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
3284
+ return true
3285
+ }
3286
+
3287
+ // MARK: UISceneSession Lifecycle
3288
+
3289
+ func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
3290
+ return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
3291
+ }
3292
+ }
3293
+ `;
3294
+ var SCENE_DELEGATE = `//
3295
+ // SceneDelegate.swift
3296
+ // Generated by lecodes app init — yours to edit.
3297
+ //
3298
+
3299
+ import UIKit
3300
+
3301
+ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
3302
+
3303
+ var window: UIWindow?
3304
+
3305
+ func scene(
3306
+ _ scene: UIScene,
3307
+ willConnectTo session: UISceneSession,
3308
+ options connectionOptions: UIScene.ConnectionOptions
3309
+ ) {
3310
+ guard let windowScene = (scene as? UIWindowScene) else { return }
3311
+
3312
+ let window = UIWindow(windowScene: windowScene)
3313
+ window.rootViewController = ViewController()
3314
+ self.window = window
3315
+ window.makeKeyAndVisible()
3316
+ }
3317
+ }
3318
+ `;
3319
+ var VIEW_CONTROLLER = `//
3320
+ // ViewController.swift
3321
+ // Generated by lecodes app init — yours to edit.
3322
+ //
3323
+ // Runs the bundled app.js (produced by \`lecodes app sync\`) offline: everything the
3324
+ // script needs is provided by the SDK core (_creatorUI / _creatorUtils / console)
3325
+ // plus the plugins registered below via LeCodesRuntime.
3326
+ //
3327
+
3328
+ import UIKit
3329
+ import LeCodesRuntime
3330
+
3331
+ class ViewController: LeCodesViewController {
3332
+
3333
+ override func viewDidLoad() {
3334
+ super.viewDidLoad()
3335
+
3336
+ let lang = Locale.preferredLanguages.first.map { String($0.prefix(2)) } ?? "en"
3337
+ engine.setLanguage(lang)
3338
+
3339
+ // \`// preload:\` assets (images, fonts) resolve out of the app bundle.
3340
+ engine.setFetchLocal { path in
3341
+ let ns = path as NSString
3342
+ let ext = ns.pathExtension
3343
+ let name = ns.deletingPathExtension
3344
+ guard let url = Bundle.main.url(forResource: name, withExtension: ext.isEmpty ? nil : ext) else {
3345
+ return nil
3346
+ }
3347
+ return try? Data(contentsOf: url)
3348
+ }
3349
+
3350
+ registerBundledPlugins(in: engine)
3351
+
3352
+ guard let url = Bundle.main.url(forResource: "app", withExtension: "js"),
3353
+ let code = try? String(contentsOf: url, encoding: .utf8) else {
3354
+ print("LeCodes: bundled app.js is missing — run \`lecodes app sync\`")
3355
+ return
3356
+ }
3357
+ engine.run(code)
3358
+ }
3359
+ }
3360
+ `;
3361
+ var infoPlistTemplate = (pluginKeys) => {
3362
+ const keys = Object.keys(pluginKeys).sort();
3363
+ const pluginBlock = keys.length === 0 ? "" : `
3364
+ <!-- lecodes:plugins — merged from installed plugin manifests by \`lecodes app sync\` -->
3365
+ ${keys.map((k) => ` <key>${k}</key>
3366
+ <string>${pluginKeys[k]}</string>`).join(`
3367
+ `)}`;
3368
+ return `<?xml version="1.0" encoding="UTF-8"?>
3369
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3370
+ <plist version="1.0">
3371
+ <dict>
3372
+ <key>UIApplicationSceneManifest</key>
3373
+ <dict>
3374
+ <key>UIApplicationSupportsMultipleScenes</key>
3375
+ <false/>
3376
+ <key>UISceneConfigurations</key>
3377
+ <dict>
3378
+ <key>UIWindowSceneSessionRoleApplication</key>
3379
+ <array>
3380
+ <dict>
3381
+ <key>UISceneConfigurationName</key>
3382
+ <string>Default Configuration</string>
3383
+ <key>UISceneDelegateClassName</key>
3384
+ <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
3385
+ </dict>
3386
+ </array>
3387
+ </dict>
3388
+ </dict>${pluginBlock}
3389
+ </dict>
3390
+ </plist>
3391
+ `;
3392
+ };
3393
+ var LAUNCH_STORYBOARD = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
3394
+ <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
3395
+ <dependencies>
3396
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
3397
+ <capability name="Safe area layout guides" minToolsVersion="9.0"/>
3398
+ <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
3399
+ </dependencies>
3400
+ <scenes>
3401
+ <!--View Controller-->
3402
+ <scene sceneID="EHf-IW-A2E">
3403
+ <objects>
3404
+ <viewController id="01J-lp-oVM" sceneMemberID="viewController">
3405
+ <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
3406
+ <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
3407
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
3408
+ <color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
3409
+ <viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
3410
+ </view>
3411
+ </viewController>
3412
+ <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
3413
+ </objects>
3414
+ <point key="canvasLocation" x="53" y="375"/>
3415
+ </scene>
3416
+ </scenes>
3417
+ </document>
3418
+ `;
3419
+ var ASSETS_ROOT_JSON = `{
3420
+ "info" : {
3421
+ "author" : "xcode",
3422
+ "version" : 1
3423
+ }
3424
+ }
3425
+ `;
3426
+ var ACCENT_COLOR_JSON = `{
3427
+ "colors" : [
3428
+ {
3429
+ "idiom" : "universal"
3430
+ }
3431
+ ],
3432
+ "info" : {
3433
+ "author" : "xcode",
3434
+ "version" : 1
3435
+ }
3436
+ }
3437
+ `;
3438
+ var APP_ICON_JSON = `{
3439
+ "images" : [
3440
+ {
3441
+ "idiom" : "universal",
3442
+ "platform" : "ios",
3443
+ "size" : "1024x1024"
3444
+ },
3445
+ {
3446
+ "appearances" : [
3447
+ {
3448
+ "appearance" : "luminosity",
3449
+ "value" : "dark"
3450
+ }
3451
+ ],
3452
+ "idiom" : "universal",
3453
+ "platform" : "ios",
3454
+ "size" : "1024x1024"
3455
+ },
3456
+ {
3457
+ "appearances" : [
3458
+ {
3459
+ "appearance" : "luminosity",
3460
+ "value" : "tinted"
3461
+ }
3462
+ ],
3463
+ "idiom" : "universal",
3464
+ "platform" : "ios",
3465
+ "size" : "1024x1024"
3466
+ }
3467
+ ],
3468
+ "info" : {
3469
+ "author" : "xcode",
3470
+ "version" : 1
3471
+ }
3472
+ }
3473
+ `;
3474
+ var runtimePackageTemplate = (sdkVersion, variant, plugins) => {
3475
+ const product = `LeCodesSDK-${variant}`;
3476
+ const pluginTargets = plugins.map((p) => `
3477
+ // plugin: ${p.id} (vendored — see plugins.lock.json)
3478
+ .target(
3479
+ name: "${p.module}",
3480
+ dependencies: [.product(name: "${product}", package: "lecodes-ios-sdk")],
3481
+ path: "Sources/${p.module}"
3482
+ ),`).join("");
3483
+ const runtimeDeps = [`.product(name: "${product}", package: "lecodes-ios-sdk")`, ...plugins.map((p) => `"${p.module}"`)];
3484
+ return `// swift-tools-version:5.9
3485
+ //
3486
+ // GENERATED by lecodes — regenerated by \`lecodes app sync\`; do not edit.
3487
+ // Owns the app's native dependency graph: the LeCodesSDK variant (app.json ios.variant)
3488
+ // and one vendored target per installed plugin (see plugins.lock.json).
3489
+
3490
+ import PackageDescription
3491
+
3492
+ let package = Package(
3493
+ name: "LeCodesRuntime",
3494
+ platforms: [.iOS(.v15)],
3495
+ products: [
3496
+ .library(name: "LeCodesRuntime", targets: ["LeCodesRuntime"]),
3497
+ ],
3498
+ dependencies: [
3499
+ .package(url: "https://github.com/letary/lecodes-ios-sdk.git", from: "${sdkVersion}"),
3500
+ ],
3501
+ targets: [
3502
+ .target(
3503
+ name: "LeCodesRuntime",
3504
+ dependencies: [
3505
+ ${runtimeDeps.join(`,
3506
+ `)},
3507
+ ],
3508
+ path: "Sources/LeCodesRuntime"
3509
+ ),${pluginTargets}
3510
+ ]
3511
+ )
3512
+ `;
3513
+ };
3514
+ var runtimeSwiftTemplate = (plugins) => {
3515
+ const imports = plugins.map((p) => `import ${p.module}
3516
+ `).join("");
3517
+ const calls = plugins.length === 0 ? `
3518
+ ` : plugins.map((p) => ` ${p.register}.register(in: engine)
3519
+ `).join("");
3520
+ return `//
3521
+ // LeCodesRuntime.swift
3522
+ // GENERATED by lecodes — regenerated by \`lecodes app sync\`; do not edit.
3523
+ //
3524
+ // Re-exports the SDK (so app code needs only \`import LeCodesRuntime\`) and registers
3525
+ // every plugin installed in app.json.
3526
+ //
3527
+
3528
+ @_exported import LeCodesSDK
3529
+ ${imports}
3530
+ public func registerBundledPlugins(in engine: LeCodesEngine) {
3531
+ ${calls}}
3532
+ `;
3533
+ };
3534
+ var IOS_README = `# ios/ — native Xcode shell
3535
+
3536
+ Generated by \`lecodes app init ios\` for the LeCodes project one folder up. The app runs the
3537
+ compiled bundle in \`App/Resources/app.js\` — fully offline, nothing fetched at launch. This
3538
+ folder is ignored by the platform (\`ios/\` in \`.lecodesignore\`) and is never pushed to LeCodes.
3539
+
3540
+ ## Daily workflow
3541
+
3542
+ Edit the app's code at the project root (\`main.ts\` …), then:
3543
+
3544
+ \`\`\`
3545
+ lecodes app sync # recompile app.js + assets into the shell, vendor plugins
3546
+ lecodes app open # open App.xcodeproj in Xcode — build & run from there
3547
+ \`\`\`
3548
+
3549
+ \`lecodes app doctor\` preflights builds; \`lecodes app build ios\` archives and exports an
3550
+ .ipa; \`lecodes app upload ios\` sends the archive to App Store Connect.
3551
+
3552
+ ## Who owns what
3553
+
3554
+ Yours — \`lecodes app sync\` never touches these:
3555
+
3556
+ - \`App/*.swift\` (AppDelegate, SceneDelegate, ViewController) — customize freely, add files.
3557
+ App-local native code goes here too: add a Swift file that calls
3558
+ \`engine.registerCallback\` / \`registerService\`, and invoke it from ViewController.
3559
+ - \`App/Assets.xcassets/\` — drop a 1024×1024 PNG into AppIcon.appiconset/ for the app icon.
3560
+ - \`App/Base.lproj/LaunchScreen.storyboard\`
3561
+
3562
+ CLI-owned — regenerated on every sync, hand edits are overwritten:
3563
+
3564
+ - \`App/Resources/\` — the compiled app.js + every asset it references (fonts included), so
3565
+ the app runs fully offline. Globs in app.json \`"resources": {"remote": [...]}\` opt heavy
3566
+ assets (videos, big models) back into streaming from the server instead.
3567
+ - \`App/Info.plist\` — template + the installed plugins' usage keys
3568
+ - \`LeCodesRuntime/\` — local Swift package: the LeCodesSDK dependency, vendored plugin
3569
+ sources (Sources/<Module>/), and the registerBundledPlugins entry point
3570
+ - the version values inside \`App.xcodeproj/project.pbxproj\` (from app.json)
3571
+ - this README
3572
+
3573
+ ## Config
3574
+
3575
+ Identity lives in \`../app.json\` (name, version, plugins, ios.bundleId / teamId / variant /
3576
+ buildNumber) — edit it there, then re-run \`lecodes app sync\`. Plugins are managed with
3577
+ \`lecodes app plugin add <id | github:owner/repo[@tag]>\` and pinned in \`../plugins.lock.json\`.
3578
+ `;
3579
+
3580
+ // src/commands/app.ts
3581
+ var SDK_PACKAGE_VERSION = "1.0.4";
3582
+ var FIRST_PARTY_PLUGINS = {
3583
+ camera: "letary/lecodes-plugins",
3584
+ "qr-scanner": "letary/lecodes-plugins"
3585
+ };
3586
+ var readJson = (path) => {
3587
+ if (!existsSync8(path))
3588
+ return null;
3589
+ try {
3590
+ return JSON.parse(readFileSync10(path, "utf8"));
3591
+ } catch {
3592
+ throw new CliError(`Invalid JSON in ${path}`);
3593
+ }
3594
+ };
3595
+ var writeJson = (path, value) => writeFileSync7(path, JSON.stringify(value, null, 2) + `
3596
+ `);
3597
+ var projectRoot = () => {
3598
+ try {
3599
+ return findProjectRoot(process.cwd());
3600
+ } catch {
3601
+ return process.cwd();
3602
+ }
3603
+ };
3604
+ var git = (cwd, ...args) => execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
3605
+ var ensureRepo = (repo, wantTag, lock) => {
3606
+ const override = process.env.LECODES_PLUGINS_DIR;
3607
+ if (override) {
3608
+ note(`Using local plugin checkout ${override} (LECODES_PLUGINS_DIR)`);
3609
+ return { dir: resolve2(override), tag: "local", commit: "local" };
3610
+ }
3611
+ const cache = join11(homedir2(), ".lecodes", "plugins", repo.replace("/", "__"));
3612
+ const url = `https://github.com/${repo}.git`;
3613
+ if (!existsSync8(join11(cache, ".git"))) {
3614
+ mkdirSync5(dirname7(cache), { recursive: true });
3615
+ note(`Cloning ${url}…`);
3616
+ git(dirname7(cache), "clone", "--quiet", url, cache);
3617
+ }
3618
+ let tag = lock?.tag ?? wantTag;
3619
+ let commit = lock?.commit;
3620
+ if (commit) {
3621
+ try {
3622
+ git(cache, "cat-file", "-e", `${commit}^{commit}`);
3623
+ } catch {
3624
+ git(cache, "fetch", "--quiet", "--tags", "origin");
3625
+ }
3626
+ } else {
3627
+ git(cache, "fetch", "--quiet", "--tags", "origin");
3628
+ if (!tag) {
3629
+ tag = git(cache, "tag", "--list", "v*", "--sort=-version:refname").split(`
3630
+ `)[0] ?? "";
3631
+ if (!tag)
3632
+ throw new CliError(`${repo} has no v* version tags to install from`);
3633
+ }
3634
+ commit = git(cache, "rev-list", "-n", "1", tag);
3635
+ }
3636
+ git(cache, "checkout", "--quiet", "--detach", commit);
3637
+ return { dir: cache, tag: tag ?? "unknown", commit };
3638
+ };
3639
+ var resolvePlugin = (id, spec, lock) => {
3640
+ let repo;
3641
+ let subdir;
3642
+ let wantTag = null;
3643
+ if (spec.startsWith("github:")) {
3644
+ const [repoPart, tagPart] = spec.slice("github:".length).split("@");
3645
+ repo = repoPart;
3646
+ wantTag = tagPart ?? null;
3647
+ subdir = "";
3648
+ } else {
3649
+ repo = FIRST_PARTY_PLUGINS[id];
3650
+ if (!repo) {
3651
+ throw new CliError(`Unknown plugin "${id}" — known: ${Object.keys(FIRST_PARTY_PLUGINS).join(", ")}, or use a github:owner/repo spec.`);
3652
+ }
3653
+ subdir = `plugins/${id}`;
3654
+ }
3655
+ const checkout = ensureRepo(repo, wantTag, lock?.repo === repo ? lock : undefined);
3656
+ const dir = subdir ? join11(checkout.dir, subdir) : checkout.dir;
3657
+ const manifest = readJson(join11(dir, "lecodes-plugin.json"));
3658
+ if (!manifest)
3659
+ throw new CliError(`Plugin "${id}": no lecodes-plugin.json in ${dir}`);
3660
+ if (manifest.id !== id)
3661
+ throw new CliError(`Plugin "${id}": manifest declares id "${manifest.id}"`);
3662
+ if (!manifest.ios)
3663
+ throw new CliError(`Plugin "${id}" does not support iOS (manifest "ios" is null)`);
3664
+ if (spec.startsWith("github:")) {
3665
+ const names = [...manifest.provides?.views ?? [], ...manifest.provides?.services ?? []];
3666
+ for (const name of names) {
3667
+ if (!name.includes("."))
3668
+ warn(`Plugin "${id}" provides bare channel name "${name}" — third-party names should be vendor-prefixed (e.g. "acme.${name}").`);
3669
+ }
3670
+ }
3671
+ return {
3672
+ id,
3673
+ dir,
3674
+ manifest,
3675
+ lock: { repo, tag: checkout.tag, commit: checkout.commit, version: manifest.version ?? "unknown" }
3676
+ };
3677
+ };
3678
+ var slugify = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "app";
3679
+ var appInit = async (args) => {
3680
+ const platform = args._[1];
3681
+ if (platform !== "ios") {
3682
+ throw new CliError(platform ? `Unsupported platform "${platform}" — only "ios" for now.` : "Usage: lecodes app init ios [--name <display name>] [--bundle-id <id>] [--team <team id>] [--variant core|full]");
3683
+ }
3684
+ const root = projectRoot();
3685
+ const iosDir = join11(root, "ios");
3686
+ if (existsSync8(iosDir)) {
3687
+ throw new CliError("ios/ already exists — run `lecodes app sync` to update it.");
3688
+ }
3689
+ const existing = readJson(join11(root, "app.json")) ?? {};
3690
+ const variant = flagStr(args, "variant") ?? existing.ios?.variant ?? "core";
3691
+ if (variant !== "core" && variant !== "full")
3692
+ throw new CliError(`--variant must be "core" or "full" (got "${variant}")`);
3693
+ const name = flagStr(args, "name") ?? existing.name ?? basename4(root);
3694
+ const config = {
3695
+ ...existing,
3696
+ name,
3697
+ version: existing.version ?? "1.0.0",
3698
+ plugins: existing.plugins ?? {},
3699
+ ios: {
3700
+ bundleId: flagStr(args, "bundle-id") ?? existing.ios?.bundleId ?? `com.example.${slugify(name)}`,
3701
+ teamId: flagStr(args, "team") ?? existing.ios?.teamId ?? "",
3702
+ variant,
3703
+ buildNumber: existing.ios?.buildNumber ?? 1
3704
+ }
3705
+ };
3706
+ writeJson(join11(root, "app.json"), config);
3707
+ log(`${c.green("written")} app.json`);
3708
+ const ignorePath = join11(root, ".lecodesignore");
3709
+ const ignore = existsSync8(ignorePath) ? readFileSync10(ignorePath, "utf8") : DEFAULT_IGNORE_FILE;
3710
+ if (!/^ios\/$/m.test(ignore)) {
3711
+ writeFileSync7(ignorePath, ignore.replace(/\n*$/, `
3712
+ `) + `
3713
+ # Native app shells — generated by \`lecodes app init\`, never pushed to LeCodes.
3714
+ ios/
3715
+ `);
3716
+ log(`${c.green("updated")} .lecodesignore (+ ios/)`);
3717
+ }
3718
+ const identity = {
3719
+ displayName: name,
3720
+ bundleId: config.ios.bundleId,
3721
+ teamId: config.ios.teamId,
3722
+ version: config.version,
3723
+ buildNumber: config.ios.buildNumber
3724
+ };
3725
+ const write = (rel, content) => {
3726
+ const abs = join11(iosDir, rel);
3727
+ mkdirSync5(dirname7(abs), { recursive: true });
3728
+ writeFileSync7(abs, content);
3729
+ log(`${c.green("created")} ios/${rel}`);
3730
+ };
3731
+ write("README.md", IOS_README);
3732
+ write("App.xcodeproj/project.pbxproj", pbxprojTemplate(identity));
3733
+ write("App.xcodeproj/xcshareddata/xcschemes/App.xcscheme", SCHEME_XML);
3734
+ write("App/AppDelegate.swift", APP_DELEGATE);
3735
+ write("App/SceneDelegate.swift", SCENE_DELEGATE);
3736
+ write("App/ViewController.swift", VIEW_CONTROLLER);
3737
+ write("App/Info.plist", infoPlistTemplate({}));
3738
+ write("App/Base.lproj/LaunchScreen.storyboard", LAUNCH_STORYBOARD);
3739
+ write("App/Assets.xcassets/Contents.json", ASSETS_ROOT_JSON);
3740
+ write("App/Assets.xcassets/AccentColor.colorset/Contents.json", ACCENT_COLOR_JSON);
3741
+ write("App/Assets.xcassets/AppIcon.appiconset/Contents.json", APP_ICON_JSON);
3742
+ write("LeCodesRuntime/Package.swift", runtimePackageTemplate(SDK_PACKAGE_VERSION, variant, []));
3743
+ write("LeCodesRuntime/Sources/LeCodesRuntime/LeCodesRuntime.swift", runtimeSwiftTemplate([]));
3744
+ log("");
3745
+ success(`iOS shell for ${c.bold(name)} ready.`);
3746
+ log(` ${c.bold("lecodes app sync")} embed the compiled bundle (+ plugins) into the shell`);
3747
+ log(` ${c.bold("open ios/App.xcodeproj")} then run on your device from Xcode`);
3748
+ log(` App icon: drop a 1024×1024 PNG into ios/App/Assets.xcassets/AppIcon.appiconset/`);
3749
+ };
3750
+ var appSync = async (_args) => {
3751
+ const root = projectRoot();
3752
+ const config = readJson(join11(root, "app.json"));
3753
+ const iosDir = join11(root, "ios");
3754
+ if (!config?.ios || !existsSync8(iosDir)) {
3755
+ throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
3756
+ }
3757
+ const variant = config.ios.variant === "full" ? "full" : "core";
3758
+ const lockPath = join11(root, "plugins.lock.json");
3759
+ const oldLock = readJson(lockPath)?.plugins ?? {};
3760
+ const pluginSpecs = Object.entries(config.plugins ?? {});
3761
+ const runtimePlugins = [];
3762
+ const infoValues = {};
3763
+ const newLock = {};
3764
+ const sourcesDir = join11(iosDir, "LeCodesRuntime", "Sources");
3765
+ for (const [id, spec] of pluginSpecs) {
3766
+ const plugin = resolvePlugin(id, spec, oldLock[id]);
3767
+ const ios = plugin.manifest.ios;
3768
+ const module = ios.module;
3769
+ const register = ios.register;
3770
+ if (!module || !register)
3771
+ throw new CliError(`Plugin "${id}": manifest ios.module / ios.register missing`);
3772
+ const src = join11(plugin.dir, ios.sources ?? "ios");
3773
+ if (!existsSync8(src))
3774
+ throw new CliError(`Plugin "${id}": sources dir ${src} missing`);
3775
+ const dest = join11(sourcesDir, module);
3776
+ rmSync(dest, { recursive: true, force: true });
3777
+ cpSync(src, dest, { recursive: true });
3778
+ for (const [key, value] of Object.entries(ios.infoPlist ?? {})) {
3779
+ (infoValues[key] ??= []).push(value);
3780
+ }
3781
+ runtimePlugins.push({ id, module, register });
3782
+ newLock[id] = plugin.lock;
3783
+ log(`${c.green("plugin")} ${id}@${plugin.lock.version} (${plugin.lock.tag} → Sources/${module})`);
3784
+ }
3785
+ const keep = new Set(["LeCodesRuntime", ...runtimePlugins.map((p) => p.module)]);
3786
+ for (const entry of existsSync8(sourcesDir) ? readdirSync4(sourcesDir) : []) {
3787
+ if (!keep.has(entry) && statSync2(join11(sourcesDir, entry)).isDirectory()) {
3788
+ rmSync(join11(sourcesDir, entry), { recursive: true });
3789
+ log(`${c.yellow("removed")} ios/LeCodesRuntime/Sources/${entry} (plugin uninstalled)`);
3790
+ }
3791
+ }
3792
+ writeFileSync7(join11(iosDir, "README.md"), IOS_README);
3793
+ writeFileSync7(join11(iosDir, "LeCodesRuntime", "Package.swift"), runtimePackageTemplate(SDK_PACKAGE_VERSION, variant, runtimePlugins));
3794
+ writeFileSync7(join11(sourcesDir, "LeCodesRuntime", "LeCodesRuntime.swift"), runtimeSwiftTemplate(runtimePlugins));
3795
+ const mergedKeys = {};
3796
+ for (const [key, values] of Object.entries(infoValues))
3797
+ mergedKeys[key] = [...new Set(values)].join(" ");
3798
+ writeFileSync7(join11(iosDir, "App", "Info.plist"), infoPlistTemplate(mergedKeys));
3799
+ if (pluginSpecs.length > 0 || existsSync8(lockPath))
3800
+ writeJson(lockPath, { plugins: newLock });
3801
+ const pbxPath = join11(iosDir, "App.xcodeproj", "project.pbxproj");
3802
+ const pbx = readFileSync10(pbxPath, "utf8").replace(/MARKETING_VERSION = [^;]+;/g, `MARKETING_VERSION = ${config.version ?? "1.0.0"};`).replace(/CURRENT_PROJECT_VERSION = [^;]+;/g, `CURRENT_PROJECT_VERSION = ${config.ios.buildNumber ?? 1};`);
3803
+ writeFileSync7(pbxPath, pbx);
3804
+ const remotePatterns = config.resources?.remote ?? [];
3805
+ if (!Array.isArray(remotePatterns) || remotePatterns.some((p) => typeof p !== "string")) {
3806
+ throw new CliError('app.json "resources.remote" must be an array of glob strings (e.g. ["videos/**"]).');
3807
+ }
3808
+ const resourcesDir = join11(iosDir, "App", "Resources");
3809
+ rmSync(resourcesDir, { recursive: true, force: true });
3810
+ mkdirSync5(resourcesDir, { recursive: true });
3811
+ const resources = await compileShell({ _: [], flags: { o: join11(resourcesDir, "app.js") } }, { remotePatterns });
3812
+ const bundleJs = readFileSync10(join11(resourcesDir, "app.js"), "utf8");
3813
+ const preload = /^\/\/ preload: (.*)$/m.exec(bundleJs)?.[1].split(";").map((s) => s.trim()).filter(Boolean) ?? [];
3814
+ const IMPLICIT_PRELOADS = new Set(["uberarchive", "capture"]);
3815
+ const byName = new Map;
3816
+ for (const r of resources) {
3817
+ if (r.remote || !r.absPath)
3818
+ continue;
3819
+ const list = byName.get(r.name) ?? [];
3820
+ list.push(r);
3821
+ byName.set(r.name, list);
3822
+ }
3823
+ let embedded = 0;
3824
+ for (const name of preload) {
3825
+ if (IMPLICIT_PRELOADS.has(name))
3826
+ continue;
3827
+ const candidates = byName.get(name) ?? [];
3828
+ if (candidates.length === 0) {
3829
+ warn(`bundle needs local asset "${name}" but no project file provides it — it won't load on device`);
3830
+ continue;
3831
+ }
3832
+ if (candidates.length > 1)
3833
+ warn(`asset name collision: ${name} (${candidates.map((r) => r.path).join(", ")}) — embedding ${candidates[0].path}`);
3834
+ cpSync(candidates[0].absPath, join11(resourcesDir, name));
3835
+ embedded++;
3836
+ }
3837
+ if (embedded > 0)
3838
+ log(`${c.green("assets")} ${embedded} file(s) embedded → ios/App/Resources/`);
3839
+ const streamed = resources.filter((r) => r.remote);
3840
+ if (streamed.length > 0)
3841
+ log(`${c.green("remote")} ${streamed.length} resource(s) stay on the server (resources.remote): ${streamed.map((r) => r.path).join(", ")}`);
3842
+ log("");
3843
+ success(`Shell synced: app.js + ${runtimePlugins.length} plugin(s), variant ${variant}, v${config.version ?? "1.0.0"} (${config.ios.buildNumber ?? 1}).`);
3844
+ note("Build & run from Xcode (open ios/App.xcodeproj) — user files in ios/App/*.swift were not touched.");
3845
+ };
3846
+ var appPlugin = async (args) => {
3847
+ const sub = args._[1];
3848
+ const root = projectRoot();
3849
+ const configPath = join11(root, "app.json");
3850
+ const config = readJson(configPath);
3851
+ if (!config)
3852
+ throw new CliError("No app.json here — run `lecodes app init ios` first.");
3853
+ config.plugins ??= {};
3854
+ if (sub === "list" || sub === undefined) {
3855
+ const entries = Object.entries(config.plugins);
3856
+ if (entries.length === 0) {
3857
+ log("No plugins installed.");
3858
+ return;
3859
+ }
3860
+ const lock = readJson(join11(root, "plugins.lock.json"))?.plugins ?? {};
3861
+ for (const [id2, spec] of entries) {
3862
+ const pin = lock[id2] ? c.dim(` ${lock[id2].repo} ${lock[id2].tag} (${lock[id2].commit.slice(0, 7)})`) : c.dim(" (not synced)");
3863
+ log(`${c.bold(id2)} ${spec}${pin}`);
3864
+ }
3865
+ return;
3866
+ }
3867
+ const id = args._[2];
3868
+ if (!id)
3869
+ throw new CliError(`Usage: lecodes app plugin ${sub} <id | github:owner/repo[@tag]>`);
3870
+ if (sub === "add") {
3871
+ if (id.startsWith("github:")) {
3872
+ const bare = id.slice("github:".length).split("@")[0].split("/").pop();
3873
+ config.plugins[bare] = id;
3874
+ log(`${c.green("added")} ${bare} → ${id} ${c.dim("(third-party native code — review it before shipping)")}`);
3875
+ } else if (FIRST_PARTY_PLUGINS[id]) {
3876
+ config.plugins[id] = "*";
3877
+ log(`${c.green("added")} ${id}`);
3878
+ } else {
3879
+ throw new CliError(`Unknown plugin "${id}" — known: ${Object.keys(FIRST_PARTY_PLUGINS).join(", ")}, or use github:owner/repo[@tag].`);
3880
+ }
3881
+ writeJson(configPath, config);
3882
+ note("Run `lecodes app sync` to vendor it into the shell.");
3883
+ return;
3884
+ }
3885
+ if (sub === "remove") {
3886
+ if (!(id in config.plugins))
3887
+ throw new CliError(`Plugin "${id}" is not installed.`);
3888
+ delete config.plugins[id];
3889
+ writeJson(configPath, config);
3890
+ log(`${c.yellow("removed")} ${id}`);
3891
+ note("Run `lecodes app sync` to drop its sources from the shell.");
3892
+ return;
3893
+ }
3894
+ throw new CliError(`Unknown subcommand "plugin ${sub}" — use list, add or remove.`);
3895
+ };
3896
+ var runTool = (cwd, cmd, args) => {
3897
+ try {
3898
+ return execFileSync(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 128 * 1024 * 1024 });
3899
+ } catch (e) {
3900
+ const err = e;
3901
+ const tail = ((err.stdout ?? "") + `
3902
+ ` + (err.stderr ?? "")).split(`
3903
+ `).filter((l) => l.trim()).slice(-25).join(`
3904
+ `);
3905
+ throw new CliError(`${cmd} ${args[0] ?? ""} failed:
3906
+ ${tail}`);
3907
+ }
3908
+ };
3909
+ var exportOptionsPlist = (method, teamId, destination) => `<?xml version="1.0" encoding="UTF-8"?>
3910
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3911
+ <plist version="1.0">
3912
+ <dict>
3913
+ <key>method</key>
3914
+ <string>${method}</string>
3915
+ <key>signingStyle</key>
3916
+ <string>automatic</string>${teamId ? `
3917
+ <key>teamID</key>
3918
+ <string>${teamId}</string>` : ""}${destination ? `
3919
+ <key>destination</key>
3920
+ <string>${destination}</string>` : ""}
3921
+ </dict>
3922
+ </plist>
3923
+ `;
3924
+ var requireShell = () => {
3925
+ const root = projectRoot();
3926
+ const config = readJson(join11(root, "app.json"));
3927
+ if (!config?.ios || !existsSync8(join11(root, "ios"))) {
3928
+ throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
3929
+ }
3930
+ return { root, config };
3931
+ };
3932
+ var appDoctor = async (_args) => {
3933
+ const { root, config } = requireShell();
3934
+ let failed = 0;
3935
+ const check = (ok, label, hint = "", fatal = true) => {
3936
+ log(`${ok ? c.green("✓") : fatal ? c.red("✗") : c.yellow("!")} ${label}${ok || !hint ? "" : c.dim(" — " + hint)}`);
3937
+ if (!ok && fatal)
3938
+ failed++;
3939
+ };
3940
+ let xcodeVersion = "";
3941
+ try {
3942
+ xcodeVersion = runTool(root, "xcodebuild", ["-version"]).split(`
3943
+ `)[0];
3944
+ } catch {}
3945
+ check(xcodeVersion !== "", `Xcode CLI (${xcodeVersion || "not found"})`, "install Xcode + command line tools");
3946
+ check(!!config.ios.teamId, "Development team set", "lecodes app init wrote app.json — fill ios.teamId", false);
3947
+ const iconDir = join11(root, "ios", "App", "Assets.xcassets", "AppIcon.appiconset");
3948
+ const hasIcon = existsSync8(iconDir) && readdirSync4(iconDir).some((f2) => f2.endsWith(".png"));
3949
+ check(hasIcon, "App icon present", "drop a 1024×1024 PNG into ios/App/Assets.xcassets/AppIcon.appiconset/ (App Store requires it)", false);
3950
+ const bundle = join11(root, "ios", "App", "Resources", "app.js");
3951
+ check(existsSync8(bundle), "Bundled app.js", "run `lecodes app sync`");
3952
+ const lock = readJson(join11(root, "plugins.lock.json"))?.plugins ?? {};
3953
+ const wanted = Object.keys(config.plugins ?? {});
3954
+ const inSync = wanted.every((id) => lock[id]) && Object.keys(lock).every((id) => wanted.includes(id));
3955
+ check(wanted.length === 0 || inSync, "Plugins synced", "run `lecodes app sync`");
3956
+ const asc = readJson(join11(homedir2(), ".lecodes", "asc.json"));
3957
+ check(!!asc?.keyPath, "App Store Connect API key configured", "pass --key-path/--key-id/--issuer-id to `lecodes app upload` once to store it", false);
3958
+ log("");
3959
+ if (failed > 0)
3960
+ throw new CliError(`${failed} check(s) failed.`);
3961
+ success("Ready to build.");
3962
+ };
3963
+ var ARCHIVE_PATH = "ios/build/App.xcarchive";
3964
+ var appBuild = async (args) => {
3965
+ const { root, config } = requireShell();
3966
+ const configuration = flagStr(args, "configuration") ?? "Release";
3967
+ const method = flagStr(args, "method") ?? "app-store-connect";
3968
+ if (!flagBool(args, "no-sync"))
3969
+ await appSync({ _: [], flags: {} });
3970
+ note(`Archiving (${configuration})… this takes a few minutes`);
3971
+ runTool(root, "xcodebuild", [
3972
+ "-project",
3973
+ "ios/App.xcodeproj",
3974
+ "-scheme",
3975
+ "App",
3976
+ "-destination",
3977
+ "generic/platform=iOS",
3978
+ "-configuration",
3979
+ configuration,
3980
+ "archive",
3981
+ "-archivePath",
3982
+ ARCHIVE_PATH,
3983
+ "-allowProvisioningUpdates"
3984
+ ]);
3985
+ success(`Archive: ios/build/App.xcarchive`);
3986
+ if (flagBool(args, "no-export"))
3987
+ return;
3988
+ const optionsPath = join11(root, "ios", "build", "ExportOptions.plist");
3989
+ writeFileSync7(optionsPath, exportOptionsPlist(method, config.ios.teamId ?? ""));
3990
+ note(`Exporting (.ipa, method ${method})…`);
3991
+ runTool(root, "xcodebuild", [
3992
+ "-exportArchive",
3993
+ "-archivePath",
3994
+ ARCHIVE_PATH,
3995
+ "-exportOptionsPlist",
3996
+ optionsPath,
3997
+ "-exportPath",
3998
+ "ios/build/export",
3999
+ "-allowProvisioningUpdates"
4000
+ ]);
4001
+ success("Export: ios/build/export/ — upload with `lecodes app upload ios`");
4002
+ };
4003
+ var appUpload = async (args) => {
4004
+ const { root, config } = requireShell();
4005
+ if (!existsSync8(join11(root, ARCHIVE_PATH))) {
4006
+ throw new CliError("No archive at ios/build/App.xcarchive — run `lecodes app build ios` first.");
4007
+ }
4008
+ const ascPath = join11(homedir2(), ".lecodes", "asc.json");
4009
+ const stored = readJson(ascPath) ?? {};
4010
+ const keyPath = flagStr(args, "key-path") ?? stored.keyPath;
4011
+ const keyId = flagStr(args, "key-id") ?? stored.keyId;
4012
+ const issuerId = flagStr(args, "issuer-id") ?? stored.issuerId;
4013
+ if (!keyPath || !keyId || !issuerId) {
4014
+ throw new CliError(`App Store Connect API key needed: --key-path <AuthKey.p8> --key-id <id> --issuer-id <id>
4015
+ (create one at appstoreconnect.apple.com → Users and Access → Integrations; stored in ~/.lecodes/asc.json after first use)`);
4016
+ }
4017
+ if (!existsSync8(keyPath))
4018
+ throw new CliError(`API key file not found: ${keyPath}`);
4019
+ if (flagStr(args, "key-path")) {
4020
+ mkdirSync5(dirname7(ascPath), { recursive: true });
4021
+ writeJson(ascPath, { keyPath, keyId, issuerId });
4022
+ note("Stored the key reference in ~/.lecodes/asc.json");
4023
+ }
4024
+ const optionsPath = join11(root, "ios", "build", "UploadOptions.plist");
4025
+ writeFileSync7(optionsPath, exportOptionsPlist("app-store-connect", config.ios.teamId ?? "", "upload"));
4026
+ note("Uploading to App Store Connect… (processing continues on Apple's side)");
4027
+ runTool(root, "xcodebuild", [
4028
+ "-exportArchive",
4029
+ "-archivePath",
4030
+ ARCHIVE_PATH,
4031
+ "-exportOptionsPlist",
4032
+ optionsPath,
4033
+ "-exportPath",
4034
+ "ios/build/upload",
4035
+ "-authenticationKeyPath",
4036
+ resolve2(keyPath),
4037
+ "-authenticationKeyID",
4038
+ keyId,
4039
+ "-authenticationKeyIssuerID",
4040
+ issuerId,
4041
+ "-allowProvisioningUpdates"
4042
+ ]);
4043
+ const configPath = join11(root, "app.json");
4044
+ const fresh = readJson(configPath);
4045
+ fresh.ios.buildNumber = (fresh.ios.buildNumber ?? 1) + 1;
4046
+ writeJson(configPath, fresh);
4047
+ success(`Uploaded build ${(fresh.ios.buildNumber ?? 2) - 1} — check App Store Connect / TestFlight once processing finishes.`);
4048
+ note(`ios.buildNumber bumped to ${fresh.ios.buildNumber} for the next build.`);
4049
+ };
4050
+ var appOpen = async (args) => {
4051
+ const root = projectRoot();
4052
+ const platform = args._[1] ?? "ios";
4053
+ if (platform === "ios") {
4054
+ const proj = join11(root, "ios", "App.xcodeproj");
4055
+ if (!existsSync8(proj))
4056
+ throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
4057
+ if (process.platform !== "darwin")
4058
+ throw new CliError("Opening Xcode requires macOS.");
4059
+ execFileSync("open", [proj], { stdio: "ignore" });
4060
+ success("Opened ios/App.xcodeproj in Xcode.");
4061
+ return;
4062
+ }
4063
+ if (platform === "android") {
4064
+ throw new CliError("Android shells don't exist yet — `lecodes app open` covers ios for now.");
4065
+ }
4066
+ throw new CliError(`Unknown platform "${platform}" — use ios.`);
4067
+ };
4068
+ var app = async (args) => {
4069
+ const sub = args._[0];
4070
+ if (sub === "init")
4071
+ return appInit(args);
4072
+ if (sub === "sync")
4073
+ return appSync(args);
4074
+ if (sub === "plugin")
4075
+ return appPlugin(args);
4076
+ if (sub === "doctor")
4077
+ return appDoctor(args);
4078
+ if (sub === "build")
4079
+ return appBuild(args);
4080
+ if (sub === "upload")
4081
+ return appUpload(args);
4082
+ if (sub === "open")
4083
+ return appOpen(args);
4084
+ throw new CliError("Usage: lecodes app <init ios | sync | open | plugin list|add|remove | doctor | build ios | upload ios> — see `lecodes help`.");
4085
+ };
4086
+
4087
+ // src/commands/clone.ts
4088
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync as readdirSync5 } from "node:fs";
4089
+ import { resolve as resolve3 } from "node:path";
4090
+
4091
+ // src/types.ts
4092
+ import { dirname as dirname8, join as join12 } from "node:path";
4093
+ import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "node:fs";
4094
+ var TSCONFIG = {
1815
4095
  compilerOptions: {
1816
4096
  target: "ES2020",
1817
4097
  module: "ESNext",
@@ -1828,11 +4108,11 @@ var TSCONFIG = {
1828
4108
  exclude: ["node_modules"]
1829
4109
  };
1830
4110
  var writeTypeBundle = (root, bundle) => {
1831
- const typesDir = join5(root, LECODES_DIR, "types");
4111
+ const typesDir = join12(root, LECODES_DIR, "types");
1832
4112
  for (const [path, text] of Object.entries(bundle)) {
1833
- const dest = join5(typesDir, path);
1834
- mkdirSync4(dirname4(dest), { recursive: true });
1835
- writeFileSync4(dest, text);
4113
+ const dest = join12(typesDir, path);
4114
+ mkdirSync6(dirname8(dest), { recursive: true });
4115
+ writeFileSync8(dest, text);
1836
4116
  }
1837
4117
  };
1838
4118
  var materializeTypes = async (root, apiUrl) => {
@@ -1840,13 +4120,13 @@ var materializeTypes = async (root, apiUrl) => {
1840
4120
  };
1841
4121
  var readLocalTypeBundle = () => {
1842
4122
  const candidates = [
1843
- join5(distRoot, "runtime", "sdk-types.json"),
1844
- join5(distRoot, "..", "sdk", "dist", "types.json")
4123
+ join12(distRoot, "runtime", "sdk-types.json"),
4124
+ join12(distRoot, "..", "sdk", "dist", "types.json")
1845
4125
  ];
1846
4126
  for (const path of candidates) {
1847
- if (existsSync3(path)) {
4127
+ if (existsSync9(path)) {
1848
4128
  try {
1849
- return JSON.parse(readFileSync3(path, "utf8"));
4129
+ return JSON.parse(readFileSync11(path, "utf8"));
1850
4130
  } catch {}
1851
4131
  }
1852
4132
  }
@@ -1860,100 +4140,7 @@ var materializeTypesLocal = (root) => {
1860
4140
  return true;
1861
4141
  };
1862
4142
  var writeTsconfig = (root) => {
1863
- writeFileSync4(join5(root, "tsconfig.json"), JSON.stringify(TSCONFIG, null, 2));
1864
- };
1865
-
1866
- // src/ignore.ts
1867
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
1868
- import { join as join6 } from "node:path";
1869
- var BUILTIN_PATTERNS = ["node_modules/", ".git/", "_*", "*.log", ".DS_Store"];
1870
- var DEFAULT_IGNORE_FILE = `# Files matched here are NOT pushed to LeCodes.
1871
- # Only untracked files are affected — assets already in the project stay tracked
1872
- # even if they match a pattern. Built-in defaults (node_modules, _*, *.log) always apply.
1873
-
1874
- # Render / preview outputs (the default \`lecodes render --png\` target)
1875
- screenshot.png
1876
- shots/
1877
-
1878
- # Image & other binary assets ARE pushed by default — they're real project resources.
1879
- # Uncomment a pattern below only if you want to keep some untracked ones out of pushes.
1880
- # *.png
1881
-
1882
- # Local notes & dumps (uncomment to ignore)
1883
- # *.json
1884
- # notes.md
1885
- `;
1886
- var translate = (glob) => {
1887
- let re = "";
1888
- for (let i = 0;i < glob.length; i++) {
1889
- const ch = glob[i];
1890
- if (ch === "*") {
1891
- if (glob[i + 1] === "*") {
1892
- re += ".*";
1893
- i++;
1894
- if (glob[i + 1] === "/")
1895
- i++;
1896
- } else
1897
- re += "[^/]*";
1898
- } else if (ch === "?")
1899
- re += "[^/]";
1900
- else if (ch === "/")
1901
- re += "/";
1902
- else if ("\\^$.|+()[]{}".includes(ch))
1903
- re += "\\" + ch;
1904
- else
1905
- re += ch;
1906
- }
1907
- return re;
1908
- };
1909
- var compile = (raw) => {
1910
- let p = raw.trim();
1911
- if (!p || p.startsWith("#"))
1912
- return null;
1913
- let negate = false;
1914
- if (p.startsWith("!")) {
1915
- negate = true;
1916
- p = p.slice(1);
1917
- }
1918
- let dirOnly = false;
1919
- if (p.endsWith("/")) {
1920
- dirOnly = true;
1921
- p = p.slice(0, -1);
1922
- }
1923
- const anchored = p.startsWith("/") || p.includes("/");
1924
- if (p.startsWith("/"))
1925
- p = p.slice(1);
1926
- if (!p)
1927
- return null;
1928
- const body = translate(p);
1929
- const re = anchored ? new RegExp("^/" + body + "(/.*)?$") : new RegExp("(^|/)" + body + "(/.*)?$");
1930
- return { re, negate, dirOnly };
1931
- };
1932
- var buildMatcher = (rules) => (key, isDir) => {
1933
- let ignored = false;
1934
- for (const r of rules) {
1935
- if (r.dirOnly && !isDir)
1936
- continue;
1937
- if (r.re.test(key))
1938
- ignored = !r.negate;
1939
- }
1940
- return ignored;
1941
- };
1942
- var loadIgnore = (root) => {
1943
- const lines = [...BUILTIN_PATTERNS];
1944
- const file = join6(root, ".lecodesignore");
1945
- if (existsSync4(file)) {
1946
- try {
1947
- lines.push(...readFileSync4(file, "utf8").split(/\r?\n/));
1948
- } catch {}
1949
- }
1950
- const rules = lines.map(compile).filter((r) => r !== null);
1951
- return buildMatcher(rules);
1952
- };
1953
- var writeDefaultIgnore = (root) => {
1954
- const file = join6(root, ".lecodesignore");
1955
- if (!existsSync4(file))
1956
- writeFileSync5(file, DEFAULT_IGNORE_FILE);
4143
+ writeFileSync8(join12(root, "tsconfig.json"), JSON.stringify(TSCONFIG, null, 2));
1957
4144
  };
1958
4145
 
1959
4146
  // src/commands/clone.ts
@@ -1974,224 +4161,81 @@ var clone = async (args) => {
1974
4161
  const uuid = parseUuid(args._[0]);
1975
4162
  const project = await getProject(apiUrl, token, uuid);
1976
4163
  const dir = args._[1] ?? slug(project.name);
1977
- const root = resolve(dir);
1978
- if (existsSync5(root) && readdirSync(root).length > 0) {
1979
- throw new CliError(`Directory "${dir}" already exists and is not empty.`);
1980
- }
1981
- mkdirSync5(root, { recursive: true });
1982
- info(c.dim(`Fetching project ${c.bold(project.name)}…`));
1983
- const files = await materializeAssets(root, project, apiUrl, token, progress);
1984
- endProgress();
1985
- if (!flagBool(args, "no-types")) {
1986
- info(c.dim("Fetching type definitions…"));
1987
- await materializeTypes(root, apiUrl);
1988
- writeTsconfig(root);
1989
- }
1990
- writeDefaultIgnore(root);
1991
- const commits = await getCommits(apiUrl, token, uuid);
1992
- writeManifest(root, { uuid, apiUrl, name: project.name, baseHeadId: commits.headId, files });
1993
- success(`Cloned ${c.bold(project.name)} into ${c.bold(dir)}/ (${Object.keys(files).length} files)`);
1994
- info(c.dim(` cd ${dir} — edit, then \`lecodes push -m "your message"\``));
1995
- };
1996
-
1997
- // src/commands/create.ts
1998
- import { existsSync as existsSync6, mkdirSync as mkdirSync6, readdirSync as readdirSync2, writeFileSync as writeFileSync6 } from "node:fs";
1999
- import { join as join7, resolve as resolve2 } from "node:path";
2000
- var slug2 = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
2001
- var STARTER = `// Welcome to le.codes — no imports needed, the whole SDK is available as globals.
2002
- const scene = new Scene2D({ background: '#10131a' })
2003
- scene.open()
2004
-
2005
- setLoop((dt) => {
2006
- // your per-frame logic — dt is seconds since the previous frame
2007
- })
2008
- `;
2009
- var create = async (args) => {
2010
- const config = loadConfig();
2011
- const apiUrl = requireApiUrl(config);
2012
- const token = requireToken(config);
2013
- const name = args._[0];
2014
- if (!name)
2015
- throw new CliError("Usage: lecodes create <name> [directory]");
2016
- const dir = args._[1] ?? slug2(name);
2017
- const root = resolve2(dir);
2018
- if (existsSync6(root) && readdirSync2(root).length > 0) {
2019
- throw new CliError(`Directory "${dir}" already exists and is not empty.`);
2020
- }
2021
- mkdirSync6(root, { recursive: true });
2022
- info(c.dim(`Creating project ${c.bold(name)}…`));
2023
- const { uuid } = await createProject(apiUrl, token, name, flagStr(args, "team"));
2024
- info(c.dim("Fetching type definitions…"));
2025
- await materializeTypes(root, apiUrl);
2026
- writeTsconfig(root);
2027
- writeDefaultIgnore(root);
2028
- writeFileSync6(join7(root, "main.ts"), STARTER);
2029
- const commits = await getCommits(apiUrl, token, uuid);
2030
- writeManifest(root, { uuid, apiUrl, name, baseHeadId: commits.headId, files: {} });
2031
- success(`Created ${c.bold(name)} in ${c.bold(dir)}/`);
2032
- info(c.dim(` cd ${dir} — edit main.ts, then \`lecodes push -m "first commit"\` to save it`));
2033
- };
2034
-
2035
- // src/commands/design.ts
2036
- import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync10, readdirSync as readdirSync6, writeFileSync as writeFileSync8, appendFileSync } from "node:fs";
2037
- import { basename as basename2, join as join13 } from "node:path";
2038
-
2039
- // src/compile/designCompile.ts
2040
- import { readFileSync as readFileSync9 } from "node:fs";
2041
- import { basename } from "node:path";
2042
-
2043
- // src/localFiles.ts
2044
- import { readdirSync as readdirSync3, readFileSync as readFileSync5 } from "node:fs";
2045
- import { join as join8, relative } from "node:path";
2046
- var SKIP_DIRS = new Set([".lecodes", ".git", "node_modules"]);
2047
- var SKIP_FILES = new Set(["tsconfig.json", "jsconfig.json", ".gitignore", ".lecodesignore", ".DS_Store"]);
2048
- var TEXT_EXTS = new Set([
2049
- "ts",
2050
- "tsx",
2051
- "js",
2052
- "jsx",
2053
- "mjs",
2054
- "cjs",
2055
- "json",
2056
- "svg",
2057
- "glsl",
2058
- "vert",
2059
- "frag",
2060
- "txt",
2061
- "md",
2062
- "css",
2063
- "html",
2064
- "xml",
2065
- "yml",
2066
- "yaml",
2067
- "csv"
2068
- ]);
2069
- var ext = (name) => {
2070
- const i = name.lastIndexOf(".");
2071
- return i < 0 ? "" : name.slice(i + 1).toLowerCase();
2072
- };
2073
- var classifyKind = (name) => {
2074
- const e = ext(name);
2075
- if (e === "mat")
2076
- return "shader";
2077
- if (e === "scene")
2078
- return "scene";
2079
- return TEXT_EXTS.has(e) ? "text" : "resource";
2080
- };
2081
- var isTextKind = (kind) => kind !== "resource";
2082
- var scanLocal = (root, opts = {}) => {
2083
- const { ignore, tracked } = opts;
2084
- const files = [];
2085
- const dirs = [];
2086
- const ignored = [];
2087
- const isTracked = (key) => tracked?.has(key) ?? false;
2088
- const hasTrackedUnder = (key) => {
2089
- if (!tracked)
2090
- return false;
2091
- const prefix = key + "/";
2092
- for (const t of tracked)
2093
- if (t.startsWith(prefix))
2094
- return true;
2095
- return false;
2096
- };
2097
- const walk = (dir, parentIgnored) => {
2098
- for (const entry of readdirSync3(dir, { withFileTypes: true })) {
2099
- const abs = join8(dir, entry.name);
2100
- const key = toKey(relative(root, abs));
2101
- if (entry.isDirectory()) {
2102
- if (SKIP_DIRS.has(entry.name))
2103
- continue;
2104
- const dirIgnored = parentIgnored || !!ignore && ignore(key, true);
2105
- if (dirIgnored && !isTracked(key) && !hasTrackedUnder(key)) {
2106
- ignored.push(key + "/");
2107
- continue;
2108
- }
2109
- dirs.push(key);
2110
- walk(abs, dirIgnored);
2111
- } else if (entry.isFile()) {
2112
- if (SKIP_FILES.has(entry.name))
2113
- continue;
2114
- const fileIgnored = parentIgnored || !!ignore && ignore(key, false);
2115
- if (fileIgnored && !isTracked(key)) {
2116
- ignored.push(key);
2117
- continue;
2118
- }
2119
- files.push({ path: key, absPath: abs, sha: sha256(readFileSync5(abs)) });
2120
- }
2121
- }
2122
- };
2123
- walk(root, false);
2124
- return { files, dirs, ignored };
2125
- };
2126
- var scanProject = (root, manifest) => scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest.files)) });
2127
- var diffLocal = (manifest, scan) => {
2128
- const added = [];
2129
- const modified = [];
2130
- const localByPath = new Map(scan.files.map((f) => [f.path, f]));
2131
- for (const file of scan.files) {
2132
- const known = manifest.files[file.path];
2133
- if (!known)
2134
- added.push(file);
2135
- else if (known.sha !== file.sha)
2136
- modified.push(file);
4164
+ const root = resolve3(dir);
4165
+ if (existsSync10(root) && readdirSync5(root).length > 0) {
4166
+ throw new CliError(`Directory "${dir}" already exists and is not empty.`);
2137
4167
  }
2138
- const deleted = Object.keys(manifest.files).filter((path) => !localByPath.has(path));
2139
- const moves = new Map;
2140
- const claimed = new Set;
2141
- for (const file of added) {
2142
- const from = deleted.find((path) => !claimed.has(path) && manifest.files[path].sha === file.sha);
2143
- if (from) {
2144
- moves.set(file.path, from);
2145
- claimed.add(from);
2146
- }
4168
+ mkdirSync7(root, { recursive: true });
4169
+ info(c.dim(`Fetching project ${c.bold(project.name)}…`));
4170
+ const files = await materializeAssets(root, project, apiUrl, token, progress);
4171
+ endProgress();
4172
+ if (!flagBool(args, "no-types")) {
4173
+ info(c.dim("Fetching type definitions…"));
4174
+ await materializeTypes(root, apiUrl);
4175
+ writeTsconfig(root);
2147
4176
  }
2148
- return { added, modified, deleted, moves };
4177
+ writeDefaultIgnore(root);
4178
+ const commits = await getCommits(apiUrl, token, uuid);
4179
+ writeManifest(root, { uuid, apiUrl, name: project.name, baseHeadId: commits.headId, files });
4180
+ success(`Cloned ${c.bold(project.name)} into ${c.bold(dir)}/ (${Object.keys(files).length} files)`);
4181
+ info(c.dim(` cd ${dir} — edit, then \`lecodes push -m "your message"\``));
2149
4182
  };
2150
- var hasChanges = (diff) => diff.added.length + diff.modified.length + diff.deleted.length > 0;
2151
4183
 
2152
- // src/compile/projectCompile.ts
2153
- import { existsSync as existsSync9 } from "node:fs";
2154
- import { createRequire as createRequire3 } from "node:module";
2155
- import { dirname as dirname6, join as join11 } from "node:path";
2156
- init_compile();
2157
- var vendoredInject = join11(distRoot, "runtime", "sdk", "inject.ts");
2158
- if (existsSync9(vendoredInject))
2159
- process.env.LECODES_SDK_INJECT = vendoredInject;
2160
- var resolveChiselBin2 = () => {
2161
- if (process.env.LECODES_CHISEL_BIN)
2162
- return process.env.LECODES_CHISEL_BIN;
2163
- try {
2164
- const fromPkg = createRequire3(import.meta.url)("@letary/chisel").binaryPath();
2165
- if (fromPkg)
2166
- return fromPkg;
2167
- } catch {}
2168
- const local = join11(dirname6(process.execPath), process.platform === "win32" ? "chisel.exe" : "chisel");
2169
- return existsSync9(local) ? local : null;
4184
+ // src/commands/create.ts
4185
+ import { existsSync as existsSync11, mkdirSync as mkdirSync8, readdirSync as readdirSync6, writeFileSync as writeFileSync9 } from "node:fs";
4186
+ import { join as join13, resolve as resolve4 } from "node:path";
4187
+ var slug2 = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
4188
+ var STARTER = `// Welcome to le.codes — no imports needed, the whole SDK is available as globals.
4189
+ const scene = new Scene2D({ background: '#10131a' })
4190
+ scene.open()
4191
+
4192
+ setLoop((dt) => {
4193
+ // your per-frame logic — dt is seconds since the previous frame
4194
+ })
4195
+ `;
4196
+ var create = async (args) => {
4197
+ const config = loadConfig();
4198
+ const apiUrl = requireApiUrl(config);
4199
+ const token = requireToken(config);
4200
+ const name = args._[0];
4201
+ if (!name)
4202
+ throw new CliError("Usage: lecodes create <name> [directory]");
4203
+ const dir = args._[1] ?? slug2(name);
4204
+ const root = resolve4(dir);
4205
+ if (existsSync11(root) && readdirSync6(root).length > 0) {
4206
+ throw new CliError(`Directory "${dir}" already exists and is not empty.`);
4207
+ }
4208
+ mkdirSync8(root, { recursive: true });
4209
+ info(c.dim(`Creating project ${c.bold(name)}…`));
4210
+ const { uuid } = await createProject(apiUrl, token, name, flagStr(args, "team"));
4211
+ info(c.dim("Fetching type definitions…"));
4212
+ await materializeTypes(root, apiUrl);
4213
+ writeTsconfig(root);
4214
+ writeDefaultIgnore(root);
4215
+ writeFileSync9(join13(root, "main.ts"), STARTER);
4216
+ const commits = await getCommits(apiUrl, token, uuid);
4217
+ writeManifest(root, { uuid, apiUrl, name, baseHeadId: commits.headId, files: {} });
4218
+ success(`Created ${c.bold(name)} in ${c.bold(dir)}/`);
4219
+ info(c.dim(` cd ${dir} — edit main.ts, then \`lecodes push -m "first commit"\` to save it`));
2170
4220
  };
2171
- var chiselBin = resolveChiselBin2();
2172
- if (chiselBin)
2173
- process.env.LECODES_CHISEL_BIN = chiselBin;
2174
- var compileProject2 = (opts) => compileProject({
2175
- entries: opts.entries.map((e) => ({ path: e.path, type: e.type, text: e.text, fileSrc: e.fileSrc, shaders: e.shaders })),
2176
- name: opts.name,
2177
- publicUrl: opts.publicUrl,
2178
- entryOverride: opts.entryOverride,
2179
- header: opts.header,
2180
- format: opts.format,
2181
- localAssets: opts.localAssets,
2182
- minify: opts.minify,
2183
- editor: opts.editor
2184
- });
4221
+
4222
+ // src/commands/design.ts
4223
+ import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync14, readdirSync as readdirSync7, writeFileSync as writeFileSync11, appendFileSync } from "node:fs";
4224
+ import { basename as basename6, join as join15 } from "node:path";
4225
+
4226
+ // src/compile/designCompile.ts
4227
+ import { readFileSync as readFileSync13 } from "node:fs";
4228
+ import { basename as basename5 } from "node:path";
2185
4229
 
2186
4230
  // src/compile/assetIcons.ts
2187
- import { createHash as createHash2 } from "node:crypto";
2188
- import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "node:fs";
2189
- import { dirname as dirname7, join as join12 } from "node:path";
4231
+ import { createHash as createHash3 } from "node:crypto";
4232
+ import { existsSync as existsSync12, mkdirSync as mkdirSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync10 } from "node:fs";
4233
+ import { dirname as dirname9, join as join14 } from "node:path";
2190
4234
  var DEFAULT_REGISTRY = process.env.LECODES_ICON_REGISTRY?.trim() || "https://icon-registry.jt3.ru/v1";
2191
4235
  var ICON_ID = /^([a-z0-9]+(?:-[a-z0-9]+)*):([a-z0-9]+(?:-[a-z0-9]+)*)$/;
2192
- var isIdentStart = (c2) => c2 >= "A" && c2 <= "Z" || c2 >= "a" && c2 <= "z" || c2 === "_" || c2 === "$";
2193
- var isIdentPart = (c2) => isIdentStart(c2) || c2 >= "0" && c2 <= "9";
2194
- var skipString = (src, start) => {
4236
+ var isIdentStart2 = (c2) => c2 >= "A" && c2 <= "Z" || c2 >= "a" && c2 <= "z" || c2 === "_" || c2 === "$";
4237
+ var isIdentPart2 = (c2) => isIdentStart2(c2) || c2 >= "0" && c2 <= "9";
4238
+ var skipString2 = (src, start) => {
2195
4239
  const quote = src[start];
2196
4240
  const n = src.length;
2197
4241
  let i = start + 1;
@@ -2202,7 +4246,7 @@ var skipString = (src, start) => {
2202
4246
  continue;
2203
4247
  }
2204
4248
  if (quote === "`" && c2 === "$" && src[i + 1] === "{") {
2205
- i = skipBraces(src, i + 1);
4249
+ i = skipBraces2(src, i + 1);
2206
4250
  continue;
2207
4251
  }
2208
4252
  if (c2 === quote)
@@ -2211,14 +4255,14 @@ var skipString = (src, start) => {
2211
4255
  }
2212
4256
  return n;
2213
4257
  };
2214
- var skipBraces = (src, open) => {
4258
+ var skipBraces2 = (src, open) => {
2215
4259
  const n = src.length;
2216
4260
  let depth = 0;
2217
4261
  let i = open;
2218
4262
  while (i < n) {
2219
4263
  const c2 = src[i];
2220
4264
  if (c2 === '"' || c2 === "'" || c2 === "`") {
2221
- i = skipString(src, i);
4265
+ i = skipString2(src, i);
2222
4266
  continue;
2223
4267
  }
2224
4268
  if (c2 === "{")
@@ -2232,14 +4276,14 @@ var skipBraces = (src, open) => {
2232
4276
  }
2233
4277
  return n;
2234
4278
  };
2235
- var matchParen = (src, open) => {
4279
+ var matchParen2 = (src, open) => {
2236
4280
  const n = src.length;
2237
4281
  let depth = 0;
2238
4282
  let i = open;
2239
4283
  while (i < n) {
2240
4284
  const c2 = src[i];
2241
4285
  if (c2 === '"' || c2 === "'" || c2 === "`") {
2242
- i = skipString(src, i);
4286
+ i = skipString2(src, i);
2243
4287
  continue;
2244
4288
  }
2245
4289
  if (c2 === "/" && src[i + 1] === "/") {
@@ -2267,7 +4311,7 @@ var matchParen = (src, open) => {
2267
4311
  }
2268
4312
  return -1;
2269
4313
  };
2270
- var splitTopLevel = (text, delim) => {
4314
+ var splitTopLevel2 = (text, delim) => {
2271
4315
  const parts = [];
2272
4316
  const n = text.length;
2273
4317
  let depth = 0;
@@ -2276,7 +4320,7 @@ var splitTopLevel = (text, delim) => {
2276
4320
  while (i < n) {
2277
4321
  const c2 = text[i];
2278
4322
  if (c2 === '"' || c2 === "'" || c2 === "`") {
2279
- i = skipString(text, i);
4323
+ i = skipString2(text, i);
2280
4324
  continue;
2281
4325
  }
2282
4326
  if (c2 === "(" || c2 === "[" || c2 === "{")
@@ -2292,7 +4336,7 @@ var splitTopLevel = (text, delim) => {
2292
4336
  parts.push(text.slice(last));
2293
4337
  return parts;
2294
4338
  };
2295
- var stringLiteralValue = (text) => {
4339
+ var stringLiteralValue2 = (text) => {
2296
4340
  const t = text.trim();
2297
4341
  if (t.length < 2)
2298
4342
  return null;
@@ -2313,7 +4357,7 @@ var scanAssetIconCalls = (source) => {
2313
4357
  while (i < n) {
2314
4358
  const c2 = source[i];
2315
4359
  if (c2 === '"' || c2 === "'" || c2 === "`") {
2316
- i = skipString(source, i);
4360
+ i = skipString2(source, i);
2317
4361
  continue;
2318
4362
  }
2319
4363
  if (c2 === "/" && source[i + 1] === "/") {
@@ -2330,12 +4374,12 @@ var scanAssetIconCalls = (source) => {
2330
4374
  i += 2;
2331
4375
  continue;
2332
4376
  }
2333
- if (isIdentStart(c2)) {
4377
+ if (isIdentStart2(c2)) {
2334
4378
  let j = i + 1;
2335
- while (j < n && isIdentPart(source[j]))
4379
+ while (j < n && isIdentPart2(source[j]))
2336
4380
  j++;
2337
- if (source.slice(i, j) === "assetIcon" && prevNonSpace(source, i) !== ".") {
2338
- const call = parseCall(source, i, j);
4381
+ if (source.slice(i, j) === "assetIcon" && prevNonSpace2(source, i) !== ".") {
4382
+ const call = parseCall2(source, i, j);
2339
4383
  if (call) {
2340
4384
  calls.push(call);
2341
4385
  i = call.end;
@@ -2349,25 +4393,25 @@ var scanAssetIconCalls = (source) => {
2349
4393
  }
2350
4394
  return calls;
2351
4395
  };
2352
- var prevNonSpace = (src, i) => {
4396
+ var prevNonSpace2 = (src, i) => {
2353
4397
  let k = i - 1;
2354
4398
  while (k >= 0 && /\s/.test(src[k]))
2355
4399
  k--;
2356
4400
  return k >= 0 ? src[k] : "";
2357
4401
  };
2358
- var parseCall = (src, start, identEnd) => {
4402
+ var parseCall2 = (src, start, identEnd) => {
2359
4403
  const n = src.length;
2360
4404
  let i = identEnd;
2361
4405
  while (i < n && /\s/.test(src[i]))
2362
4406
  i++;
2363
4407
  if (src[i] !== "(")
2364
4408
  return null;
2365
- const close = matchParen(src, i);
4409
+ const close = matchParen2(src, i);
2366
4410
  if (close === -1)
2367
4411
  return null;
2368
4412
  const call = { start, end: close + 1, id: "", pack: "", name: "" };
2369
- const args = splitTopLevel(src.slice(i + 1, close), ",");
2370
- const id = stringLiteralValue(args[0] ?? "");
4413
+ const args = splitTopLevel2(src.slice(i + 1, close), ",");
4414
+ const id = stringLiteralValue2(args[0] ?? "");
2371
4415
  if (id === null) {
2372
4416
  call.error = `assetIcon(...) needs a string-literal id like "lucide:bell" (got \`${(args[0] ?? "").trim() || "()"}\`).`;
2373
4417
  return call;
@@ -2398,8 +4442,8 @@ var extractColor = (optsText) => {
2398
4442
  if (!optsText.startsWith("{") || !optsText.endsWith("}")) {
2399
4443
  return { error: `assetIcon(id, opts): opts must be an object literal like { color: "#5b8cff" }.` };
2400
4444
  }
2401
- for (const prop of splitTopLevel(optsText.slice(1, -1), ",")) {
2402
- const kv = splitTopLevel(prop, ":");
4445
+ for (const prop of splitTopLevel2(optsText.slice(1, -1), ",")) {
4446
+ const kv = splitTopLevel2(prop, ":");
2403
4447
  if (kv.length < 2)
2404
4448
  continue;
2405
4449
  const key = kv[0].trim().replace(/^["'`]|["'`]$/g, "");
@@ -2408,7 +4452,7 @@ var extractColor = (optsText) => {
2408
4452
  const raw = kv.slice(1).join(":").trim();
2409
4453
  if (!raw)
2410
4454
  continue;
2411
- const literal = stringLiteralValue(raw);
4455
+ const literal = stringLiteralValue2(raw);
2412
4456
  return literal !== null ? { bake: literal } : { tint: raw };
2413
4457
  }
2414
4458
  return {};
@@ -2429,34 +4473,34 @@ var iconExpr = (svg, call) => {
2429
4473
  return call.tint ? `Object.assign(${src}, { tintColor: (${call.tint}) })` : src;
2430
4474
  };
2431
4475
  var escapeTemplate = (s) => s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
2432
- var isHttp = (s) => /^https?:/i.test(s);
2433
- var sha12 = (s) => createHash2("sha256").update(s).digest("hex").slice(0, 12);
2434
- var registryHome = (registry) => registry.replace(/\/v\d+$/, "");
4476
+ var isHttp2 = (s) => /^https?:/i.test(s);
4477
+ var sha122 = (s) => createHash3("sha256").update(s).digest("hex").slice(0, 12);
4478
+ var registryHome2 = (registry) => registry.replace(/\/v\d+$/, "");
2435
4479
  var readText = async (registry, rel) => {
2436
- if (isHttp(registry)) {
4480
+ if (isHttp2(registry)) {
2437
4481
  const res = await fetch(`${registry}/${rel}`);
2438
4482
  return res.ok ? await res.text() : null;
2439
4483
  }
2440
4484
  try {
2441
- return readFileSync8(join12(registry, rel), "utf8");
4485
+ return readFileSync12(join14(registry, rel), "utf8");
2442
4486
  } catch {
2443
4487
  return null;
2444
4488
  }
2445
4489
  };
2446
- var readLock = (path) => {
4490
+ var readLock2 = (path) => {
2447
4491
  try {
2448
- const lock = JSON.parse(readFileSync8(path, "utf8"));
4492
+ const lock = JSON.parse(readFileSync12(path, "utf8"));
2449
4493
  if (lock && typeof lock === "object" && lock.icons && lock.packs)
2450
4494
  return lock;
2451
4495
  } catch {}
2452
4496
  return null;
2453
4497
  };
2454
- var serializeLock = (lock) => {
4498
+ var serializeLock2 = (lock) => {
2455
4499
  const sortKeys = (rec) => Object.fromEntries(Object.keys(rec).sort().map((k) => [k, rec[k]]));
2456
4500
  return JSON.stringify({ registry: lock.registry, icons: sortKeys(lock.icons), packs: sortKeys(lock.packs) }, null, 2) + `
2457
4501
  `;
2458
4502
  };
2459
- var renderNotices = (lock) => {
4503
+ var renderNotices2 = (lock) => {
2460
4504
  const packs = [...new Set(Object.values(lock.icons).map((i) => i.pack))].sort();
2461
4505
  const lines = [
2462
4506
  "# Icon attribution & notices",
@@ -2481,29 +4525,29 @@ var renderNotices = (lock) => {
2481
4525
  return lines.join(`
2482
4526
  `);
2483
4527
  };
2484
- var writeIfChanged = (path, text) => {
2485
- const current = existsSync10(path) ? safeRead(path) : null;
4528
+ var writeIfChanged2 = (path, text) => {
4529
+ const current = existsSync12(path) ? safeRead(path) : null;
2486
4530
  if (current === text)
2487
4531
  return false;
2488
- writeFileSync7(path, text);
4532
+ writeFileSync10(path, text);
2489
4533
  return true;
2490
4534
  };
2491
4535
  var safeRead = (path) => {
2492
4536
  try {
2493
- return readFileSync8(path, "utf8");
4537
+ return readFileSync12(path, "utf8");
2494
4538
  } catch {
2495
4539
  return null;
2496
4540
  }
2497
4541
  };
2498
4542
  var vendorIcons = async (designDir, ids, opts = {}) => {
2499
4543
  const registry = (opts.registry ?? DEFAULT_REGISTRY).replace(/\/+$/, "");
2500
- const lockPath = join12(designDir, "icons.lock.json");
2501
- const lock = readLock(lockPath) ?? { registry, icons: {}, packs: {} };
4544
+ const lockPath = join14(designDir, "icons.lock.json");
4545
+ const lock = readLock2(lockPath) ?? { registry, icons: {}, packs: {} };
2502
4546
  const svgById = new Map;
2503
4547
  const missing = [];
2504
4548
  for (const id of ids) {
2505
4549
  const li = lock.icons[id];
2506
- const cached = li && safeRead(join12(designDir, li.path));
4550
+ const cached = li && safeRead(join14(designDir, li.path));
2507
4551
  if (cached)
2508
4552
  svgById.set(id, cached);
2509
4553
  else
@@ -2519,23 +4563,23 @@ var vendorIcons = async (designDir, ids, opts = {}) => {
2519
4563
  const [, pack, name] = m;
2520
4564
  const lic = licenses[pack];
2521
4565
  if (!lic)
2522
- throw new Error(`assetIcon: unknown icon pack "${pack}" (in "${id}"). Browse packs at ${registryHome(registry)}.`);
4566
+ throw new Error(`assetIcon: unknown icon pack "${pack}" (in "${id}"). Browse packs at ${registryHome2(registry)}.`);
2523
4567
  const svg = await readText(registry, `${pack}/${name}.svg`);
2524
4568
  if (svg === null)
2525
- throw new Error(`assetIcon: icon "${id}" not found in the registry. Check the name at ${registryHome(registry)}.`);
4569
+ throw new Error(`assetIcon: icon "${id}" not found in the registry. Check the name at ${registryHome2(registry)}.`);
2526
4570
  const rel = `assets/icons/${pack}/${name}.svg`;
2527
- const abs = join12(designDir, rel);
2528
- mkdirSync7(dirname7(abs), { recursive: true });
2529
- writeFileSync7(abs, svg);
2530
- lock.icons[id] = { pack, name, hash: sha12(svg), spdx: lic.spdx, path: rel };
4571
+ const abs = join14(designDir, rel);
4572
+ mkdirSync9(dirname9(abs), { recursive: true });
4573
+ writeFileSync10(abs, svg);
4574
+ lock.icons[id] = { pack, name, hash: sha122(svg), spdx: lic.spdx, path: rel };
2531
4575
  lock.packs[pack] = { spdx: lic.spdx, title: lic.title, url: lic.url, attribution: lic.attribution, modifiedNotice: lic.modifiedNotice };
2532
4576
  svgById.set(id, svg);
2533
4577
  }
2534
4578
  lock.registry = registry;
2535
- writeIfChanged(lockPath, serializeLock(lock));
4579
+ writeIfChanged2(lockPath, serializeLock2(lock));
2536
4580
  }
2537
4581
  if (Object.keys(lock.icons).length)
2538
- writeIfChanged(join12(designDir, "ICON-NOTICES.md"), renderNotices(lock));
4582
+ writeIfChanged2(join14(designDir, "ICON-NOTICES.md"), renderNotices2(lock));
2539
4583
  return svgById;
2540
4584
  };
2541
4585
  var applyAssetIcons = async (entries, designDir, opts = {}) => {
@@ -2559,7 +4603,7 @@ var compileDesignScreen = async (opts) => {
2559
4603
  const entries = [];
2560
4604
  const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest?.files ?? {})) });
2561
4605
  for (const file of scan.files) {
2562
- const type = manifest?.files[file.path]?.type ?? classifyKind(basename(file.path));
4606
+ const type = manifest?.files[file.path]?.type ?? classifyKind(basename5(file.path));
2563
4607
  if (type === "resource") {
2564
4608
  entries.push({
2565
4609
  path: file.path,
@@ -2570,7 +4614,7 @@ var compileDesignScreen = async (opts) => {
2570
4614
  } else if (type === "shader") {
2571
4615
  entries.push({ path: file.path, type, shaders: [] });
2572
4616
  } else {
2573
- entries.push({ path: file.path, type, text: readFileSync9(file.absPath, "utf8") });
4617
+ entries.push({ path: file.path, type, text: readFileSync13(file.absPath, "utf8") });
2574
4618
  }
2575
4619
  }
2576
4620
  const invoke = state !== undefined ? `screen(${JSON.stringify(state)})` : "screen()";
@@ -2584,6 +4628,18 @@ var compileDesignScreen = async (opts) => {
2584
4628
  `
2585
4629
  });
2586
4630
  await applyAssetIcons(entries, designDir);
4631
+ await applyProjectFonts(entries, {
4632
+ mode: "vendor",
4633
+ root,
4634
+ vendorDir: designDir,
4635
+ vendorPrefix: designKey,
4636
+ makeResourceEntry: (path, absPath) => ({
4637
+ path,
4638
+ type: "resource",
4639
+ fileSrc: opts.assets === "disk" ? encodeURIComponent(absPath) : encodeURI(path),
4640
+ absPath
4641
+ })
4642
+ });
2587
4643
  return compileProject2({
2588
4644
  entries,
2589
4645
  name: opts.name,
@@ -2630,10 +4686,10 @@ var openBrowser = (url) => {
2630
4686
  };
2631
4687
  var browserLogin = async (webBase, timeoutMs = 5 * 60 * 1000, open = openBrowser) => {
2632
4688
  const state = randomBytes(16).toString("hex");
2633
- let resolve3;
4689
+ let resolve5;
2634
4690
  let reject;
2635
4691
  const tokenPromise = new Promise((res, rej) => {
2636
- resolve3 = res;
4692
+ resolve5 = res;
2637
4693
  reject = rej;
2638
4694
  });
2639
4695
  const server = createServer((req, res) => {
@@ -2651,7 +4707,7 @@ var browserLogin = async (webBase, timeoutMs = 5 * 60 * 1000, open = openBrowser
2651
4707
  }
2652
4708
  res.writeHead(200, { "content-type": "text/html" });
2653
4709
  res.end(SUCCESS_HTML);
2654
- resolve3(token);
4710
+ resolve5(token);
2655
4711
  });
2656
4712
  await new Promise((res, rej) => {
2657
4713
  server.once("error", rej);
@@ -2715,6 +4771,9 @@ Behaviour that isn't visible in a single screen — validation, permissions, not
2715
4771
  cases — and the decisions you've made and why, so they aren't re-litigated later.
2716
4772
  `;
2717
4773
  var TOKENS_TS = `// Design tokens shared by every screen. Colors are hex strings (the engine parses hex only).
4774
+ // App font: one theme() call here sets the default text font everywhere. font("id") installs a
4775
+ // registry family (vendored at compile time) and returns its name — no loading code needed.
4776
+ // theme({ fontFamily: font("manrope") })
2718
4777
  export const colors = {
2719
4778
  bg: "#101114",
2720
4779
  card: "#1c1e24",
@@ -2728,6 +4787,57 @@ export const spacing = { xs: 4, sm: 8, md: 16, lg: 24 }
2728
4787
 
2729
4788
  export const radius = { sm: 8, md: 12, lg: 20 }
2730
4789
  `;
4790
+ var TABS_TS = `// The app's tab bar, declared ONCE — the design canvas discovers it from this file.
4791
+ //
4792
+ // export const mainTabs = defineTabs({
4793
+ // home: { label: "Home", icon: assetIcon("lucide:house") },
4794
+ // news: { label: "News", icon: assetIcon("lucide:newspaper") },
4795
+ // })
4796
+ //
4797
+ // The KEYS are screen ids — each tab's root screen (screens/<key>.ts), in tab order, literals only.
4798
+ // Every screen that shows the bar mounts it with a literal tab id — tab roots AND pushed details
4799
+ // that keep the bar:
4800
+ //
4801
+ // export default () => mainTabs.screen("news", [ ...content ])
4802
+ //
4803
+ // A screen that doesn't mount it is a full-screen push (bar hidden). Never draw meta.json edges
4804
+ // between tab screens — the bar already navigates; the board renders it as a rail + lanes.
4805
+ // Restyle the bar below freely: only the defineTabs({ keys }) declaration and the
4806
+ // .screen("<tab>", …) / .bar("<tab>") call shapes are parsed.
4807
+
4808
+ type TabDef = { label: string, icon?: { svg: string, tintColor: string | null } }
4809
+
4810
+ // Theme-variable driven — theme({ primaryColor, mutedColor, tabbarBg, screenBg }) anywhere
4811
+ // restyles every screen's bar at once; the fallbacks keep an untouched scaffold looking right.
4812
+ const ACTIVE = "var(--primaryColor, #5b8cff)"
4813
+ const INACTIVE = "var(--mutedColor, #8a919e)"
4814
+ const BAR_BG = "var(--tabbarBg, #15171c)"
4815
+ const SCREEN_BG = "var(--screenBg, #101114)"
4816
+
4817
+ export const defineTabs = <T extends Record<string, TabDef>>(tabs: T) => {
4818
+ const ids = Object.keys(tabs) as (keyof T & string)[]
4819
+
4820
+ /** The bar alone — mount it yourself when the screen owns its whole layout. */
4821
+ const bar = (active: keyof T & string) =>
4822
+ UIRow(ids.map((id) => {
4823
+ const color = id === active ? ACTIVE : INACTIVE
4824
+ return UIButton([
4825
+ ...(tabs[id].icon ? [UIImage(tabs[id].icon!).style({ width: 22, height: 22, tintColor: color })] : []),
4826
+ UIText(tabs[id].label).style({ fontSize: 10, color }),
4827
+ ]).style({ name: \`tab-\${id}\`, flexDirection: "column", gap: 3, flexGrow: 1, flexBasis: 0, pt: 8, pb: 6 })
4828
+ })).style({ bgColor: BAR_BG, pb: "safe-bottom" })
4829
+
4830
+ /** A screen that sits ON the bar (the common case): [ content, bar ] — the same column the real
4831
+ * app composes as [ UIPager, bar ]. */
4832
+ const screen = (active: keyof T & string, children: UINodeChild[]) =>
4833
+ UIScreen([
4834
+ UIColumn(children).style({ flexGrow: 1, p: 16, pt: "safe-top" }),
4835
+ bar(active),
4836
+ ]).style({ bgColor: SCREEN_BG })
4837
+
4838
+ return { ids, bar, screen }
4839
+ }
4840
+ `;
2731
4841
  var HOME_SCREEN_TS = `import { colors, spacing, radius } from "../shared/tokens"
2732
4842
 
2733
4843
  // Mock data: the domain shown on realistic values. Inline, per screen.
@@ -2806,6 +4916,12 @@ positions or descriptions.
2806
4916
 
2807
4917
  - An edge's optional \`activator\` names the element (its \`name\` style option) that triggers the
2808
4918
  transition. Give a \`name\` to interactive elements an edge refers to — nothing else needs names.
4919
+ - **Edges are forward intent only.** Never draw back/dismiss/logout edges — the navigation stack
4920
+ implies return paths. And never draw edges between tab screens — the tab bar already navigates
4921
+ (see "The tab bar" below); the board flags bar-implied edges for deletion.
4922
+ - **An edge from a screen to itself** (\`"from": "home", "to": "home@empty"\`) is a *state link*: the
4923
+ board draws it as a chip on the activator element (clicking it switches the tile's state), never
4924
+ as an arrow. Use it to record "tapping X puts this screen into state Y".
2809
4925
  - **Group each screen** you create by its user-facing area — \`"group": "Onboarding"\` — 2–6 screens
2810
4926
  per group (Onboarding, Checkout, Settings…). Groups render as labeled regions and \`arrange\` lays
2811
4927
  each out as its own sub-flow, so grouping keeps big boards legible. Semantic clustering is your
@@ -2853,6 +4969,25 @@ keep in the chat.
2853
4969
  typing a component's return value or a container's children — with no imports. Trust the types;
2854
4970
  don't fall back to \`any\`.
2855
4971
 
4972
+ ## The tab bar
4973
+
4974
+ Most apps have one — declare it ONCE, in source, and never draw edges for it:
4975
+
4976
+ - \`shared/tabs.ts\` ships a \`defineTabs\` helper. Declare the bar there:
4977
+ \`export const mainTabs = defineTabs({ home: { label: "Home", icon: assetIcon("lucide:house") }, … })\`
4978
+ — the **keys are screen ids** (each tab's root screen, in tab order), literals only.
4979
+ - Every screen that shows the bar mounts it with a **literal** tab id:
4980
+ \`export default () => mainTabs.screen("news", [ …content ])\` — tab roots AND pushed detail
4981
+ screens that keep the bar (the news lane's article screen mounts \`"news"\` too). A screen that
4982
+ doesn't mount it is a full-screen push (bar hidden), e.g. a photo viewer.
4983
+ - The canvas discovers all of this from the source (like screen states) and draws the bar as a
4984
+ **rail + lane bands** — tab switching needs NO edges, and \`design_state\` serves the discovered
4985
+ \`hubs\`. In the live prototype, tapping a tab button jumps the board to that tab's root screen.
4986
+ - Style through theme variables: the bar reads \`var(--primaryColor)\`, \`var(--mutedColor)\`,
4987
+ \`var(--tabbarBg)\`, \`var(--screenBg)\` (with fallbacks) — one \`theme({ primaryColor: … })\` call
4988
+ restyles it app-wide. You may restyle \`shared/tabs.ts\` itself; keep the \`defineTabs({ … })\`
4989
+ declaration and \`.screen("<tab>", …)\` call shapes intact (the canvas parses them).
4990
+
2856
4991
  ## Screen states (variants)
2857
4992
 
2858
4993
  A screen often has more than one look — a list vs. its empty state, a form vs. its error state.
@@ -2860,10 +4995,9 @@ Instead of a second file per variant, make the default export a **function of a
2860
4995
  type is a string-literal union** — the union IS the state list (no separate schema):
2861
4996
 
2862
4997
  \`\`\`ts
2863
- export default (state: "default" | "empty" = "default") => UIScreen([
4998
+ export default (state: "default" | "empty" = "default") => mainTabs.screen("home", [
2864
4999
  Header(child),
2865
5000
  state === "empty" ? EmptyState("Nothing here yet") : UIScrollable(items.map(Card)),
2866
- TabBar(tabs, "home"),
2867
5001
  ])
2868
5002
  \`\`\`
2869
5003
 
@@ -2881,6 +5015,35 @@ export default (state: "default" | "empty" = "default") => UIScreen([
2881
5015
  \`"home@empty"\`) — still one node per screen. \`lecodes design check\` errors on an edge pointing at
2882
5016
  a state the screen doesn't declare.
2883
5017
 
5018
+ ## Overlays (sheets & dialogs)
5019
+
5020
+ A bottom sheet or confirm dialog is a **\`UIWidget\`**, not an absolutely-positioned child of the
5021
+ screen. Its \`overlayColor\` is the scrim — a real modal layer: it blocks everything beneath it
5022
+ (taps AND the canvas's hover/hints), and a tap on it fires \`onOverlayTap\`. \`show()\` it from the
5023
+ state branch instead of embedding it:
5024
+
5025
+ \`\`\`ts
5026
+ const FilterSheet = () => {
5027
+ const sheet = UIWidget(
5028
+ { left: 0, right: 0, bottom: 0, p: 16, gap: 8, overlayColor: "#0008" },
5029
+ [ SheetCard([ … ]), PrimaryButton("Apply", { name: "apply" }) ],
5030
+ )
5031
+ return sheet.onOverlayTap(() => sheet.hide())
5032
+ }
5033
+
5034
+ export default (state: "default" | "filters" = "default") => {
5035
+ if (state === "filters") FilterSheet().show()
5036
+ return UIScreen([ … ])
5037
+ }
5038
+ \`\`\`
5039
+
5040
+ - Size the widget box to the **content only** (bottom-anchor a sheet, or center a dialog card
5041
+ inside a full-inset box) — the scrim is the \`overlayColor\`, painted full-screen behind it.
5042
+ - A tap lands **outside the widget box** → \`onOverlayTap\` (dismiss a sheet there). A confirm
5043
+ dialog that demands an explicit choice uses a full-inset box and no \`onOverlayTap\`.
5044
+ - Headless snapshots render widgets as \`overlays\` in the JSON and composite them in the PNG,
5045
+ and the edge \`activator\` names on widget elements (\`"apply"\`, \`"confirm"\`) work as usual.
5046
+
2884
5047
  ## Icons
2885
5048
 
2886
5049
  Use real icons instead of emoji. \`assetIcon("pack:name")\` is a **compile-time macro**: it vendors
@@ -2903,6 +5066,28 @@ UIImage(assetIcon("lucide:check")).style({ width: 20, height: 20, tintColor: col
2903
5066
  (attribution) — **commit these**; they make rebuilds offline. An unknown \`pack:name\` is a clear
2904
5067
  compile error.
2905
5068
 
5069
+ ## Fonts
5070
+
5071
+ Real typography sells a design. \`font("id")\` installs a font from the registry AND returns its
5072
+ family name — no loading code, no awaiting, ever:
5073
+
5074
+ \`\`\`ts
5075
+ theme({ fontFamily: font("manrope") }) // app font, in shared/tokens.ts
5076
+ UIText("Hero").style({ fontFamily: font("unbounded"), fontSize: 34 }) // display accents per node
5077
+ font("ibm-plex-sans", { weights: [400, 600] }) // pick exact weights
5078
+ \`\`\`
5079
+
5080
+ - Registry ids (all OFL, cyrillic-capable unless noted):
5081
+ sans \`inter manrope golos-text onest rubik nunito montserrat ibm-plex-sans pt-sans jost exo-2\`;
5082
+ display \`unbounded russo-one oswald comfortaa space-grotesk\` (latin only);
5083
+ serif \`playfair-display lora pt-serif cormorant literata\`;
5084
+ mono \`jetbrains-mono fira-code ibm-plex-mono\`; handwriting \`caveat pacifico\`.
5085
+ - Default weights are ~400/500/700 — install the weights you use (\`fontWeight: 600\` wants a 600
5086
+ face). The id must be a **string literal**; an unknown one is a clear compile error.
5087
+ - Your own file works too: \`font("./fonts/Brand.ttf")\` — family/weight are read from the file.
5088
+ - Vendored faces land in \`assets/fonts/\`, tracked by \`fonts.lock.json\` + \`FONT-NOTICES.md\` —
5089
+ **commit these**; they make rebuilds offline and they're what the platform board renders from.
5090
+
2906
5091
  ## See what you built
2907
5092
 
2908
5093
  The developer watches the canvas live (screens hot-reload on save). To see a screen yourself:
@@ -2953,6 +5138,15 @@ var DESIGN_MACROS_DTS = `// Generated by \`lecodes design init\`. Globals the de
2953
5138
  * string literal (resolved, and the SVG vendored, at compile time).
2954
5139
  */
2955
5140
  declare function assetIcon(id: string, opts?: { color?: string }): { readonly svg: string, tintColor: string | null }
5141
+
5142
+ /**
5143
+ * Install a font and get its family name back — using a font IS declaring it. A registry id
5144
+ * (\`font("manrope")\`) vendors faces from the font registry at compile time ({ weights, italic }
5145
+ * narrows the set); a project path (\`font("./fonts/Brand.ttf")\`) reads family/weight/italic from
5146
+ * the file itself. The argument must be a string literal. No awaiting — pass the result straight
5147
+ * to \`theme({ fontFamily: font("manrope") })\` or a \`fontFamily\` style.
5148
+ */
5149
+ declare function font(id: string, opts?: { weights?: number[], italic?: boolean }): string
2956
5150
  `;
2957
5151
  var README_HINT = `# design/
2958
5152
 
@@ -2977,33 +5171,33 @@ var getContext = (args) => {
2977
5171
  return {
2978
5172
  root,
2979
5173
  manifest,
2980
- name: manifest?.name ?? basename2(root),
5174
+ name: manifest?.name ?? basename6(root),
2981
5175
  dirName,
2982
- designDir: join13(root, dirName),
5176
+ designDir: join15(root, dirName),
2983
5177
  designKey: "/" + dirName.replace(/\\/g, "/")
2984
5178
  };
2985
5179
  };
2986
5180
  var listScreens = (designDir) => {
2987
- const dir = join13(designDir, "screens");
2988
- if (!existsSync11(dir))
5181
+ const dir = join15(designDir, "screens");
5182
+ if (!existsSync13(dir))
2989
5183
  return [];
2990
- return readdirSync6(dir).filter((f) => f.endsWith(".ts") && !f.startsWith(".")).map((f) => f.slice(0, -3)).sort();
5184
+ return readdirSync7(dir).filter((f2) => f2.endsWith(".ts") && !f2.startsWith(".")).map((f2) => f2.slice(0, -3)).sort();
2991
5185
  };
2992
5186
  var readMetaSizes = (designDir) => {
2993
5187
  try {
2994
- const meta = JSON.parse(readFileSync10(join13(designDir, "meta.json"), "utf8"));
5188
+ const meta = JSON.parse(readFileSync14(join15(designDir, "meta.json"), "utf8"));
2995
5189
  return meta?.screens ?? {};
2996
5190
  } catch {
2997
5191
  return {};
2998
5192
  }
2999
5193
  };
3000
5194
  var writeMcpConfig = (ctx, port) => {
3001
- const path = join13(ctx.root, ".mcp.json");
5195
+ const path = join15(ctx.root, ".mcp.json");
3002
5196
  const entry = { type: "http", url: `http://localhost:${port}/mcp` };
3003
5197
  let config = {};
3004
- if (existsSync11(path)) {
5198
+ if (existsSync13(path)) {
3005
5199
  try {
3006
- config = JSON.parse(readFileSync10(path, "utf8"));
5200
+ config = JSON.parse(readFileSync14(path, "utf8"));
3007
5201
  } catch {
3008
5202
  warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-design server by hand.");
3009
5203
  return;
@@ -3016,7 +5210,7 @@ var writeMcpConfig = (ctx, port) => {
3016
5210
  return;
3017
5211
  }
3018
5212
  config.mcpServers["lecodes-design"] = entry;
3019
- writeFileSync8(path, JSON.stringify(config, null, 2) + `
5213
+ writeFileSync11(path, JSON.stringify(config, null, 2) + `
3020
5214
  `);
3021
5215
  log(`${c.green(existing ? "updated" : "created")} .mcp.json (lecodes-design MCP → ${entry.url})`);
3022
5216
  };
@@ -3026,25 +5220,26 @@ var init = async (ctx, args) => {
3026
5220
  ["spec.md", SPEC_MD],
3027
5221
  ["README.md", README_HINT],
3028
5222
  ["CLAUDE.md", CLAUDE_MD],
3029
- [join13("shared", "tokens.ts"), TOKENS_TS],
3030
- [join13("screens", "home.ts"), HOME_SCREEN_TS]
5223
+ [join15("shared", "tokens.ts"), TOKENS_TS],
5224
+ [join15("shared", "tabs.ts"), TABS_TS],
5225
+ [join15("screens", "home.ts"), HOME_SCREEN_TS]
3031
5226
  ];
3032
- mkdirSync8(join13(ctx.designDir, "screens"), { recursive: true });
3033
- mkdirSync8(join13(ctx.designDir, "shared"), { recursive: true });
5227
+ mkdirSync10(join15(ctx.designDir, "screens"), { recursive: true });
5228
+ mkdirSync10(join15(ctx.designDir, "shared"), { recursive: true });
3034
5229
  let created = 0;
3035
5230
  for (const [rel, content] of files) {
3036
- const abs = join13(ctx.designDir, rel);
3037
- if (existsSync11(abs)) {
5231
+ const abs = join15(ctx.designDir, rel);
5232
+ if (existsSync13(abs)) {
3038
5233
  note(`kept ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
3039
5234
  continue;
3040
5235
  }
3041
- writeFileSync8(abs, content);
5236
+ writeFileSync11(abs, content);
3042
5237
  log(`${c.green("created")} ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
3043
5238
  created++;
3044
5239
  }
3045
- const ignorePath = join13(ctx.root, ".lecodesignore");
5240
+ const ignorePath = join15(ctx.root, ".lecodesignore");
3046
5241
  const ignoreLine = `${ctx.dirName}/.snapshots/`;
3047
- const current = existsSync11(ignorePath) ? readFileSync10(ignorePath, "utf8") : "";
5242
+ const current = existsSync13(ignorePath) ? readFileSync14(ignorePath, "utf8") : "";
3048
5243
  if (!current.split(/\r?\n/).includes(ignoreLine)) {
3049
5244
  appendFileSync(ignorePath, (current && !current.endsWith(`
3050
5245
  `) ? `
@@ -3055,8 +5250,8 @@ var init = async (ctx, args) => {
3055
5250
  writeMcpConfig(ctx, Number(flagStr(args, "port")) || DEFAULT_PORT);
3056
5251
  const wroteTypes = materializeTypesLocal(ctx.designDir);
3057
5252
  writeTsconfig(ctx.designDir);
3058
- mkdirSync8(join13(ctx.designDir, ".lecodes", "types"), { recursive: true });
3059
- writeFileSync8(join13(ctx.designDir, ".lecodes", "types", "design-macros.d.ts"), DESIGN_MACROS_DTS);
5253
+ mkdirSync10(join15(ctx.designDir, ".lecodes", "types"), { recursive: true });
5254
+ writeFileSync11(join15(ctx.designDir, ".lecodes", "types", "design-macros.d.ts"), DESIGN_MACROS_DTS);
3060
5255
  log(`${c.green("created")} ${ctx.dirName}/tsconfig.json${wroteTypes ? ` + ${ctx.dirName}/.lecodes/types/` : ""} (IDE types)`);
3061
5256
  if (!wroteTypes)
3062
5257
  warnErr("Bundled SDK types not found — tsconfig written, but globals may show unresolved until the CLI is rebuilt with vendored types.");
@@ -3105,16 +5300,16 @@ var snapshot = async (ctx, args) => {
3105
5300
  killer.unref?.();
3106
5301
  const png = flagBool(args, "png") || flagStr(args, "png") !== undefined;
3107
5302
  const sizes = readMetaSizes(ctx.designDir);
3108
- const outDir = flagStr(args, "out-dir") ?? join13(ctx.designDir, ".snapshots");
5303
+ const outDir = flagStr(args, "out-dir") ?? join15(ctx.designDir, ".snapshots");
3109
5304
  const single = targets.length === 1 && requested.length === 1 && !png && !allStates && !flagStr(args, "out-dir");
3110
5305
  const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
3111
5306
  if (!single)
3112
- mkdirSync8(outDir, { recursive: true });
5307
+ mkdirSync10(outDir, { recursive: true });
3113
5308
  for (const id of targets) {
3114
5309
  const [width, height] = sizes[id]?.size ?? [390, 844];
3115
5310
  let plan = [{ state: undefined, bases: [id] }];
3116
5311
  if (discover) {
3117
- const { states, canonical } = discover(readFileSync10(join13(ctx.designDir, "screens", `${id}.ts`), "utf8"));
5312
+ const { states, canonical } = discover(readFileSync14(join15(ctx.designDir, "screens", `${id}.ts`), "utf8"));
3118
5313
  if (stateFlag !== undefined) {
3119
5314
  if (!states.includes(stateFlag)) {
3120
5315
  if (requested.length === 1)
@@ -3155,18 +5350,18 @@ var snapshot = async (ctx, args) => {
3155
5350
  for (const w of pngResult.warnings)
3156
5351
  warnErr(`${tag}: ${w}`);
3157
5352
  for (const base of bases) {
3158
- writeFileSync8(join13(outDir, `${base}.json`), result.text);
3159
- log(`${c.green("✓")} ${tag} → ${join13(outDir, `${base}.json`)}`);
5353
+ writeFileSync11(join15(outDir, `${base}.json`), result.text);
5354
+ log(`${c.green("✓")} ${tag} → ${join15(outDir, `${base}.json`)}`);
3160
5355
  if (pngResult) {
3161
- writeFileSync8(join13(outDir, `${base}.png`), pngResult.png);
3162
- log(`${c.green("✓")} ${tag} → ${join13(outDir, `${base}.png`)}`);
5356
+ writeFileSync11(join15(outDir, `${base}.png`), pngResult.png);
5357
+ log(`${c.green("✓")} ${tag} → ${join15(outDir, `${base}.png`)}`);
3163
5358
  }
3164
5359
  }
3165
5360
  }
3166
5361
  }
3167
5362
  };
3168
5363
  var serve = async (ctx, args) => {
3169
- if (!existsSync11(ctx.designDir)) {
5364
+ if (!existsSync13(ctx.designDir)) {
3170
5365
  throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
3171
5366
  }
3172
5367
  let designServer;
@@ -3239,7 +5434,7 @@ var serve = async (ctx, args) => {
3239
5434
  openBrowser(server.url);
3240
5435
  };
3241
5436
  var arrange = async (ctx, args) => {
3242
- if (!existsSync11(ctx.designDir)) {
5437
+ if (!existsSync13(ctx.designDir)) {
3243
5438
  throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
3244
5439
  }
3245
5440
  let designServer;
@@ -3275,7 +5470,7 @@ var share = async (ctx, args) => {
3275
5470
  note("Turn it off with: lecodes design share --off");
3276
5471
  };
3277
5472
  var check = async (ctx, args) => {
3278
- if (!existsSync11(ctx.designDir)) {
5473
+ if (!existsSync13(ctx.designDir)) {
3279
5474
  throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
3280
5475
  }
3281
5476
  let designServer;
@@ -3312,6 +5507,14 @@ var check = async (ctx, args) => {
3312
5507
  log(` ${c.yellow("!")} ${s.id}: ${s.statesWarning}`);
3313
5508
  log("");
3314
5509
  }
5510
+ if (state.hubs.length) {
5511
+ log(c.bold("Tab bars"));
5512
+ for (const h of state.hubs) {
5513
+ const mounted = Object.keys(h.lanes).length;
5514
+ log(` ${h.name} (${h.file}): ${h.tabs.join(" · ")} — ${mounted} screen${mounted === 1 ? "" : "s"} on the bar`);
5515
+ }
5516
+ log("");
5517
+ }
3315
5518
  const hard = state.warnings.length;
3316
5519
  if (hard === 0) {
3317
5520
  const n = state.screens.length;
@@ -3340,36 +5543,15 @@ var design = async (args) => {
3340
5543
  };
3341
5544
 
3342
5545
  // src/commands/dev.ts
3343
- import { existsSync as existsSync13, mkdirSync as mkdirSync9, readFileSync as readFileSync12, watch, writeFileSync as writeFileSync9 } from "node:fs";
5546
+ import { existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync15, watch, writeFileSync as writeFileSync12 } from "node:fs";
3344
5547
  import { randomBytes as randomBytes2 } from "node:crypto";
3345
5548
  import { networkInterfaces } from "node:os";
3346
- import { basename as basename4, join as join15 } from "node:path";
5549
+ import { basename as basename7, join as join17 } from "node:path";
3347
5550
 
3348
5551
  // src/dev/devServer.ts
3349
5552
  import { createServer as createServer2 } from "node:http";
3350
- import { createReadStream, existsSync as existsSync12, statSync as statSync2 } from "node:fs";
3351
- import { join as join14 } from "node:path";
3352
-
3353
- // src/compile/collectLocal.ts
3354
- import { readFileSync as readFileSync11 } from "node:fs";
3355
- import { basename as basename3 } from "node:path";
3356
- var collectLocalEntries = (root, manifest, assetBase) => {
3357
- const entries = [];
3358
- const warnings = [];
3359
- const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest?.files ?? {})) });
3360
- for (const file of scan.files) {
3361
- const type = manifest?.files[file.path]?.type ?? classifyKind(basename3(file.path));
3362
- if (type === "resource") {
3363
- entries.push({ path: file.path, type, fileSrc: assetBase + encodeURI(file.path), absPath: file.absPath });
3364
- } else if (type === "shader") {
3365
- warnings.push(`${file.path}: shaders compile on the server — push the project once for shaders to work in dev mode.`);
3366
- entries.push({ path: file.path, type, shaders: [] });
3367
- } else {
3368
- entries.push({ path: file.path, type, text: readFileSync11(file.absPath, "utf8") });
3369
- }
3370
- }
3371
- return { entries, warnings };
3372
- };
5553
+ import { createReadStream, existsSync as existsSync14, statSync as statSync3 } from "node:fs";
5554
+ import { join as join16 } from "node:path";
3373
5555
 
3374
5556
  // src/dev/clientTemplates.ts
3375
5557
  var DEV_SCREEN_TS = `const title = "LeCodes Dev"
@@ -3484,9 +5666,9 @@ var renderPrelude = (p) => DEV_PRELUDE_JS.split("__DEV_ORIGIN__").join(p.origin)
3484
5666
  var renderDevScreen = (name) => DEV_SCREEN_TS.split("__DEV_NAME__").join(JSON.stringify(name));
3485
5667
 
3486
5668
  // src/dev/wsServer.ts
3487
- import { createHash as createHash3 } from "node:crypto";
5669
+ import { createHash as createHash4 } from "node:crypto";
3488
5670
  var WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
3489
- var acceptKey = (key) => createHash3("sha1").update(key + WS_GUID).digest("base64");
5671
+ var acceptKey = (key) => createHash4("sha1").update(key + WS_GUID).digest("base64");
3490
5672
  var encodeTextFrame = (text) => {
3491
5673
  const payload = Buffer.from(text, "utf8");
3492
5674
  const len = payload.length;
@@ -3633,14 +5815,14 @@ var sendText = (res, status, type, body) => {
3633
5815
  res.end(body);
3634
5816
  };
3635
5817
  var sendJson = (res, status, body) => sendText(res, status, "application/json", JSON.stringify(body));
3636
- var readBody = (req) => new Promise((resolve3) => {
5818
+ var readBody = (req) => new Promise((resolve5) => {
3637
5819
  let body = "";
3638
5820
  req.on("data", (c2) => {
3639
5821
  if (body.length < 256 * 1024)
3640
5822
  body += c2.toString("utf8");
3641
5823
  });
3642
- req.on("end", () => resolve3(body));
3643
- req.on("error", () => resolve3(body));
5824
+ req.on("end", () => resolve5(body));
5825
+ req.on("error", () => resolve5(body));
3644
5826
  });
3645
5827
  var startDevServer = async (options) => {
3646
5828
  const { root, manifest, name, origin, token, entryOverride } = options;
@@ -3653,7 +5835,15 @@ var startDevServer = async (options) => {
3653
5835
  const compileOnce = async () => {
3654
5836
  try {
3655
5837
  const { entries, warnings } = collectLocalEntries(root, manifest, `${base}/assets`);
3656
- const js = await compileProject2({ entries, name, publicUrl: origin, entryOverride, header: true });
5838
+ const fontsHeader = await applyProjectFonts(entries, {
5839
+ mode: "vendor",
5840
+ root,
5841
+ vendorDir: root,
5842
+ vendorPrefix: "",
5843
+ publicUrl: origin,
5844
+ makeResourceEntry: (path, absPath) => ({ path, type: "resource", fileSrc: `${base}/assets` + encodeURI(path), absPath })
5845
+ });
5846
+ const js = await compileProject2({ entries, name, publicUrl: origin, entryOverride, header: true, fontsHeader });
3657
5847
  const changed = bundleJs === null || stripVolatile(js) !== stripVolatile(bundleJs);
3658
5848
  if (changed) {
3659
5849
  bundleJs = js;
@@ -3725,8 +5915,8 @@ var startDevServer = async (options) => {
3725
5915
  } catch {
3726
5916
  return sendText(res, 400, "text/plain", "Bad path");
3727
5917
  }
3728
- const abs = join14(root, key.slice(1));
3729
- if (!existsSync12(abs) || !statSync2(abs).isFile())
5918
+ const abs = join16(root, key.slice(1));
5919
+ if (!existsSync14(abs) || !statSync3(abs).isFile())
3730
5920
  return sendText(res, 404, "text/plain", "Not found");
3731
5921
  res.writeHead(200, { "content-type": contentType(key), "cache-control": "no-store" });
3732
5922
  createReadStream(abs).pipe(res);
@@ -3769,11 +5959,11 @@ var startDevServer = async (options) => {
3769
5959
  if (conn)
3770
5960
  clients.add(conn);
3771
5961
  });
3772
- await new Promise((resolve3, reject) => {
5962
+ await new Promise((resolve5, reject) => {
3773
5963
  server.once("error", reject);
3774
5964
  server.listen(options.port, options.bind ?? "0.0.0.0", () => {
3775
5965
  server.removeListener("error", reject);
3776
- resolve3();
5966
+ resolve5();
3777
5967
  });
3778
5968
  });
3779
5969
  const address = server.address();
@@ -3809,24 +5999,24 @@ var pickLanAddress = () => {
3809
5999
  return null;
3810
6000
  };
3811
6001
  var loadDevToken = (root) => {
3812
- const path = join15(root, LECODES_DIR, "dev.json");
6002
+ const path = join17(root, LECODES_DIR, "dev.json");
3813
6003
  try {
3814
- const token2 = JSON.parse(readFileSync12(path, "utf8")).token;
6004
+ const token2 = JSON.parse(readFileSync15(path, "utf8")).token;
3815
6005
  if (token2 && /^[a-z0-9]{8,}$/.test(token2))
3816
6006
  return token2;
3817
6007
  } catch {}
3818
6008
  const token = randomBytes2(6).toString("hex");
3819
- mkdirSync9(join15(root, LECODES_DIR), { recursive: true });
3820
- writeFileSync9(path, JSON.stringify({ token }, null, 2) + `
6009
+ mkdirSync11(join17(root, LECODES_DIR), { recursive: true });
6010
+ writeFileSync12(path, JSON.stringify({ token }, null, 2) + `
3821
6011
  `);
3822
6012
  return token;
3823
6013
  };
3824
6014
  var printQr = async (url) => {
3825
6015
  try {
3826
6016
  const qrcode = (await Promise.resolve().then(() => __toESM(require_main(), 1))).default;
3827
- await new Promise((resolve3) => qrcode.generate(url, { small: true }, (out) => {
6017
+ await new Promise((resolve5) => qrcode.generate(url, { small: true }, (out) => {
3828
6018
  log(out);
3829
- resolve3();
6019
+ resolve5();
3830
6020
  }));
3831
6021
  } catch {
3832
6022
  note("(couldn't render a QR code — open the URL below on the device)");
@@ -3842,7 +6032,7 @@ var dev = async (args) => {
3842
6032
  root = process.cwd();
3843
6033
  manifest = null;
3844
6034
  }
3845
- const name = manifest?.name ?? basename4(root);
6035
+ const name = manifest?.name ?? basename7(root);
3846
6036
  if (typeof globalThis.Bun !== "undefined") {
3847
6037
  warnErr("Running under Bun: the websocket reload channel doesn't work here — run `lecodes dev` with Node (or use ?t=poll on the device URL).");
3848
6038
  }
@@ -3929,15 +6119,15 @@ ${result.error}`);
3929
6119
  await printQr(server.url);
3930
6120
  log(`Scan the QR with the LeCodes app, or open: ${c.bold(server.url)}`);
3931
6121
  note(`Older desktop builds (no websocket bridge): ${server.url}?t=poll`);
3932
- if (existsSync13(join15(root, LECODES_DIR, "manifest.json")))
6122
+ if (existsSync15(join17(root, LECODES_DIR, "manifest.json")))
3933
6123
  note(`Token stored in ${LECODES_DIR}/dev.json (delete it to rotate the URL).`);
3934
6124
  log("");
3935
6125
  log(`Watching ${root} — save a file to hot-reload. Ctrl+C to stop.`);
3936
6126
  };
3937
6127
 
3938
6128
  // src/serverDiff.ts
3939
- import { readFileSync as readFileSync13 } from "node:fs";
3940
- import { basename as basename5 } from "node:path";
6129
+ import { readFileSync as readFileSync16 } from "node:fs";
6130
+ import { basename as basename8 } from "node:path";
3941
6131
  var isTextAsset = (t) => t === "text" || t === "shader" || t === "scene";
3942
6132
  var computeServerDiff = (project, scan) => {
3943
6133
  const pathById = buildPaths(project.assets);
@@ -3947,15 +6137,15 @@ var computeServerDiff = (project, scan) => {
3947
6137
  continue;
3948
6138
  serverByPath.set(pathById.get(asset.id), asset);
3949
6139
  }
3950
- const localByPath = new Map(scan.files.map((f) => [f.path, f]));
6140
+ const localByPath = new Map(scan.files.map((f2) => [f2.path, f2]));
3951
6141
  const entries = [];
3952
6142
  const paths = new Set([...serverByPath.keys(), ...localByPath.keys()]);
3953
6143
  for (const path of [...paths].sort()) {
3954
6144
  const server = serverByPath.get(path);
3955
6145
  const local = localByPath.get(path);
3956
6146
  if (local && !server) {
3957
- const text = isTextKind(classifyKind(basename5(path)));
3958
- entries.push({ path, status: "added", binary: !text, serverText: "", localText: text ? readFileSync13(local.absPath, "utf8") : undefined });
6147
+ const text = isTextKind(classifyKind(basename8(path)));
6148
+ entries.push({ path, status: "added", binary: !text, serverText: "", localText: text ? readFileSync16(local.absPath, "utf8") : undefined });
3959
6149
  continue;
3960
6150
  }
3961
6151
  if (server && !local) {
@@ -3966,12 +6156,12 @@ var computeServerDiff = (project, scan) => {
3966
6156
  if (server && local) {
3967
6157
  if (isTextAsset(server.type)) {
3968
6158
  const serverText = server.text ?? "";
3969
- const localText = readFileSync13(local.absPath, "utf8");
6159
+ const localText = readFileSync16(local.absPath, "utf8");
3970
6160
  if (serverText !== localText)
3971
6161
  entries.push({ path, status: "modified", binary: false, serverText, localText });
3972
6162
  } else {
3973
6163
  const serverSize = server.file?.size;
3974
- const localSize = Buffer.byteLength(readFileSync13(local.absPath));
6164
+ const localSize = Buffer.byteLength(readFileSync16(local.absPath));
3975
6165
  if (serverSize !== undefined && serverSize !== localSize) {
3976
6166
  entries.push({ path, status: "modified", binary: true });
3977
6167
  }
@@ -4124,10 +6314,10 @@ var diff = async (args) => {
4124
6314
  var statusLabel = (e) => e.status === "added" ? c.green(" added ") : e.status === "removed" ? c.red(" removed ") : c.yellow(" modified ");
4125
6315
 
4126
6316
  // src/commands/init.ts
4127
- import { existsSync as existsSync14, mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "node:fs";
4128
- import { basename as basename6, dirname as dirname8, join as join16, resolve as resolve3 } from "node:path";
6317
+ import { existsSync as existsSync16, mkdirSync as mkdirSync12, writeFileSync as writeFileSync13 } from "node:fs";
6318
+ import { basename as basename9, dirname as dirname10, join as join18, resolve as resolve5 } from "node:path";
4129
6319
  var STARTER2 = `// Welcome to LeCodes — the whole SDK is available as globals, no imports needed.
4130
- // Run it live on a device with \`lecodes dev\`, or bundle it with \`lecodes preview -o app.js\`.
6320
+ // Run it live on a device with \`lecodes dev\`, or bundle it with \`lecodes compile -o app.js\`.
4131
6321
 
4132
6322
  let count = 0
4133
6323
 
@@ -4161,24 +6351,55 @@ const screen = UIScreen(
4161
6351
 
4162
6352
  screen.open()
4163
6353
  `;
6354
+ var readmeTemplate = (name) => `# ${name}
6355
+
6356
+ A LeCodes app. The whole SDK is available as globals (\`UIScreen\`, \`UIText\`, \`Scene2D\`, …) —
6357
+ no imports needed. \`main.ts\` is the entry point.
6358
+
6359
+ ## Run it
6360
+
6361
+ \`\`\`
6362
+ lecodes dev # live on your phone: scan the QR with the LeCodes app, hot reload
6363
+ lecodes compile -o app.js # bundle to a single self-contained JS file
6364
+ \`\`\`
6365
+
6366
+ ## Go native
6367
+
6368
+ \`\`\`
6369
+ lecodes app init ios # generate a native Xcode shell in ios/
6370
+ lecodes app sync # embed the compiled bundle + plugins into it
6371
+ lecodes app open # open it in Xcode, build & run
6372
+ \`\`\`
6373
+
6374
+ ## Layout
6375
+
6376
+ - \`main.ts\` — the app. Add more top-level .ts files freely; the largest export-free one
6377
+ is picked as the entry point.
6378
+ - \`assets/\` — images/fonts the app references (created on demand)
6379
+ - \`tsconfig.json\` + \`.lecodes/types/\` — generated IDE types (rewritten by the CLI; don't edit)
6380
+ - \`.lecodesignore\` — files a future \`lecodes push\` would skip
6381
+
6382
+ Full command list: \`lecodes --help\`.
6383
+ `;
4164
6384
  var init2 = async (args) => {
4165
6385
  const dirArg = (args._[0] ?? ".").replace(/[/\\]+$/, "") || ".";
4166
- const root = resolve3(dirArg);
4167
- const name = basename6(root);
4168
- mkdirSync10(root, { recursive: true });
6386
+ const root = resolve5(dirArg);
6387
+ const name = basename9(root);
6388
+ mkdirSync12(root, { recursive: true });
4169
6389
  let created = 0;
4170
6390
  const write = (rel, content) => {
4171
- const abs = join16(root, rel);
4172
- if (existsSync14(abs)) {
6391
+ const abs = join18(root, rel);
6392
+ if (existsSync16(abs)) {
4173
6393
  note(`kept ${rel}`);
4174
6394
  return;
4175
6395
  }
4176
- mkdirSync10(dirname8(abs), { recursive: true });
4177
- writeFileSync10(abs, content);
6396
+ mkdirSync12(dirname10(abs), { recursive: true });
6397
+ writeFileSync13(abs, content);
4178
6398
  log(`${c.green("created")} ${rel}`);
4179
6399
  created++;
4180
6400
  };
4181
6401
  write("main.ts", STARTER2);
6402
+ write("README.md", readmeTemplate(name));
4182
6403
  write(".lecodesignore", DEFAULT_IGNORE_FILE);
4183
6404
  const wroteTypes = materializeTypesLocal(root);
4184
6405
  writeTsconfig(root);
@@ -4190,7 +6411,7 @@ var init2 = async (args) => {
4190
6411
  success(created ? `Local project ${c.bold(name)} ready${dirArg === "." ? "" : ` in ${c.bold(dirArg)}/`}.` : `Project ${c.bold(name)} already initialized.`);
4191
6412
  const cd = dirArg === "." ? "" : `cd ${dirArg} && `;
4192
6413
  log(` ${c.bold(cd + "lecodes dev")} run it on a device with hot reload`);
4193
- log(` ${c.bold(cd + "lecodes preview -o app.js")} bundle it for LeCodes Desktop`);
6414
+ log(` ${c.bold(cd + "lecodes compile -o app.js")} bundle it for LeCodes Desktop`);
4194
6415
  };
4195
6416
 
4196
6417
  // src/commands/login.ts
@@ -4204,115 +6425,21 @@ var login = async (args) => {
4204
6425
  success(`Logged in as ${c.bold(account.email)} on ${apiUrl}`);
4205
6426
  };
4206
6427
 
4207
- // src/commands/preview.ts
4208
- import { writeFileSync as writeFileSync11 } from "node:fs";
4209
- import { basename as basename8, resolve as resolve4 } from "node:path";
4210
-
4211
- // src/compile/collect.ts
4212
- import { readFileSync as readFileSync14 } from "node:fs";
4213
- import { basename as basename7 } from "node:path";
4214
- var collectEntries = (root, manifest, project) => {
4215
- const pathById = buildPaths(project.assets);
4216
- const serverByPath = new Map;
4217
- for (const asset of project.assets) {
4218
- if (asset.type === "folder")
4219
- continue;
4220
- serverByPath.set(pathById.get(asset.id), { fileSrc: asset.file?.src ?? undefined, shaders: asset.shaders ?? [] });
4221
- }
4222
- const entries = [];
4223
- const warnings = [];
4224
- for (const file of scanLocal(root).files) {
4225
- const type = manifest.files[file.path]?.type ?? classifyKind(basename7(file.path));
4226
- if (type === "resource") {
4227
- const src = serverByPath.get(file.path)?.fileSrc;
4228
- if (!src)
4229
- warnings.push(`No server URL for ${file.path} — push it so its URL resolves.`);
4230
- entries.push({ path: file.path, type, fileSrc: src, absPath: file.absPath });
4231
- } else if (type === "shader") {
4232
- const shaders = serverByPath.get(file.path)?.shaders;
4233
- if (!shaders?.length)
4234
- warnings.push(`No compiled shader for ${file.path} — push it to compile the shader.`);
4235
- entries.push({ path: file.path, type, shaders });
4236
- } else {
4237
- entries.push({ path: file.path, type, text: readFileSync14(file.absPath, "utf8") });
4238
- }
4239
- }
4240
- return { entries, warnings };
4241
- };
4242
-
4243
- // src/commands/preview.ts
4244
- var preview = async (args) => {
4245
- let root;
4246
- let manifest;
4247
- try {
4248
- root = findProjectRoot(process.cwd());
4249
- manifest = readManifest(root);
4250
- } catch {
4251
- root = process.cwd();
4252
- manifest = null;
4253
- }
4254
- let entries;
4255
- let warnings;
4256
- let name;
4257
- let publicUrl;
4258
- let localAssets = false;
4259
- if (manifest) {
4260
- const config = loadConfig();
4261
- const apiUrl = requireApiUrl(config);
4262
- const token = requireToken(config);
4263
- note("Resolving project assets…");
4264
- const project = await getProject(apiUrl, token, manifest.uuid);
4265
- ({ entries, warnings } = collectEntries(root, manifest, project));
4266
- name = manifest.name;
4267
- publicUrl = flagStr(args, "public-url") ?? apiUrl;
4268
- } else {
4269
- const publicUrlFlag = flagStr(args, "public-url");
4270
- localAssets = publicUrlFlag === undefined;
4271
- publicUrl = publicUrlFlag ?? "";
4272
- ({ entries, warnings } = collectLocalEntries(root, null, publicUrl));
4273
- name = basename8(root);
4274
- }
4275
- note("Compiling…");
4276
- const js = await compileProject2({
4277
- entries,
4278
- name,
4279
- publicUrl,
4280
- entryOverride: flagStr(args, "entry"),
4281
- header: !flagBool(args, "no-header"),
4282
- localAssets
4283
- });
4284
- for (const w of warnings)
4285
- warnErr(w);
4286
- const out = flagStr(args, "o", "out");
4287
- if (out && isNullSink(out)) {
4288
- note(`Discarded output (${out})`);
4289
- } else if (out) {
4290
- writeFileSync11(resolve4(out), js);
4291
- note(`Wrote ${out} (${js.length} bytes)`);
4292
- } else {
4293
- process.stdout.write(js);
4294
- if (!js.endsWith(`
4295
- `))
4296
- process.stdout.write(`
4297
- `);
4298
- }
4299
- };
4300
-
4301
6428
  // src/commands/render.ts
4302
- import { writeFileSync as writeFileSync12 } from "node:fs";
4303
- import { resolve as resolve5 } from "node:path";
6429
+ import { writeFileSync as writeFileSync14 } from "node:fs";
6430
+ import { resolve as resolve6 } from "node:path";
4304
6431
  var LOCAL_ASSET_SCHEME2 = "lecodesfile://";
4305
6432
  var DEVICE_SAFE_AREA = {
4306
- none: [0, 0, 0, 0],
4307
- iphone: [47, 0, 34, 0],
4308
- android: [24, 0, 24, 0]
6433
+ none: { insets: [0, 0, 0, 0], bars: [] },
6434
+ iphone: { insets: [59, 0, 34, 0], bars: [] },
6435
+ android: { insets: [24, 0, 48, 0], bars: ["top", "bottom"] }
4309
6436
  };
4310
6437
  var resolveSafeArea = (args) => {
4311
6438
  const explicit = flagStr(args, "safe-area");
4312
6439
  if (explicit) {
4313
6440
  const n = explicit.split(",").map((s) => Number(s.trim()));
4314
6441
  if (n.length === 4 && n.every((v) => Number.isFinite(v)))
4315
- return n;
6442
+ return { safeArea: n, safeAreaBars: [] };
4316
6443
  throw new CliError(`--safe-area expects "top,right,bottom,left" (4 numbers), got "${explicit}".`);
4317
6444
  }
4318
6445
  const device = flagStr(args, "device");
@@ -4320,7 +6447,7 @@ var resolveSafeArea = (args) => {
4320
6447
  const preset = DEVICE_SAFE_AREA[device];
4321
6448
  if (!preset)
4322
6449
  throw new CliError(`Unknown --device "${device}". Options: ${Object.keys(DEVICE_SAFE_AREA).join(", ")}.`);
4323
- return preset;
6450
+ return { safeArea: preset.insets, safeAreaBars: preset.bars };
4324
6451
  }
4325
6452
  return;
4326
6453
  };
@@ -4343,6 +6470,13 @@ var render = async (args) => {
4343
6470
  const useLocalAssets = !publicUrlFlag && !flagBool(args, "remote-assets");
4344
6471
  const compileEntries = useLocalAssets ? entries.map((e) => e.type === "resource" && e.absPath ? { ...e, fileSrc: encodeURIComponent(e.absPath) } : e) : entries;
4345
6472
  const warnings = useLocalAssets ? collectWarnings.filter((w) => !w.includes("No server URL")) : collectWarnings;
6473
+ await applyProjectFonts(compileEntries, useLocalAssets ? {
6474
+ mode: "vendor",
6475
+ root,
6476
+ vendorDir: root,
6477
+ vendorPrefix: "",
6478
+ makeResourceEntry: (path, absPath) => ({ path, type: "resource", fileSrc: encodeURIComponent(absPath), absPath })
6479
+ } : { mode: "cdn", root });
4346
6480
  note("Compiling…");
4347
6481
  const js = await compileProject2({
4348
6482
  entries: compileEntries,
@@ -4375,7 +6509,7 @@ var render = async (args) => {
4375
6509
  localAssets: useLocalAssets,
4376
6510
  timeMs: timeStr !== undefined ? Number(timeStr) : undefined,
4377
6511
  afterAnimations: !flagBool(args, "no-after-animations"),
4378
- safeArea: resolveSafeArea(args),
6512
+ ...resolveSafeArea(args),
4379
6513
  onConsole
4380
6514
  };
4381
6515
  const done = (flush) => flush(() => {
@@ -4396,7 +6530,7 @@ var render = async (args) => {
4396
6530
  if (isNullSink(pngFlag)) {
4397
6531
  note(`Discarded PNG (${pngFlag})`);
4398
6532
  } else {
4399
- writeFileSync12(resolve5(pngFlag), png);
6533
+ writeFileSync14(resolve6(pngFlag), png);
4400
6534
  note(`Wrote ${pngFlag} (${png.length} bytes, ${width}x${height})`);
4401
6535
  }
4402
6536
  return done((cb) => cb());
@@ -4411,7 +6545,7 @@ var render = async (args) => {
4411
6545
  return done((cb) => cb());
4412
6546
  }
4413
6547
  if (out) {
4414
- writeFileSync12(resolve5(out), result.text);
6548
+ writeFileSync14(resolve6(out), result.text);
4415
6549
  note(`Wrote ${out} (${result.text.length} bytes)`);
4416
6550
  return done((cb) => cb());
4417
6551
  }
@@ -4422,8 +6556,8 @@ var render = async (args) => {
4422
6556
  };
4423
6557
 
4424
6558
  // src/commands/pull.ts
4425
- import { existsSync as existsSync15, mkdirSync as mkdirSync11, rmSync, writeFileSync as writeFileSync13 } from "node:fs";
4426
- import { dirname as dirname9, join as join17 } from "node:path";
6559
+ import { existsSync as existsSync17, mkdirSync as mkdirSync13, rmSync as rmSync2, writeFileSync as writeFileSync15 } from "node:fs";
6560
+ import { dirname as dirname11, join as join19 } from "node:path";
4427
6561
  var pull = async (args) => {
4428
6562
  const root = findProjectRoot(process.cwd());
4429
6563
  const manifest = readManifest(root);
@@ -4443,9 +6577,9 @@ var pull = async (args) => {
4443
6577
  const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a) => a.id === id)?.type !== "folder").map(([, path]) => path));
4444
6578
  for (const path of Object.keys(manifest.files)) {
4445
6579
  if (!remotePaths.has(path)) {
4446
- const abs = join17(root, ...path.replace(/^\//, "").split("/"));
4447
- if (existsSync15(abs))
4448
- rmSync(abs);
6580
+ const abs = join19(root, ...path.replace(/^\//, "").split("/"));
6581
+ if (existsSync17(abs))
6582
+ rmSync2(abs);
4449
6583
  }
4450
6584
  }
4451
6585
  const files = await materializeAssets(root, project, apiUrl, token, progress);
@@ -4482,18 +6616,18 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
4482
6616
  const files = { ...manifest.files };
4483
6617
  for (const asset of targets) {
4484
6618
  const key = pathById.get(asset.id);
4485
- const abs = join17(root, ...key.replace(/^\//, "").split("/"));
4486
- mkdirSync11(dirname9(abs), { recursive: true });
6619
+ const abs = join19(root, ...key.replace(/^\//, "").split("/"));
6620
+ mkdirSync13(dirname11(abs), { recursive: true });
4487
6621
  let sha;
4488
6622
  if (asset.type === "file") {
4489
6623
  if (!asset.file?.src)
4490
6624
  continue;
4491
6625
  const bytes = await downloadBlob(apiUrl, asset.file.src);
4492
- writeFileSync13(abs, bytes);
6626
+ writeFileSync15(abs, bytes);
4493
6627
  sha = sha256(bytes);
4494
6628
  } else {
4495
6629
  const text = asset.text ?? "";
4496
- writeFileSync13(abs, text);
6630
+ writeFileSync15(abs, text);
4497
6631
  sha = sha256(text);
4498
6632
  }
4499
6633
  files[key] = { id: asset.id, type: kindOf2(asset.type), sha };
@@ -4505,8 +6639,8 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
4505
6639
  };
4506
6640
 
4507
6641
  // src/commands/push.ts
4508
- import { basename as basename9 } from "node:path";
4509
- import { readFileSync as readFileSync15 } from "node:fs";
6642
+ import { basename as basename10 } from "node:path";
6643
+ import { readFileSync as readFileSync17 } from "node:fs";
4510
6644
  var push = async (args) => {
4511
6645
  const root = findProjectRoot(process.cwd());
4512
6646
  const manifest = readManifest(root);
@@ -4560,14 +6694,14 @@ var push = async (args) => {
4560
6694
  let uploaded = 0;
4561
6695
  for (const file of scan.files) {
4562
6696
  const source = sourceFor(file.path);
4563
- const kind = source?.type ?? classifyKind(basename9(file.path));
6697
+ const kind = source?.type ?? classifyKind(basename10(file.path));
4564
6698
  const entry = { path: file.path, type: kind };
4565
6699
  if (source)
4566
6700
  entry.assetId = source.id;
4567
6701
  if (isTextKind(kind)) {
4568
- entry.text = readFileSync15(file.absPath, "utf8");
6702
+ entry.text = readFileSync17(file.absPath, "utf8");
4569
6703
  } else if (!source || source.sha !== file.sha) {
4570
- const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename9(file.path), readFileSync15(file.absPath));
6704
+ const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename10(file.path), readFileSync17(file.absPath));
4571
6705
  entry.blobFileId = blob.id;
4572
6706
  uploaded++;
4573
6707
  }
@@ -4583,7 +6717,7 @@ var push = async (args) => {
4583
6717
  reportPublish(res);
4584
6718
  return;
4585
6719
  }
4586
- const shaByPath = new Map(scan.files.map((f) => [f.path, f.sha]));
6720
+ const shaByPath = new Map(scan.files.map((f2) => [f2.path, f2.sha]));
4587
6721
  const newFiles = {};
4588
6722
  for (const node of res.nodes) {
4589
6723
  newFiles[node.path] = { id: node.id, type: node.type, sha: shaByPath.get(node.path) ?? "" };
@@ -4620,22 +6754,22 @@ var reportPublish = (res) => {
4620
6754
  };
4621
6755
 
4622
6756
  // src/commands/scene.ts
4623
- import { existsSync as existsSync17, readFileSync as readFileSync17, readdirSync as readdirSync7, statSync as statSync3, writeFileSync as writeFileSync14 } from "node:fs";
4624
- import { basename as basename10, dirname as dirname10, join as join19, resolve as resolve6 } from "node:path";
6757
+ import { existsSync as existsSync19, readFileSync as readFileSync19, readdirSync as readdirSync8, statSync as statSync4, writeFileSync as writeFileSync16 } from "node:fs";
6758
+ import { basename as basename11, dirname as dirname12, join as join21, resolve as resolve7 } from "node:path";
4625
6759
 
4626
6760
  // src/compile/sceneCompile.ts
4627
- import { existsSync as existsSync16, readFileSync as readFileSync16 } from "node:fs";
4628
- import { join as join18 } from "node:path";
6761
+ import { existsSync as existsSync18, readFileSync as readFileSync18 } from "node:fs";
6762
+ import { join as join20 } from "node:path";
4629
6763
  var TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i;
4630
6764
  var rel = (key) => key.replace(/^\//, "");
4631
6765
  var resolveHarness = (loadSceneHarness2) => {
4632
- const inRepo = join18(distRoot, "..", "projects", "src", "scene-editor");
6766
+ const inRepo = join20(distRoot, "..", "projects", "src", "scene-editor");
4633
6767
  try {
4634
- return loadSceneHarness2(existsSync16(inRepo) ? inRepo : undefined);
6768
+ return loadSceneHarness2(existsSync18(inRepo) ? inRepo : undefined);
4635
6769
  } catch {
4636
- const vendored = join18(distRoot, "runtime", "scene-harness.json");
4637
- if (existsSync16(vendored))
4638
- return JSON.parse(readFileSync16(vendored, "utf8"));
6770
+ const vendored = join20(distRoot, "runtime", "scene-harness.json");
6771
+ if (existsSync18(vendored))
6772
+ return JSON.parse(readFileSync18(vendored, "utf8"));
4639
6773
  throw new CliError("scene-editor harness sources not found — reinstall the CLI or set LECODES_SCENE_HARNESS_DIR.");
4640
6774
  }
4641
6775
  };
@@ -4647,7 +6781,7 @@ var compileSceneEditor = async (opts) => {
4647
6781
  for (const file of scan.files) {
4648
6782
  const path = "./" + rel(file.path);
4649
6783
  if (TEXT_EXT.test(path)) {
4650
- entries.push({ path, type: "text", text: readFileSync16(file.absPath, "utf8") });
6784
+ entries.push({ path, type: "text", text: readFileSync18(file.absPath, "utf8") });
4651
6785
  } else {
4652
6786
  entries.push({ path, type: "resource", fileSrc: "/project/" + rel(file.path) });
4653
6787
  }
@@ -4687,24 +6821,24 @@ var getContext2 = (args) => {
4687
6821
  manifest = null;
4688
6822
  const pathArg = args._[0];
4689
6823
  if (pathArg) {
4690
- const abs = resolve6(process.cwd(), pathArg);
4691
- if (existsSync17(abs) && statSync3(abs).isDirectory())
6824
+ const abs = resolve7(process.cwd(), pathArg);
6825
+ if (existsSync19(abs) && statSync4(abs).isDirectory())
4692
6826
  root = abs;
4693
- else if (abs.endsWith(".scene.ts") && existsSync17(abs))
4694
- root = dirname10(abs);
6827
+ else if (abs.endsWith(".scene.ts") && existsSync19(abs))
6828
+ root = dirname12(abs);
4695
6829
  else
4696
6830
  throw new CliError(`No such folder or .scene.ts: ${pathArg}`);
4697
6831
  }
4698
6832
  }
4699
- return { root, manifest, name: manifest?.name ?? basename10(root) };
6833
+ return { root, manifest, name: manifest?.name ?? basename11(root) };
4700
6834
  };
4701
6835
  var writeMcpConfig2 = (root, port) => {
4702
- const path = join19(root, ".mcp.json");
6836
+ const path = join21(root, ".mcp.json");
4703
6837
  const entry = { type: "http", url: `http://localhost:${port}/mcp` };
4704
6838
  let config = {};
4705
- if (existsSync17(path)) {
6839
+ if (existsSync19(path)) {
4706
6840
  try {
4707
- config = JSON.parse(readFileSync17(path, "utf8"));
6841
+ config = JSON.parse(readFileSync19(path, "utf8"));
4708
6842
  } catch {
4709
6843
  warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-scene server by hand.");
4710
6844
  return;
@@ -4717,17 +6851,17 @@ var writeMcpConfig2 = (root, port) => {
4717
6851
  return;
4718
6852
  }
4719
6853
  config.mcpServers["lecodes-scene"] = entry;
4720
- writeFileSync14(path, JSON.stringify(config, null, 2) + `
6854
+ writeFileSync16(path, JSON.stringify(config, null, 2) + `
4721
6855
  `);
4722
6856
  note(`${existing ? "updated" : "created"} .mcp.json (lecodes-scene MCP → ${entry.url})`);
4723
6857
  };
4724
6858
  var countScenes = (dir) => {
4725
6859
  let n = 0;
4726
- for (const name of readdirSync7(dir)) {
6860
+ for (const name of readdirSync8(dir)) {
4727
6861
  if (name === "node_modules" || name.startsWith("."))
4728
6862
  continue;
4729
- const full = join19(dir, name);
4730
- if (statSync3(full).isDirectory())
6863
+ const full = join21(dir, name);
6864
+ if (statSync4(full).isDirectory())
4731
6865
  n += countScenes(full);
4732
6866
  else if (name.endsWith(".scene.ts"))
4733
6867
  n++;
@@ -4779,12 +6913,12 @@ var status = async (_args) => {
4779
6913
  info(c.dim("Working tree clean — nothing to push.") + ignoredNote);
4780
6914
  return;
4781
6915
  }
4782
- for (const f of diff2.added) {
4783
- if (!diff2.moves.has(f.path))
4784
- log(c.green(` added ${f.path}`));
6916
+ for (const f2 of diff2.added) {
6917
+ if (!diff2.moves.has(f2.path))
6918
+ log(c.green(` added ${f2.path}`));
4785
6919
  }
4786
- for (const f of diff2.modified)
4787
- log(c.yellow(` modified ${f.path}`));
6920
+ for (const f2 of diff2.modified)
6921
+ log(c.yellow(` modified ${f2.path}`));
4788
6922
  for (const [to, from] of diff2.moves)
4789
6923
  log(c.cyan(` moved ${from} → ${to}`));
4790
6924
  const movedFrom = new Set(diff2.moves.values());
@@ -4797,21 +6931,22 @@ var status = async (_args) => {
4797
6931
  };
4798
6932
 
4799
6933
  // src/index.ts
4800
- import { existsSync as existsSync19, realpathSync as realpathSync2 } from "node:fs";
4801
- import { delimiter as delimiter2, dirname as dirname12, join as join21 } from "node:path";
6934
+ import { existsSync as existsSync21, readFileSync as readFileSync21, realpathSync as realpathSync2 } from "node:fs";
6935
+ import { createRequire as createRequire4 } from "node:module";
6936
+ import { delimiter as delimiter2, dirname as dirname14, join as join23 } from "node:path";
4802
6937
 
4803
6938
  // src/commands/install.ts
4804
- import { execFileSync } from "node:child_process";
4805
- import { appendFileSync as appendFileSync2, chmodSync, cpSync, existsSync as existsSync18, mkdirSync as mkdirSync12, readFileSync as readFileSync18, realpathSync, rmSync as rmSync2, symlinkSync } from "node:fs";
4806
- import { homedir as homedir2 } from "node:os";
4807
- import { basename as basename11, delimiter, dirname as dirname11, join as join20, resolve as resolve7, sep } from "node:path";
6939
+ import { execFileSync as execFileSync2 } from "node:child_process";
6940
+ import { appendFileSync as appendFileSync2, chmodSync, cpSync as cpSync2, existsSync as existsSync20, mkdirSync as mkdirSync14, readFileSync as readFileSync20, realpathSync, rmSync as rmSync3, symlinkSync } from "node:fs";
6941
+ import { homedir as homedir3 } from "node:os";
6942
+ import { basename as basename12, delimiter, dirname as dirname13, join as join22, resolve as resolve8, sep } from "node:path";
4808
6943
  var isWin = process.platform === "win32";
4809
- var HOME = homedir2();
6944
+ var HOME = homedir3();
4810
6945
  var safeRealpath = (p) => {
4811
6946
  try {
4812
6947
  return realpathSync(p);
4813
6948
  } catch {
4814
- return resolve7(p);
6949
+ return resolve8(p);
4815
6950
  }
4816
6951
  };
4817
6952
  var onPath = (dir) => {
@@ -4827,25 +6962,25 @@ var onPath = (dir) => {
4827
6962
  });
4828
6963
  };
4829
6964
  var install = async (args) => {
4830
- const bundle = dirname11(process.execPath);
4831
- const exeName = basename11(process.execPath);
6965
+ const bundle = dirname13(process.execPath);
6966
+ const exeName = basename12(process.execPath);
4832
6967
  const chiselName = isWin ? "chisel.exe" : "chisel";
4833
- if (!existsSync18(join20(bundle, "runtime")) || !existsSync18(join20(bundle, chiselName))) {
6968
+ if (!existsSync20(join22(bundle, "runtime")) || !existsSync20(join22(bundle, chiselName))) {
4834
6969
  throw new CliError("`lecodes install` only works from the packaged release build (the archive containing lecodes + " + "chisel + runtime/). You appear to be running from source or an npm install — install globally " + "with your package manager there instead.");
4835
6970
  }
4836
- const companions = [exeName, chiselName, "runtime", "README.txt"].filter((f) => existsSync18(join20(bundle, f)));
6971
+ const companions = [exeName, chiselName, "runtime", "README.txt"].filter((f2) => existsSync20(join22(bundle, f2)));
4837
6972
  if (isWin)
4838
6973
  return installWindows(args, bundle, companions);
4839
6974
  return installUnix(args, bundle, companions);
4840
6975
  };
4841
6976
  var copyBundle = (bundle, target, companions) => {
4842
- mkdirSync12(target, { recursive: true });
6977
+ mkdirSync14(target, { recursive: true });
4843
6978
  if (safeRealpath(target) === safeRealpath(bundle))
4844
6979
  return;
4845
- for (const f of companions) {
4846
- const dest = join20(target, f);
4847
- rmSync2(dest, { recursive: true, force: true });
4848
- cpSync(join20(bundle, f), dest, { recursive: true });
6980
+ for (const f2 of companions) {
6981
+ const dest = join22(target, f2);
6982
+ rmSync3(dest, { recursive: true, force: true });
6983
+ cpSync2(join22(bundle, f2), dest, { recursive: true });
4849
6984
  }
4850
6985
  };
4851
6986
  var permError = (e, dir, system) => {
@@ -4857,41 +6992,41 @@ var permError = (e, dir, system) => {
4857
6992
  };
4858
6993
  var installUnix = (args, bundle, companions) => {
4859
6994
  const system = flagBool(args, "system");
4860
- const libDir = resolve7(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join20(HOME, ".local", "lib", "lecodes-cli")));
4861
- const binDir = resolve7(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join20(HOME, ".local", "bin")));
4862
- const link = join20(binDir, "lecodes");
6995
+ const libDir = resolve8(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join22(HOME, ".local", "lib", "lecodes-cli")));
6996
+ const binDir = resolve8(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join22(HOME, ".local", "bin")));
6997
+ const link = join22(binDir, "lecodes");
4863
6998
  try {
4864
6999
  copyBundle(bundle, libDir, companions);
4865
- chmodSync(join20(libDir, "lecodes"), 493);
4866
- chmodSync(join20(libDir, "chisel"), 493);
7000
+ chmodSync(join22(libDir, "lecodes"), 493);
7001
+ chmodSync(join22(libDir, "chisel"), 493);
4867
7002
  } catch (e) {
4868
7003
  throw permError(e, libDir, system);
4869
7004
  }
4870
7005
  try {
4871
- mkdirSync12(binDir, { recursive: true });
4872
- rmSync2(link, { force: true });
4873
- symlinkSync(join20(libDir, "lecodes"), link);
7006
+ mkdirSync14(binDir, { recursive: true });
7007
+ rmSync3(link, { force: true });
7008
+ symlinkSync(join22(libDir, "lecodes"), link);
4874
7009
  } catch (e) {
4875
7010
  throw permError(e, binDir, system);
4876
7011
  }
4877
7012
  success(`Installed to ${c.bold(libDir)}`);
4878
- log(` ${c.dim(link + " → " + join20(libDir, "lecodes"))}`);
7013
+ log(` ${c.dim(link + " → " + join22(libDir, "lecodes"))}`);
4879
7014
  finishPathUnix(args, binDir);
4880
7015
  };
4881
7016
  var shellRc = () => {
4882
- const shell = basename11(process.env.SHELL ?? "");
7017
+ const shell = basename12(process.env.SHELL ?? "");
4883
7018
  if (shell === "zsh")
4884
- return { file: join20(HOME, ".zshrc"), name: "~/.zshrc" };
7019
+ return { file: join22(HOME, ".zshrc"), name: "~/.zshrc" };
4885
7020
  if (shell === "bash")
4886
- return { file: join20(HOME, ".bashrc"), name: "~/.bashrc" };
4887
- return { file: join20(HOME, ".profile"), name: "~/.profile" };
7021
+ return { file: join22(HOME, ".bashrc"), name: "~/.bashrc" };
7022
+ return { file: join22(HOME, ".profile"), name: "~/.profile" };
4888
7023
  };
4889
7024
  var PATH_MARKER = "# Added by `lecodes install`";
4890
7025
  var appendPathToShellRc = (binDir) => {
4891
7026
  const { file, name } = shellRc();
4892
7027
  const line = `export PATH="${binDir}:$PATH"`;
4893
7028
  try {
4894
- const existing = existsSync18(file) ? readFileSync18(file, "utf8") : "";
7029
+ const existing = existsSync20(file) ? readFileSync20(file, "utf8") : "";
4895
7030
  if (existing.includes(line))
4896
7031
  return name;
4897
7032
  appendFileSync2(file, `${existing && !existing.endsWith(`
@@ -4929,7 +7064,7 @@ var finishPathUnix = (args, binDir) => {
4929
7064
  log(` • or install system-wide (no PATH edit): ${c.bold("sudo lecodes install --system")}`);
4930
7065
  };
4931
7066
  var installWindows = (args, bundle, companions) => {
4932
- const target = resolve7(flagStr(args, "dir") ?? join20(process.env.LOCALAPPDATA ?? join20(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
7067
+ const target = resolve8(flagStr(args, "dir") ?? join22(process.env.LOCALAPPDATA ?? join22(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
4933
7068
  try {
4934
7069
  copyBundle(bundle, target, companions);
4935
7070
  } catch (e) {
@@ -4957,7 +7092,7 @@ var installWindows = (args, bundle, companions) => {
4957
7092
  var addToUserPathWindows = (dir) => {
4958
7093
  const script = "$d = $env:LECODES_INSTALL_DIR;" + "$p = [Environment]::GetEnvironmentVariable('Path','User'); if (-not $p) { $p = '' };" + "$parts = @($p.Split(';') | Where-Object { $_ -ne '' });" + "if ($parts -notcontains $d) { [Environment]::SetEnvironmentVariable('Path', (@($parts + $d) -join ';'), 'User') }";
4959
7094
  try {
4960
- execFileSync("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], { stdio: "ignore", env: { ...process.env, LECODES_INSTALL_DIR: dir } });
7095
+ execFileSync2("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], { stdio: "ignore", env: { ...process.env, LECODES_INSTALL_DIR: dir } });
4961
7096
  return true;
4962
7097
  } catch {
4963
7098
  return false;
@@ -4966,9 +7101,9 @@ var addToUserPathWindows = (dir) => {
4966
7101
 
4967
7102
  // src/index.ts
4968
7103
  var STANDALONE = (() => {
4969
- const dir = dirname12(process.execPath);
7104
+ const dir = dirname14(process.execPath);
4970
7105
  const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
4971
- return existsSync19(join21(dir, "runtime")) && existsSync19(join21(dir, chisel));
7106
+ return existsSync21(join23(dir, "runtime")) && existsSync21(join23(dir, chisel));
4972
7107
  })();
4973
7108
  var INSTALLED = STANDALONE && (() => {
4974
7109
  const exe = process.platform === "win32" ? "lecodes.exe" : "lecodes";
@@ -4983,8 +7118,8 @@ var INSTALLED = STANDALONE && (() => {
4983
7118
  if (!d)
4984
7119
  return false;
4985
7120
  try {
4986
- const cand = join21(d, exe);
4987
- return existsSync19(cand) && realpathSync2(cand) === self;
7121
+ const cand = join23(d, exe);
7122
+ return existsSync21(cand) && realpathSync2(cand) === self;
4988
7123
  } catch {
4989
7124
  return false;
4990
7125
  }
@@ -5004,7 +7139,7 @@ ${c.bold("Usage:")} lecodes <command> [options]
5004
7139
  ${c.bold("Commands:")}
5005
7140
  init [dir] Scaffold a new LOCAL project (no login/server): a starter
5006
7141
  main.ts, IDE types + tsconfig, and a .lecodesignore.
5007
- Run it with 'lecodes dev' or 'lecodes preview'.${INSTALL_HELP}
7142
+ Run it with 'lecodes dev' or 'lecodes compile'.${INSTALL_HELP}
5008
7143
  login Authorize in the browser and store an access token
5009
7144
  --token <pat> store a token from the web UI instead
5010
7145
  --api <url> API origin (default https://le.codes)
@@ -5034,10 +7169,13 @@ ${c.bold("Commands:")}
5034
7169
  --host <ip> LAN address to advertise (default: auto-detect)
5035
7170
  --entry <path> override the entrypoint
5036
7171
  --no-qr don't render the QR code
5037
- preview Compile the local project to JS (prints to stdout)
7172
+ compile Compile the local project to JS (prints to stdout)
7173
+ (was 'lecodes preview' — the old name still works)
5038
7174
  -o, --out <file> write to a file instead of stdout
5039
7175
  --entry <path> override the entrypoint
5040
7176
  --no-header omit the metadata header
7177
+ --no-minify keep the bundle readable (no compress/mangle)
7178
+ --no-sourcemap don't generate/inline the source map
5041
7179
  --public-url <url> asset URL prefix (default: the API origin)
5042
7180
  render Render the local UI project to a semantic JSON description
5043
7181
  (needs the optional 'lecodes-renderer' package; UI projects only)
@@ -5068,12 +7206,70 @@ ${c.bold("Commands:")}
5068
7206
  --port <n> editor-server port (default 4488)
5069
7207
  --no-open don't open the browser
5070
7208
  --no-mcp don't register the MCP server in .mcp.json
7209
+ app init ios Generate a native Xcode shell in <project>/ios/ (offline-first:
7210
+ runs the bundled app.js; ios/ is never pushed to LeCodes)
7211
+ --name <s> display name (default: app.json / folder name)
7212
+ --bundle-id <id> iOS bundle identifier
7213
+ --team <id> Apple development team id
7214
+ --variant <v> SDK variant: core (default) or full (3D/AR)
7215
+ app sync Make the shell match the project: compile the bundle + embed
7216
+ its assets into ios/App/Resources/ (offline by default, also
7217
+ for server projects; app.json "resources": {"remote": [globs]}
7218
+ keeps heavy assets streaming from the server), vendor app.json
7219
+ plugins (pinned in plugins.lock.json), regenerate the
7220
+ CLI-owned shell files
7221
+ app plugin <list|add|remove> Manage native plugins in app.json (then run 'lecodes app sync')
7222
+ add takes a known id (camera, qr-scanner) or github:owner/repo[@tag]
7223
+ app open [ios] Open the native shell in its IDE (Xcode; Android Studio later)
7224
+ app doctor Preflight checks for building/uploading the shell
7225
+ app build ios Sync, archive and export the .ipa (automatic signing)
7226
+ --configuration <c> build configuration (default Release)
7227
+ --method <m> export method (default app-store-connect)
7228
+ --no-export stop after the archive
7229
+ --no-sync skip the lecodes app sync step
7230
+ app upload ios Upload the archive to App Store Connect (then bumps buildNumber)
7231
+ --key-path <p8> --key-id <id> --issuer-id <id>
7232
+ ASC API key; stored in ~/.lecodes/asc.json
5071
7233
 
5072
7234
  Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKEN).`;
5073
- var commands = { init: init2, login, create, clone, status, diff, pull, push, preview, render, design, scene, dev };
7235
+ var commands = { init: init2, login, create, clone, status, diff, pull, push, compile: compile2, preview: previewLegacy, render, design, scene, dev, app };
5074
7236
  if (STANDALONE)
5075
7237
  commands.install = install;
5076
7238
  var readVersion = () => package_default.version ?? "unknown";
7239
+ var OPTIONAL_PEERS = [
7240
+ ["lecodes-renderer", "lecodes-renderer/headless"],
7241
+ ["lecodes-design", "lecodes-design/server"],
7242
+ ["lecodes-3d-editor", "lecodes-3d-editor/server"]
7243
+ ];
7244
+ var peerVersion = (name, probe) => {
7245
+ try {
7246
+ const require2 = createRequire4(import.meta.url);
7247
+ let dir = dirname14(require2.resolve(probe));
7248
+ for (let depth = 0;depth < 16; depth++) {
7249
+ const manifest = join23(dir, "package.json");
7250
+ if (existsSync21(manifest)) {
7251
+ const parsed = JSON.parse(readFileSync21(manifest, "utf8"));
7252
+ if (parsed.name === name)
7253
+ return parsed.version ?? "unknown";
7254
+ }
7255
+ const parent = dirname14(dir);
7256
+ if (parent === dir)
7257
+ break;
7258
+ dir = parent;
7259
+ }
7260
+ } catch {}
7261
+ return null;
7262
+ };
7263
+ var printVersion = () => {
7264
+ const rows = [["lecodes-cli", readVersion()]];
7265
+ for (const [name, probe] of OPTIONAL_PEERS) {
7266
+ const version = peerVersion(name, probe);
7267
+ rows.push([name, version ?? c.dim("not installed")]);
7268
+ }
7269
+ const width = Math.max(...rows.map(([label]) => label.length));
7270
+ for (const [label, value] of rows)
7271
+ log(`${label.padEnd(width)} ${value}`);
7272
+ };
5077
7273
  var main = async () => {
5078
7274
  const argv = process.argv.slice(2);
5079
7275
  const command = argv[0];
@@ -5082,7 +7278,7 @@ var main = async () => {
5082
7278
  return;
5083
7279
  }
5084
7280
  if (command === "--version" || command === "-v" || command === "version") {
5085
- log(readVersion());
7281
+ printVersion();
5086
7282
  return;
5087
7283
  }
5088
7284
  const run = commands[command];