lecodes-cli 0.2.0 → 0.5.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 (48) hide show
  1. package/README.md +3 -0
  2. package/dist/index.js +883 -507
  3. package/package.json +9 -4
  4. package/runtime/sdk/canvas/Canvas.ts +270 -0
  5. package/runtime/sdk/compile/aspectMacro.ts +42 -0
  6. package/runtime/sdk/compile/assetMacro.ts +44 -0
  7. package/runtime/sdk/compile/bundler.ts +173 -0
  8. package/runtime/sdk/compile/compileProject.ts +84 -0
  9. package/runtime/sdk/compile/header.ts +46 -0
  10. package/runtime/sdk/compile/index.ts +12 -0
  11. package/runtime/sdk/compile/sourcemap.ts +25 -0
  12. package/runtime/sdk/core/Aspect.ts +82 -26
  13. package/runtime/sdk/core/signals.ts +277 -0
  14. package/runtime/sdk/g2/CharacterController2D.ts +5 -4
  15. package/runtime/sdk/g2/Node2D.ts +1 -1
  16. package/runtime/sdk/g2/Physics2D.ts +22 -12
  17. package/runtime/sdk/g2/Scene2D.ts +30 -0
  18. package/runtime/sdk/g2/{Shape.ts → Shape2D.ts} +10 -10
  19. package/runtime/sdk/g2/Sprite.ts +16 -6
  20. package/runtime/sdk/g2/Texture2D.ts +12 -2
  21. package/runtime/sdk/g2/{Trigger.ts → Trigger2D.ts} +9 -9
  22. package/runtime/sdk/gl/CharacterController.ts +101 -0
  23. package/runtime/sdk/gl/Material.ts +10 -3
  24. package/runtime/sdk/gl/Node.ts +11 -2
  25. package/runtime/sdk/gl/Physics.ts +65 -41
  26. package/runtime/sdk/gl/Scene.ts +18 -0
  27. package/runtime/sdk/gl/Shape.ts +98 -0
  28. package/runtime/sdk/gl/Texture.ts +13 -2
  29. package/runtime/sdk/gl/Trigger.ts +46 -0
  30. package/runtime/sdk/gl/physicsEvents.ts +20 -0
  31. package/runtime/sdk/inject.ts +14 -3
  32. package/runtime/sdk/runtime/camera.ts +31 -0
  33. package/runtime/sdk/runtime/device.ts +26 -0
  34. package/runtime/sdk/runtime/share.ts +9 -0
  35. package/runtime/sdk/ui/UI.ts +5 -1
  36. package/runtime/sdk/ui/UIButton.ts +11 -5
  37. package/runtime/sdk/ui/UIContainer.ts +28 -13
  38. package/runtime/sdk/ui/UIImage.ts +31 -11
  39. package/runtime/sdk/ui/UIInput.ts +10 -0
  40. package/runtime/sdk/ui/UINode.ts +206 -8
  41. package/runtime/sdk/ui/UIScreen.ts +17 -7
  42. package/runtime/sdk/ui/UIScrollable.ts +10 -5
  43. package/runtime/sdk/ui/UISpacer.ts +4 -0
  44. package/runtime/sdk/ui/UIText.ts +21 -5
  45. package/runtime/sdk/ui/UIVideo.ts +5 -0
  46. package/runtime/sdk/ui/UIVirtualizedList.ts +5 -0
  47. package/runtime/sdk/ui/UIWidget.ts +10 -5
  48. package/runtime/sdk/gl/Collider.ts +0 -29
package/dist/index.js CHANGED
@@ -3,8 +3,8 @@ import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // src/index.ts
6
- import { readFileSync as readFileSync9 } from "node:fs";
7
- import { dirname as dirname7, join as join11 } from "node:path";
6
+ import { readFileSync as readFileSync11 } from "node:fs";
7
+ import { dirname as dirname7, join as join12 } from "node:path";
8
8
  import { fileURLToPath as fileURLToPath3 } from "node:url";
9
9
 
10
10
  // src/commands/clone.ts
@@ -483,6 +483,14 @@ var create = async (args) => {
483
483
  info(c.dim(` cd ${dir} — edit main.ts, then \`lecodes push -m "first commit"\` to save it`));
484
484
  };
485
485
 
486
+ // src/commands/design.ts
487
+ import { existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync7, readdirSync as readdirSync5, writeFileSync as writeFileSync7, appendFileSync } from "node:fs";
488
+ import { basename as basename2, join as join10 } from "node:path";
489
+
490
+ // src/compile/designCompile.ts
491
+ import { readFileSync as readFileSync6 } from "node:fs";
492
+ import { basename } from "node:path";
493
+
486
494
  // src/localFiles.ts
487
495
  import { readdirSync as readdirSync3, readFileSync as readFileSync4 } from "node:fs";
488
496
  import { join as join7, relative } from "node:path";
@@ -592,193 +600,352 @@ var diffLocal = (manifest, scan) => {
592
600
  };
593
601
  var hasChanges = (diff) => diff.added.length + diff.modified.length + diff.deleted.length > 0;
594
602
 
595
- // src/serverDiff.ts
596
- import { readFileSync as readFileSync5 } from "node:fs";
597
- import { basename } from "node:path";
598
- var isTextAsset = (t) => t === "text" || t === "shader" || t === "scene";
599
- var computeServerDiff = (project, scan) => {
600
- const pathById = buildPaths(project.assets);
601
- const serverByPath = new Map;
602
- for (const asset of project.assets) {
603
- if (asset.type === "folder")
604
- continue;
605
- serverByPath.set(pathById.get(asset.id), asset);
606
- }
607
- const localByPath = new Map(scan.files.map((f) => [f.path, f]));
608
- const entries = [];
609
- const paths = new Set([...serverByPath.keys(), ...localByPath.keys()]);
610
- for (const path of [...paths].sort()) {
611
- const server = serverByPath.get(path);
612
- const local = localByPath.get(path);
613
- if (local && !server) {
614
- const text = isTextKind(classifyKind(basename(path)));
615
- entries.push({ path, status: "added", binary: !text, serverText: "", localText: text ? readFileSync5(local.absPath, "utf8") : undefined });
616
- continue;
617
- }
618
- if (server && !local) {
619
- const text = isTextAsset(server.type);
620
- entries.push({ path, status: "removed", binary: !text, serverText: text ? server.text ?? "" : undefined, localText: "" });
621
- continue;
622
- }
623
- if (server && local) {
624
- if (isTextAsset(server.type)) {
625
- const serverText = server.text ?? "";
626
- const localText = readFileSync5(local.absPath, "utf8");
627
- if (serverText !== localText)
628
- entries.push({ path, status: "modified", binary: false, serverText, localText });
629
- } else {
630
- const serverSize = server.file?.size;
631
- const localSize = Buffer.byteLength(readFileSync5(local.absPath));
632
- if (serverSize !== undefined && serverSize !== localSize) {
633
- entries.push({ path, status: "modified", binary: true });
634
- }
635
- }
603
+ // src/compile/projectCompile.ts
604
+ import { existsSync as existsSync6 } from "node:fs";
605
+ import { createRequire as createRequire3 } from "node:module";
606
+ import { dirname as dirname5, join as join9 } from "node:path";
607
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
608
+ // ../sdk/src/compile/bundler.ts
609
+ import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync } from "node:fs";
610
+ import { spawnSync } from "node:child_process";
611
+ import { createRequire as createRequire2 } from "node:module";
612
+ import { dirname as dirname4, join as join8, relative as relative2 } from "node:path";
613
+ import { fileURLToPath } from "node:url";
614
+
615
+ // ../sdk/src/compile/assetMacro.ts
616
+ var ASSET_RE = /\basset\(\s*(["'])((?:[^"'\\]|\\.)*)\1\s*\)/g;
617
+ var hasAssetMacro = (src) => src.includes("asset(");
618
+ var desugarAssetMacros = (src, filePath) => {
619
+ const byPath = new Map;
620
+ let i = 0;
621
+ const out = src.replace(ASSET_RE, (_match, _quote, path) => {
622
+ let name = byPath.get(path);
623
+ if (!name) {
624
+ name = `__asset_${i++}`;
625
+ byPath.set(path, name);
636
626
  }
627
+ return name;
628
+ });
629
+ if (/\basset\(/.test(out)) {
630
+ throw new Error(`asset() expects a string literal, found a dynamic argument in: ${filePath}`);
637
631
  }
638
- return entries;
632
+ if (byPath.size === 0)
633
+ return src;
634
+ const imports = [...byPath].map(([path, name]) => `import ${name} from ${JSON.stringify(path)};`).join(`
635
+ `);
636
+ return out + `
637
+ ` + imports;
639
638
  };
640
639
 
641
- // src/textDiff.ts
642
- var MAX_LINES = 4000;
643
- var lcsDiff = (a, b) => {
644
- const n = a.length, m = b.length;
645
- const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
646
- for (let i2 = n - 1;i2 >= 0; i2--) {
647
- for (let j2 = m - 1;j2 >= 0; j2--) {
648
- dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
640
+ // ../sdk/src/compile/aspectMacro.ts
641
+ var ASPECT_RE = /\bclass\s+\w+\s+extends\s+Aspect\s*<\s*(["'])((?:[^"'\\]|\\.)*)\1[^{};]*>\s*\{/g;
642
+ var hasAspectExtends = (src) => /\bextends\s+Aspect\s*</.test(src);
643
+ var injectAspectNames = (src) => {
644
+ let out = "";
645
+ let last = 0;
646
+ ASPECT_RE.lastIndex = 0;
647
+ let m;
648
+ while ((m = ASPECT_RE.exec(src)) !== null) {
649
+ const braceEnd = m.index + m[0].length;
650
+ if (!classBodyDeclaresAspect(src, braceEnd)) {
651
+ out += src.slice(last, braceEnd) + ` static aspect = ${JSON.stringify(m[2])};`;
652
+ last = braceEnd;
649
653
  }
650
654
  }
651
- const ops = [];
652
- let i = 0, j = 0;
653
- while (i < n && j < m) {
654
- if (a[i] === b[j]) {
655
- ops.push({ tag: " ", line: a[i] });
656
- i++;
657
- j++;
658
- } else if (dp[i + 1][j] >= dp[i][j + 1]) {
659
- ops.push({ tag: "-", line: a[i] });
660
- i++;
661
- } else {
662
- ops.push({ tag: "+", line: b[j] });
663
- j++;
664
- }
655
+ return last === 0 ? src : out + src.slice(last);
656
+ };
657
+ var classBodyDeclaresAspect = (src, from) => {
658
+ let depth = 1;
659
+ let i = from;
660
+ for (;i < src.length && depth > 0; i++) {
661
+ const c2 = src[i];
662
+ if (c2 === "{")
663
+ depth++;
664
+ else if (c2 === "}")
665
+ depth--;
665
666
  }
666
- while (i < n)
667
- ops.push({ tag: "-", line: a[i++] });
668
- while (j < m)
669
- ops.push({ tag: "+", line: b[j++] });
670
- return ops;
667
+ return /\bstatic\b[^;{}()=]*\baspect\b/.test(src.slice(from, i));
671
668
  };
672
- var splitLines = (text) => {
673
- if (text === "")
674
- return [];
675
- const lines = text.split(`
676
- `);
677
- if (lines[lines.length - 1] === "")
678
- lines.pop();
679
- return lines;
669
+
670
+ // ../sdk/src/compile/bundler.ts
671
+ var resolveInject = () => {
672
+ const override = process.env.LECODES_SDK_INJECT;
673
+ if (override && existsSync5(override))
674
+ return override;
675
+ const here = dirname4(fileURLToPath(import.meta.url));
676
+ const src = join8(here, "..", "inject.ts");
677
+ if (existsSync5(src))
678
+ return src;
679
+ throw new Error("sdk: could not locate the inject source (LECODES_SDK_INJECT / src/inject.ts)");
680
680
  };
681
- var unifiedDiff = (oldText, newText, oldLabel, newLabel, context = 3) => {
682
- if (oldText === newText)
683
- return "";
684
- const a = splitLines(oldText);
685
- const b = splitLines(newText);
686
- if (a.length + b.length > MAX_LINES) {
687
- return c.dim(` (large file — ${a.length} → ${b.length} lines, diff omitted)`);
688
- }
689
- const ops = lcsDiff(a, b);
690
- const hunks = [];
691
- let aLine = 0, bLine = 0;
692
- let cur = null;
693
- let trailingContext = 0;
694
- for (let k = 0;k < ops.length; k++) {
695
- const op = ops[k];
696
- const isChange = op.tag !== " ";
697
- if (isChange) {
698
- if (!cur) {
699
- const back = [];
700
- let bi = k - 1;
701
- while (bi >= 0 && ops[bi].tag === " " && back.length < context) {
702
- back.unshift(ops[bi]);
703
- bi--;
704
- }
705
- cur = { aStart: aLine - back.length, bStart: bLine - back.length, lines: [...back] };
706
- }
707
- cur.lines.push(op);
708
- trailingContext = 0;
709
- } else if (cur) {
710
- cur.lines.push(op);
711
- if (++trailingContext >= context * 2) {
712
- cur.lines.length -= trailingContext - context;
713
- hunks.push(cur);
714
- cur = null;
715
- trailingContext = 0;
716
- }
717
- }
718
- if (op.tag !== "+")
719
- aLine++;
720
- if (op.tag !== "-")
721
- bLine++;
681
+ var normalize = (path) => {
682
+ const parts = [];
683
+ for (const part of path.split("/")) {
684
+ if (!part || part === ".")
685
+ continue;
686
+ if (part === "..")
687
+ parts.pop();
688
+ else
689
+ parts.push(part);
722
690
  }
723
- if (cur) {
724
- if (trailingContext > context)
725
- cur.lines.length -= trailingContext - context;
726
- hunks.push(cur);
691
+ return "/" + parts.join("/");
692
+ };
693
+ var CHISEL_DEFINE = { RAD2DEG: "57.29577951308232", DEG2RAD: "0.017453292519943295" };
694
+ var CHISEL_KEEP = ["_*"];
695
+ var resolveSdkSrc = () => {
696
+ const inject = resolveInject();
697
+ if (!inject.endsWith("inject.ts")) {
698
+ throw new Error(`chisel needs the SDK source tree (src/inject.ts); got ${inject}. Set LECODES_SDK_INJECT to the source.`);
727
699
  }
728
- const out = [c.bold(`--- ${oldLabel}`), c.bold(`+++ ${newLabel}`)];
729
- for (const h of hunks) {
730
- const aCount = h.lines.filter((o) => o.tag !== "+").length;
731
- const bCount = h.lines.filter((o) => o.tag !== "-").length;
732
- out.push(c.cyan(`@@ -${h.aStart + 1},${aCount} +${h.bStart + 1},${bCount} @@`));
733
- for (const o of h.lines) {
734
- if (o.tag === "+")
735
- out.push(c.green("+" + o.line));
736
- else if (o.tag === "-")
737
- out.push(c.red("-" + o.line));
738
- else
739
- out.push(c.dim(" " + o.line));
700
+ return dirname4(inject);
701
+ };
702
+ var isSdkSource = (path) => /\.[tj]s$/.test(path) && !path.endsWith(".d.ts") && !path.includes("/compile/") && !path.includes("/tests/") && !path.includes("/__tests__/");
703
+ var sdkFilesCache = new Map;
704
+ var collectSdkFiles = (sdkSrc) => {
705
+ const cached = sdkFilesCache.get(sdkSrc);
706
+ if (cached)
707
+ return cached;
708
+ const out = {};
709
+ const walk = (dir) => {
710
+ for (const name of readdirSync4(dir)) {
711
+ const p = join8(dir, name);
712
+ if (statSync(p).isDirectory())
713
+ walk(p);
714
+ else if (isSdkSource(p))
715
+ out["/__sdk/" + relative2(sdkSrc, p).split("\\").join("/")] = readFileSync5(p, "utf8");
740
716
  }
717
+ };
718
+ walk(sdkSrc);
719
+ sdkFilesCache.set(sdkSrc, out);
720
+ return out;
721
+ };
722
+ var resolveChiselBin = () => {
723
+ const override = process.env.LECODES_CHISEL_BIN;
724
+ if (override) {
725
+ if (existsSync5(override))
726
+ return override;
727
+ throw new Error(`LECODES_CHISEL_BIN points at a missing file: ${override}`);
741
728
  }
742
- return out.join(`
743
- `);
729
+ try {
730
+ const bin = createRequire2(import.meta.url)("@letary/chisel").binaryPath();
731
+ if (bin && existsSync5(bin))
732
+ return bin;
733
+ } catch {}
734
+ throw new Error("chisel binary not found. Install @letary/chisel, or set LECODES_CHISEL_BIN.");
744
735
  };
745
-
746
- // src/commands/diff.ts
747
- var diff = async (args) => {
748
- const root = findProjectRoot(process.cwd());
749
- const manifest = readManifest(root);
750
- const config = loadConfig();
751
- const apiUrl = requireApiUrl(config);
752
- const token = requireToken(config);
753
- info(c.dim("Comparing with the server…"));
754
- const project = await getProject(apiUrl, token, manifest.uuid);
755
- const scan = scanProject(root, manifest);
756
- let entries = computeServerDiff(project, scan);
757
- if (args._.length > 0) {
758
- const keys = args._.map(toKey);
759
- entries = entries.filter((e) => keys.some((k) => e.path === k || e.path.startsWith(k + "/")));
736
+ var bundleProjectChisel = async (entrypoint, files, format, sourcemap) => {
737
+ const entry = normalize(entrypoint);
738
+ const projFiles = {};
739
+ for (const [k, v] of Object.entries(files)) {
740
+ const path = normalize(k);
741
+ let src = v;
742
+ if (/\.[tj]sx?$/.test(path)) {
743
+ if (hasAssetMacro(src))
744
+ src = desugarAssetMacros(src, path);
745
+ if (hasAspectExtends(src))
746
+ src = injectAspectNames(src);
747
+ }
748
+ projFiles[path] = src;
760
749
  }
761
- if (entries.length === 0) {
762
- info(c.dim("No differences between local and server."));
763
- return;
750
+ if (!projFiles[entry])
751
+ throw new Error(`Entrypoint not found: ${entry}`);
752
+ const sdkFiles = collectSdkFiles(resolveSdkSrc());
753
+ const minify = process.env.LECODES_CHISEL_MINIFY !== "0";
754
+ const input = JSON.stringify({
755
+ files: { ...sdkFiles, ...projFiles },
756
+ entry,
757
+ inject: ["/__sdk/inject.ts"],
758
+ format,
759
+ minify,
760
+ fuse: process.env.LECODES_CHISEL_FUSE === "1",
761
+ define: CHISEL_DEFINE,
762
+ keep: CHISEL_KEEP,
763
+ sourcemap,
764
+ reactive_ui: process.env.LECODES_CHISEL_REACTIVE_UI !== "0"
765
+ });
766
+ const bin = resolveChiselBin();
767
+ const r = spawnSync(bin, [], { input, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
768
+ if (r.status === 2 || r.error) {
769
+ throw new Error(`chisel failed: ${r.error?.message ?? r.stderr ?? "unknown error"}`);
764
770
  }
765
- if (flagBool(args, "name-only")) {
766
- for (const e of entries)
767
- log(statusLabel(e) + e.path + (e.binary ? c.dim(" (binary)") : ""));
768
- return;
771
+ let parsed;
772
+ try {
773
+ parsed = JSON.parse(r.stdout || "{}");
774
+ } catch {
775
+ throw new Error(`chisel produced invalid output: ${(r.stdout || r.stderr || "").slice(0, 200)}`);
776
+ }
777
+ if (parsed.error)
778
+ throw new Error(`chisel: ${parsed.error}`);
779
+ if (parsed.code == null)
780
+ throw new Error("chisel produced no code");
781
+ return { code: parsed.code, map: parsed.map ?? null };
782
+ };
783
+ var bundleProjectWithMap = async (entrypoint, files, format = "esm") => bundleProjectChisel(entrypoint, files, format, true);
784
+
785
+ // ../sdk/src/compile/header.ts
786
+ var buildHeader = (name, code, opts = {}) => {
787
+ let header = `// name: ${name.slice(0, 20)}
788
+ `;
789
+ if (opts.icon)
790
+ header += `// icon: ${opts.icon}
791
+ `;
792
+ header += `// last-updated: ${new Date().toISOString()}
793
+ `;
794
+ const localAssets = new Set;
795
+ for (const m of code.matchAll(/_creatorUtils\.fetchLocal\((.+?)\)/g)) {
796
+ localAssets.add(m[1].replaceAll(/['"]/g, "").trim());
769
797
  }
798
+ if (/_creator\.createGlb/.test(code))
799
+ localAssets.add("uberarchive");
800
+ if (/_creator\.captureImage/.test(code))
801
+ localAssets.add("capture");
802
+ if (localAssets.size > 0)
803
+ header += `// preload: ${[...localAssets].join(";")}
804
+ `;
805
+ if (!code.includes("_creator.createScene") && !code.includes("_creator.createEntity"))
806
+ header += `// gl: disable
807
+ `;
808
+ else if (code.includes("_creator.createEngine"))
809
+ header += `// gl: defer
810
+ `;
811
+ if (code.includes("_creator2d."))
812
+ header += `// 2d: enable
813
+ `;
814
+ if (code.includes("_creator.createARController"))
815
+ header += "// ar: enable";
816
+ header += `
817
+ `;
818
+ return header;
819
+ };
820
+
821
+ // ../sdk/src/compile/sourcemap.ts
822
+ var offsetSourceMap = (mapJson, lines) => {
823
+ if (lines <= 0)
824
+ return mapJson;
825
+ const map = JSON.parse(mapJson);
826
+ map.mappings = ";".repeat(lines) + map.mappings;
827
+ return JSON.stringify(map);
828
+ };
829
+ var inlineSourceMapComment = (mapJson) => {
830
+ const b64 = Buffer.from(mapJson, "utf8").toString("base64");
831
+ return `//# lecodesMap=data:application/json;charset=utf-8;base64,${b64}`;
832
+ };
833
+
834
+ // ../sdk/src/compile/compileProject.ts
835
+ var detectEntry = (entries) => {
836
+ let entry = "";
837
+ let max = -1;
770
838
  for (const e of entries) {
771
- if (e.binary) {
772
- log(statusLabel(e) + e.path + c.dim(" (binary — content not shown)"));
773
- continue;
839
+ const ext2 = e.path.split(".").at(-1)?.toLowerCase();
840
+ if (e.type === "text" && e.text !== undefined && !e.text.includes("export") && (ext2 === "ts" || ext2 === "js")) {
841
+ if (e.text.length > max) {
842
+ entry = e.path;
843
+ max = e.text.length;
844
+ }
774
845
  }
775
- const d = unifiedDiff(e.serverText ?? "", e.localText ?? "", `server${e.path}`, `local${e.path}`);
776
- if (d)
777
- log(`
778
- ` + d);
779
846
  }
847
+ return entry;
848
+ };
849
+ var compileProject = async (opts) => {
850
+ const entry = opts.entryOverride ?? detectEntry(opts.entries);
851
+ if (!entry) {
852
+ throw new Error("Couldn't find an entrypoint (a top-level .ts/.js file with no `export`). Pass entryOverride.");
853
+ }
854
+ const files = {};
855
+ for (const e of opts.entries) {
856
+ if (e.type === "resource") {
857
+ files[e.path] = `export default "${opts.publicUrl}${e.fileSrc ?? ""}"`;
858
+ } else if (e.type === "shader") {
859
+ const shaders = e.shaders ?? [];
860
+ if (shaders.length > 2) {
861
+ files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.subBackend ?? _creator.backend}")}\``;
862
+ } else if (shaders.length > 0) {
863
+ files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.backend}")}\``;
864
+ } else {
865
+ files[e.path] = `export default ""`;
866
+ }
867
+ } else if (e.path.toLowerCase().endsWith(".svg")) {
868
+ files[e.path] = `export default SvgSource(\`${e.text ?? ""}\`)`;
869
+ } else {
870
+ files[e.path] = e.text ?? "";
871
+ }
872
+ }
873
+ const { code, map } = await bundleProjectWithMap(entry, files, opts.format ?? "esm");
874
+ if (opts.header === false) {
875
+ return map ? `${code}
876
+ ${inlineSourceMapComment(map)}` : code;
877
+ }
878
+ const header = buildHeader(opts.name, code, { icon: opts.icon });
879
+ if (!map)
880
+ return header + code;
881
+ const headerLines = (header.match(/\n/g) ?? []).length;
882
+ return `${header}${code}
883
+ ${inlineSourceMapComment(offsetSourceMap(map, headerLines))}`;
884
+ };
885
+ // src/compile/projectCompile.ts
886
+ var here = dirname5(fileURLToPath2(import.meta.url));
887
+ var vendoredInject = join9(here, "..", "runtime", "sdk", "inject.ts");
888
+ if (existsSync6(vendoredInject))
889
+ process.env.LECODES_SDK_INJECT = vendoredInject;
890
+ var resolveChiselBin2 = () => {
891
+ if (process.env.LECODES_CHISEL_BIN)
892
+ return process.env.LECODES_CHISEL_BIN;
893
+ try {
894
+ return createRequire3(import.meta.url)("@letary/chisel").binaryPath() ?? null;
895
+ } catch {
896
+ return null;
897
+ }
898
+ };
899
+ var chiselBin = resolveChiselBin2();
900
+ if (chiselBin)
901
+ process.env.LECODES_CHISEL_BIN = chiselBin;
902
+ var compileProject2 = (opts) => compileProject({
903
+ entries: opts.entries.map((e) => ({ path: e.path, type: e.type, text: e.text, fileSrc: e.fileSrc, shaders: e.shaders })),
904
+ name: opts.name,
905
+ publicUrl: opts.publicUrl,
906
+ entryOverride: opts.entryOverride,
907
+ header: opts.header,
908
+ format: opts.format
909
+ });
910
+
911
+ // src/compile/designCompile.ts
912
+ var LOCAL_ASSET_SCHEME = "lecodesfile://";
913
+ var compileDesignScreen = async (opts) => {
914
+ const { root, manifest, designKey, screenId } = opts;
915
+ const entries = [];
916
+ const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest?.files ?? {})) });
917
+ for (const file of scan.files) {
918
+ const type = manifest?.files[file.path]?.type ?? classifyKind(basename(file.path));
919
+ if (type === "resource") {
920
+ entries.push({
921
+ path: file.path,
922
+ type,
923
+ fileSrc: opts.assets === "disk" ? encodeURIComponent(file.absPath) : encodeURI(file.path),
924
+ absPath: file.absPath
925
+ });
926
+ } else if (type === "shader") {
927
+ entries.push({ path: file.path, type, shaders: [] });
928
+ } else {
929
+ entries.push({ path: file.path, type, text: readFileSync6(file.absPath, "utf8") });
930
+ }
931
+ }
932
+ const entryPath = `${designKey}/.screen-entry.ts`;
933
+ entries.push({
934
+ path: entryPath,
935
+ type: "text",
936
+ text: `import screen from "./screens/${screenId}"
937
+ screen.open()
938
+ `
939
+ });
940
+ return compileProject2({
941
+ entries,
942
+ name: opts.name,
943
+ publicUrl: opts.assets === "disk" ? LOCAL_ASSET_SCHEME : "/project",
944
+ entryOverride: entryPath,
945
+ header: false,
946
+ format: "iife"
947
+ });
780
948
  };
781
- var statusLabel = (e) => e.status === "added" ? c.green(" added ") : e.status === "removed" ? c.red(" removed ") : c.yellow(" modified ");
782
949
 
783
950
  // src/browserAuth.ts
784
951
  import { createServer } from "node:http";
@@ -858,359 +1025,556 @@ var browserLogin = async (webBase, timeoutMs = 5 * 60 * 1000, open = openBrowser
858
1025
  }
859
1026
  };
860
1027
 
861
- // src/commands/login.ts
862
- var login = async (args) => {
863
- const config = loadConfig();
864
- const apiUrl = normalizeApiUrl(flagStr(args, "api") ?? config.apiUrl ?? DEFAULT_API);
865
- const webBase = normalizeApiUrl(flagStr(args, "web") ?? apiUrl);
866
- const token = flagStr(args, "token") ?? await browserLogin(webBase);
867
- const account = await getAccount(apiUrl, token);
868
- saveConfig({ apiUrl, token });
869
- success(`Logged in as ${c.bold(account.email)} on ${apiUrl}`);
870
- };
1028
+ // src/commands/designTemplates.ts
1029
+ var META_JSON = `{
1030
+ "screens": {
1031
+ "home": { "pos": [0, 0], "desc": "Example screen — replace me" }
1032
+ },
1033
+ "edges": []
1034
+ }
1035
+ `;
1036
+ var TOKENS_TS = `// Design tokens shared by every screen. Colors are hex strings (the engine parses hex only).
1037
+ export const colors = {
1038
+ bg: "#101114",
1039
+ card: "#1c1e24",
1040
+ text: "#f2f3f5",
1041
+ muted: "#8a919e",
1042
+ accent: "#5b8cff",
1043
+ danger: "#e5484d",
1044
+ }
871
1045
 
872
- // src/commands/preview.ts
873
- import { writeFileSync as writeFileSync7 } from "node:fs";
874
- import { resolve as resolve3 } from "node:path";
1046
+ export const spacing = { xs: 4, sm: 8, md: 16, lg: 24 }
875
1047
 
876
- // src/compile/collect.ts
877
- import { readFileSync as readFileSync6 } from "node:fs";
878
- import { basename as basename2 } from "node:path";
879
- var collectEntries = (root, manifest, project) => {
880
- const pathById = buildPaths(project.assets);
881
- const serverByPath = new Map;
882
- for (const asset of project.assets) {
883
- if (asset.type === "folder")
884
- continue;
885
- serverByPath.set(pathById.get(asset.id), { fileSrc: asset.file?.src ?? undefined, shaders: asset.shaders ?? [] });
886
- }
887
- const entries = [];
888
- const warnings = [];
889
- for (const file of scanLocal(root).files) {
890
- const type = manifest.files[file.path]?.type ?? classifyKind(basename2(file.path));
891
- if (type === "resource") {
892
- const src = serverByPath.get(file.path)?.fileSrc;
893
- if (!src)
894
- warnings.push(`No server URL for ${file.path} — push it so its URL resolves.`);
895
- entries.push({ path: file.path, type, fileSrc: src, absPath: file.absPath });
896
- } else if (type === "shader") {
897
- const shaders = serverByPath.get(file.path)?.shaders;
898
- if (!shaders?.length)
899
- warnings.push(`No compiled shader for ${file.path} — push it to compile the shader.`);
900
- entries.push({ path: file.path, type, shaders });
901
- } else {
902
- entries.push({ path: file.path, type, text: readFileSync6(file.absPath, "utf8") });
903
- }
904
- }
905
- return { entries, warnings };
906
- };
1048
+ export const radius = { sm: 8, md: 12, lg: 20 }
1049
+ `;
1050
+ var HOME_SCREEN_TS = `import { colors, spacing, radius } from "../shared/tokens"
907
1051
 
908
- // src/compile/projectCompile.ts
909
- import { existsSync as existsSync6 } from "node:fs";
910
- import { createRequire as createRequire3 } from "node:module";
911
- import { dirname as dirname5, join as join9 } from "node:path";
912
- import { fileURLToPath as fileURLToPath2 } from "node:url";
913
- // ../sdk/src/compile/bundler.ts
914
- import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync7, statSync } from "node:fs";
915
- import { spawnSync } from "node:child_process";
916
- import { createRequire as createRequire2 } from "node:module";
917
- import { dirname as dirname4, join as join8, relative as relative2 } from "node:path";
918
- import { fileURLToPath } from "node:url";
1052
+ // Mock data: the domain shown on realistic values. Inline, per screen.
1053
+ const items = [
1054
+ { title: "Design the home screen", done: true },
1055
+ { title: "Sketch the flow on the canvas", done: false },
1056
+ { title: "Ask Claude to add a details screen", done: false },
1057
+ ]
919
1058
 
920
- // ../sdk/src/compile/assetMacro.ts
921
- var ASSET_RE = /\basset\(\s*(["'])((?:[^"'\\]|\\.)*)\1\s*\)/g;
922
- var hasAssetMacro = (src) => src.includes("asset(");
923
- var desugarAssetMacros = (src, filePath) => {
924
- const byPath = new Map;
925
- let i = 0;
926
- const out = src.replace(ASSET_RE, (_match, _quote, path) => {
927
- let name = byPath.get(path);
928
- if (!name) {
929
- name = `__asset_${i++}`;
930
- byPath.set(path, name);
931
- }
932
- return name;
933
- });
934
- if (/\basset\(/.test(out)) {
935
- throw new Error(`asset() expects a string literal, found a dynamic argument in: ${filePath}`);
1059
+ export default UIScreen([
1060
+ UIText("Today").style({ fontSize: 28, fontWeight: 700, color: colors.text, mt: spacing.lg }),
1061
+ UIText("3 tasks").style({ fontSize: 14, color: colors.muted, mt: spacing.xs }),
1062
+ UIColumn(items.map((item) =>
1063
+ UIRow([
1064
+ UIColumn(item.done ? [ UIText("✓").style({ fontSize: 12, color: colors.text }) ] : []).style({
1065
+ width: 22,
1066
+ height: 22,
1067
+ borderRadius: 11,
1068
+ justifyContent: "center",
1069
+ alignItems: "center",
1070
+ border: item.done ? undefined : \`2px solid \${colors.muted}\`,
1071
+ bgColor: item.done ? colors.accent : undefined,
1072
+ }),
1073
+ UIText(item.title).style({ fontSize: 16, color: item.done ? colors.muted : colors.text, flexGrow: 1 }),
1074
+ ]).style({
1075
+ bgColor: colors.card,
1076
+ borderRadius: radius.md,
1077
+ p: spacing.md,
1078
+ gap: spacing.md,
1079
+ alignItems: "center",
1080
+ }),
1081
+ )).style({ gap: spacing.sm, mt: spacing.lg }),
1082
+ UISpacer(),
1083
+ UIButton([
1084
+ UIText("Add task").style({ fontSize: 16, fontWeight: 600, color: colors.text }),
1085
+ ]).style({
1086
+ name: "add-task",
1087
+ bgColor: colors.accent,
1088
+ borderRadius: radius.lg,
1089
+ p: spacing.md,
1090
+ mb: spacing.lg,
1091
+ }),
1092
+ ]).style({ bgColor: colors.bg, p: spacing.md, pt: "safe-top" })
1093
+ `;
1094
+ var CLAUDE_MD = `# LeCodes Design — screen prototyping canvas
1095
+
1096
+ This folder is a **design canvas** for a LeCodes app: each file in \`screens/\` is one live,
1097
+ executable screen shown on a spatial canvas in the browser (\`lecodes design\` runs the dev server).
1098
+ You (Claude) write the screens; the developer views them on the canvas and gives feedback.
1099
+ This is an intent-capture stage — screens run on inline mock data, before any backend exists.
1100
+
1101
+ ## Read this first, then meta.json
1102
+
1103
+ \`meta.json\` is the app map: every screen (position, description) and the edges between them
1104
+ (navigation intent). **Read it before opening any screen file.** It is the human's file — the
1105
+ canvas and the developer own positions and descriptions. You MAY add entries for new screens you
1106
+ create (pick a free position: screens are 390×844, leave ~160px gaps) and add edges when the flow
1107
+ is clear, but never reposition or rewrite existing human-authored descriptions.
1108
+
1109
+ \`\`\`json
1110
+ {
1111
+ "screens": { "login": { "pos": [0, 0], "desc": "Phone login" } },
1112
+ "edges": [ { "from": "login", "to": "code", "label": "after phone entry", "activator": "submit" } ]
1113
+ }
1114
+ \`\`\`
1115
+
1116
+ - An edge's optional \`activator\` names the element (its \`name\` style option) that triggers the
1117
+ transition. Give a \`name\` to interactive elements an edge refers to — nothing else needs names.
1118
+
1119
+ ## Screen rules
1120
+
1121
+ - **One file = one screen.** \`screens/<id>.ts\` default-exports an already-computed screen:
1122
+ \`export default UIScreen([ … ]).style({ … })\`. The filename is the screen's identity.
1123
+ - **Styles go through the chained \`.style({ … })\` method** — e.g.
1124
+ \`UIText("Today").style({ fontSize: 28, fontWeight: 700 })\`. Passing a style object as the
1125
+ first argument is a legacy form.
1126
+ - **Screens are islands.** A screen file imports ONLY from \`../shared/\` — never from another
1127
+ screen, and screens take no props. All relational info lives in meta.json.
1128
+ - **Mock data is the domain.** Put realistic inline mock data at the top of the screen file
1129
+ (entities + fields ARE the future schema — make them meaningful, not "Lorem ipsum").
1130
+ - **Reuse via shared/.** Design tokens and reusable components live in \`shared/\`. Use them in
1131
+ every screen. Promoting a repeated pattern into \`shared/\` is an explicit, deliberate step.
1132
+ - The LeCodes UI API (\`UIScreen\`, \`UIColumn\`, \`UIRow\`, \`UIBox\`, \`UIText\`, \`UIImage\`,
1133
+ \`UIButton\`, \`UIInput\`, \`UISpacer\`, \`UIScrollable\`, …) is **global** — no imports needed.
1134
+ Colors must be hex strings.
1135
+
1136
+ ## See what you built
1137
+
1138
+ The developer watches the canvas live (screens hot-reload on save). To see a screen yourself:
1139
+
1140
+ \`\`\`sh
1141
+ lecodes design snapshot <id> # semantic JSON of the laid-out screen → stdout
1142
+ lecodes design snapshot <id> --png # + PNG screenshot → design/.snapshots/<id>.png
1143
+ lecodes design snapshot # all screens → design/.snapshots/
1144
+ \`\`\`
1145
+
1146
+ Snapshot after every screen you create or change, and fix what looks wrong (overflow, overlap,
1147
+ unreadable contrast, off-screen content) before reporting back.
1148
+
1149
+ ## Workflow
1150
+
1151
+ 1. Read \`meta.json\`, then the \`shared/\` signatures, then only the screens you need.
1152
+ 2. Create/edit screen files; register new screens (and clear edges) in \`meta.json\`.
1153
+ 3. Snapshot → verify → fix.
1154
+ 4. When the developer edits a description or the flow on the canvas, treat meta.json as the
1155
+ updated spec.
1156
+ `;
1157
+ var README_HINT = `# design/
1158
+
1159
+ LeCodes Design canvas — run \`lecodes design\` here and open the printed URL.
1160
+ See CLAUDE.md for the conventions (it's written for AI assistants, but it's the spec).
1161
+ `;
1162
+
1163
+ // src/commands/design.ts
1164
+ var DEFAULT_DIR = "design";
1165
+ var DEFAULT_PORT = 4477;
1166
+ var getContext = (args) => {
1167
+ let root;
1168
+ let manifest;
1169
+ try {
1170
+ root = findProjectRoot(process.cwd());
1171
+ manifest = readManifest(root);
1172
+ } catch {
1173
+ root = process.cwd();
1174
+ manifest = null;
936
1175
  }
937
- if (byPath.size === 0)
938
- return src;
939
- const header = [...byPath].map(([path, name]) => `import ${name} from ${JSON.stringify(path)};`).join(`
940
- `);
941
- return header + `
942
- ` + out;
1176
+ const dirName = (flagStr(args, "dir") ?? DEFAULT_DIR).replace(/[/\\]+$/, "");
1177
+ return {
1178
+ root,
1179
+ manifest,
1180
+ name: manifest?.name ?? basename2(root),
1181
+ dirName,
1182
+ designDir: join10(root, dirName),
1183
+ designKey: "/" + dirName.replace(/\\/g, "/")
1184
+ };
943
1185
  };
944
-
945
- // ../sdk/src/compile/aspectMacro.ts
946
- var ASPECT_RE = /\bclass\s+\w+\s+extends\s+Aspect\s*<\s*(["'])((?:[^"'\\]|\\.)*)\1[^{};]*>\s*\{/g;
947
- var hasAspectExtends = (src) => /\bextends\s+Aspect\s*</.test(src);
948
- var injectAspectNames = (src) => {
949
- let out = "";
950
- let last = 0;
951
- ASPECT_RE.lastIndex = 0;
952
- let m;
953
- while ((m = ASPECT_RE.exec(src)) !== null) {
954
- const braceEnd = m.index + m[0].length;
955
- if (!classBodyDeclaresAspect(src, braceEnd)) {
956
- out += src.slice(last, braceEnd) + ` static aspect = ${JSON.stringify(m[2])};`;
957
- last = braceEnd;
1186
+ var listScreens = (designDir) => {
1187
+ const dir = join10(designDir, "screens");
1188
+ if (!existsSync7(dir))
1189
+ return [];
1190
+ return readdirSync5(dir).filter((f) => f.endsWith(".ts") && !f.startsWith(".")).map((f) => f.slice(0, -3)).sort();
1191
+ };
1192
+ var readMetaSizes = (designDir) => {
1193
+ try {
1194
+ const meta = JSON.parse(readFileSync7(join10(designDir, "meta.json"), "utf8"));
1195
+ return meta?.screens ?? {};
1196
+ } catch {
1197
+ return {};
1198
+ }
1199
+ };
1200
+ var init = async (ctx) => {
1201
+ const files = [
1202
+ ["meta.json", META_JSON],
1203
+ ["README.md", README_HINT],
1204
+ ["CLAUDE.md", CLAUDE_MD],
1205
+ [join10("shared", "tokens.ts"), TOKENS_TS],
1206
+ [join10("screens", "home.ts"), HOME_SCREEN_TS]
1207
+ ];
1208
+ mkdirSync7(join10(ctx.designDir, "screens"), { recursive: true });
1209
+ mkdirSync7(join10(ctx.designDir, "shared"), { recursive: true });
1210
+ let created = 0;
1211
+ for (const [rel, content] of files) {
1212
+ const abs = join10(ctx.designDir, rel);
1213
+ if (existsSync7(abs)) {
1214
+ note(`kept ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
1215
+ continue;
1216
+ }
1217
+ writeFileSync7(abs, content);
1218
+ log(`${c.green("created")} ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
1219
+ created++;
1220
+ }
1221
+ const ignorePath = join10(ctx.root, ".lecodesignore");
1222
+ const ignoreLine = `${ctx.dirName}/.snapshots/`;
1223
+ const current = existsSync7(ignorePath) ? readFileSync7(ignorePath, "utf8") : "";
1224
+ if (!current.split(/\r?\n/).includes(ignoreLine)) {
1225
+ appendFileSync(ignorePath, (current && !current.endsWith(`
1226
+ `) ? `
1227
+ ` : "") + ignoreLine + `
1228
+ `);
1229
+ log(`${c.green("ignored")} ${ignoreLine} (in .lecodesignore)`);
1230
+ }
1231
+ log("");
1232
+ log(created ? "Design canvas ready." : "Design canvas already initialized.");
1233
+ log(` ${c.bold("lecodes design")} start the canvas (browser)`);
1234
+ log(` ${c.bold("lecodes design snapshot")} render screens headless (for AI feedback)`);
1235
+ log(` Point Claude Code at ${c.bold(ctx.dirName + "/CLAUDE.md")} — it documents the conventions.`);
1236
+ };
1237
+ var snapshot = async (ctx, args) => {
1238
+ const all = listScreens(ctx.designDir);
1239
+ if (all.length === 0)
1240
+ throw new CliError(`No screens in ${ctx.dirName}/screens/ — run "lecodes design init" first.`);
1241
+ const requested = args._.slice(1);
1242
+ for (const id of requested) {
1243
+ if (!all.includes(id))
1244
+ throw new CliError(`No screen "${id}" (have: ${all.join(", ")}).`);
1245
+ }
1246
+ const targets = requested.length ? requested : all;
1247
+ let renderer;
1248
+ try {
1249
+ renderer = await import("lecodes-renderer/headless");
1250
+ } catch {
1251
+ throw new CliError("The renderer isn't installed. Run: npm i -D lecodes-renderer");
1252
+ }
1253
+ const killer = setTimeout(() => {
1254
+ warnErr("Snapshot timed out after 120s.");
1255
+ process.exit(1);
1256
+ }, 120000);
1257
+ killer.unref?.();
1258
+ const png = flagBool(args, "png") || flagStr(args, "png") !== undefined;
1259
+ const sizes = readMetaSizes(ctx.designDir);
1260
+ const outDir = flagStr(args, "out-dir") ?? join10(ctx.designDir, ".snapshots");
1261
+ const single = targets.length === 1 && requested.length === 1 && !png && !flagStr(args, "out-dir");
1262
+ const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
1263
+ if (!single)
1264
+ mkdirSync7(outDir, { recursive: true });
1265
+ for (const id of targets) {
1266
+ note(`Rendering ${id}…`);
1267
+ const js = await compileDesignScreen({
1268
+ root: ctx.root,
1269
+ manifest: ctx.manifest,
1270
+ name: ctx.name,
1271
+ designKey: ctx.designKey,
1272
+ screenId: id,
1273
+ assets: "disk"
1274
+ });
1275
+ const [width, height] = sizes[id]?.size ?? [390, 844];
1276
+ const result = await renderer.renderToJson(js, { width, height, localAssets: true, onConsole });
1277
+ for (const w of result.warnings)
1278
+ warnErr(`${id}: ${w}`);
1279
+ if (single) {
1280
+ const text = result.text.endsWith(`
1281
+ `) ? result.text : result.text + `
1282
+ `;
1283
+ process.stdout.write(text);
1284
+ return;
1285
+ }
1286
+ writeFileSync7(join10(outDir, `${id}.json`), result.text);
1287
+ log(`${c.green("✓")} ${id} → ${join10(outDir, `${id}.json`)}`);
1288
+ if (png) {
1289
+ const pngResult = await renderer.renderToPng(js, { width, height, localAssets: true, scale: 2, onConsole });
1290
+ for (const w of pngResult.warnings)
1291
+ warnErr(`${id}: ${w}`);
1292
+ writeFileSync7(join10(outDir, `${id}.png`), pngResult.png);
1293
+ log(`${c.green("✓")} ${id} → ${join10(outDir, `${id}.png`)}`);
958
1294
  }
959
1295
  }
960
- return last === 0 ? src : out + src.slice(last);
961
1296
  };
962
- var classBodyDeclaresAspect = (src, from) => {
963
- let depth = 1;
964
- let i = from;
965
- for (;i < src.length && depth > 0; i++) {
966
- const c2 = src[i];
967
- if (c2 === "{")
968
- depth++;
969
- else if (c2 === "}")
970
- depth--;
1297
+ var serve = async (ctx, args) => {
1298
+ if (!existsSync7(ctx.designDir)) {
1299
+ throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
971
1300
  }
972
- return /\bstatic\b[^;{}()=]*\baspect\b/.test(src.slice(from, i));
1301
+ let designServer;
1302
+ try {
1303
+ designServer = await import("lecodes-design/server");
1304
+ } catch {
1305
+ throw new CliError("The design canvas isn't installed. Run: npm i -D lecodes-design");
1306
+ }
1307
+ const port = Number(flagStr(args, "port")) || DEFAULT_PORT;
1308
+ const server = await designServer.startDesignServer({
1309
+ root: ctx.root,
1310
+ designDir: ctx.designDir,
1311
+ name: ctx.name,
1312
+ port,
1313
+ log: note,
1314
+ compileScreen: async (id) => ({
1315
+ js: await compileDesignScreen({
1316
+ root: ctx.root,
1317
+ manifest: ctx.manifest,
1318
+ name: ctx.name,
1319
+ designKey: ctx.designKey,
1320
+ screenId: id,
1321
+ assets: "server"
1322
+ }),
1323
+ warnings: []
1324
+ })
1325
+ });
1326
+ log(`${c.bold("LeCodes Design")} — ${ctx.name}`);
1327
+ log(`Canvas: ${c.bold(server.url)}`);
1328
+ log(`Watching: ${ctx.dirName}/ (screens hot-reload on save; Ctrl+C to stop)`);
1329
+ if (!flagBool(args, "no-open"))
1330
+ openBrowser(server.url);
1331
+ };
1332
+ var design = async (args) => {
1333
+ const sub = args._[0] ?? "serve";
1334
+ const ctx = getContext(args);
1335
+ if (sub === "serve")
1336
+ return serve(ctx, args);
1337
+ if (sub === "init")
1338
+ return init(ctx);
1339
+ if (sub === "snapshot")
1340
+ return snapshot(ctx, args);
1341
+ throw new CliError(`Unknown design subcommand "${sub}" (serve | init | snapshot).`);
973
1342
  };
974
1343
 
975
- // ../sdk/src/compile/bundler.ts
976
- var resolveInject = () => {
977
- const override = process.env.LECODES_SDK_INJECT;
978
- if (override && existsSync5(override))
979
- return override;
980
- const here = dirname4(fileURLToPath(import.meta.url));
981
- const src = join8(here, "..", "inject.ts");
982
- if (existsSync5(src))
983
- return src;
984
- throw new Error("sdk: could not locate the inject source (LECODES_SDK_INJECT / src/inject.ts)");
985
- };
986
- var normalize = (path) => {
987
- const parts = [];
988
- for (const part of path.split("/")) {
989
- if (!part || part === ".")
1344
+ // src/serverDiff.ts
1345
+ import { readFileSync as readFileSync8 } from "node:fs";
1346
+ import { basename as basename3 } from "node:path";
1347
+ var isTextAsset = (t) => t === "text" || t === "shader" || t === "scene";
1348
+ var computeServerDiff = (project, scan) => {
1349
+ const pathById = buildPaths(project.assets);
1350
+ const serverByPath = new Map;
1351
+ for (const asset of project.assets) {
1352
+ if (asset.type === "folder")
990
1353
  continue;
991
- if (part === "..")
992
- parts.pop();
993
- else
994
- parts.push(part);
1354
+ serverByPath.set(pathById.get(asset.id), asset);
995
1355
  }
996
- return "/" + parts.join("/");
997
- };
998
- var CHISEL_DEFINE = { RAD2DEG: "57.29577951308232", DEG2RAD: "0.017453292519943295" };
999
- var CHISEL_KEEP = ["_*"];
1000
- var resolveSdkSrc = () => {
1001
- const inject = resolveInject();
1002
- if (!inject.endsWith("inject.ts")) {
1003
- throw new Error(`chisel needs the SDK source tree (src/inject.ts); got ${inject}. Set LECODES_SDK_INJECT to the source.`);
1356
+ const localByPath = new Map(scan.files.map((f) => [f.path, f]));
1357
+ const entries = [];
1358
+ const paths = new Set([...serverByPath.keys(), ...localByPath.keys()]);
1359
+ for (const path of [...paths].sort()) {
1360
+ const server = serverByPath.get(path);
1361
+ const local = localByPath.get(path);
1362
+ if (local && !server) {
1363
+ const text = isTextKind(classifyKind(basename3(path)));
1364
+ entries.push({ path, status: "added", binary: !text, serverText: "", localText: text ? readFileSync8(local.absPath, "utf8") : undefined });
1365
+ continue;
1366
+ }
1367
+ if (server && !local) {
1368
+ const text = isTextAsset(server.type);
1369
+ entries.push({ path, status: "removed", binary: !text, serverText: text ? server.text ?? "" : undefined, localText: "" });
1370
+ continue;
1371
+ }
1372
+ if (server && local) {
1373
+ if (isTextAsset(server.type)) {
1374
+ const serverText = server.text ?? "";
1375
+ const localText = readFileSync8(local.absPath, "utf8");
1376
+ if (serverText !== localText)
1377
+ entries.push({ path, status: "modified", binary: false, serverText, localText });
1378
+ } else {
1379
+ const serverSize = server.file?.size;
1380
+ const localSize = Buffer.byteLength(readFileSync8(local.absPath));
1381
+ if (serverSize !== undefined && serverSize !== localSize) {
1382
+ entries.push({ path, status: "modified", binary: true });
1383
+ }
1384
+ }
1385
+ }
1004
1386
  }
1005
- return dirname4(inject);
1387
+ return entries;
1006
1388
  };
1007
- var isSdkSource = (path) => /\.[tj]s$/.test(path) && !path.endsWith(".d.ts") && !path.includes("/compile/") && !path.includes("/tests/") && !path.includes("/__tests__/");
1008
- var sdkFilesCache = new Map;
1009
- var collectSdkFiles = (sdkSrc) => {
1010
- const cached = sdkFilesCache.get(sdkSrc);
1011
- if (cached)
1012
- return cached;
1013
- const out = {};
1014
- const walk = (dir) => {
1015
- for (const name of readdirSync4(dir)) {
1016
- const p = join8(dir, name);
1017
- if (statSync(p).isDirectory())
1018
- walk(p);
1019
- else if (isSdkSource(p))
1020
- out["/__sdk/" + relative2(sdkSrc, p).split("\\").join("/")] = readFileSync7(p, "utf8");
1389
+
1390
+ // src/textDiff.ts
1391
+ var MAX_LINES = 4000;
1392
+ var lcsDiff = (a, b) => {
1393
+ const n = a.length, m = b.length;
1394
+ const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
1395
+ for (let i2 = n - 1;i2 >= 0; i2--) {
1396
+ for (let j2 = m - 1;j2 >= 0; j2--) {
1397
+ dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
1021
1398
  }
1022
- };
1023
- walk(sdkSrc);
1024
- sdkFilesCache.set(sdkSrc, out);
1025
- return out;
1026
- };
1027
- var resolveChiselBin = () => {
1028
- const override = process.env.LECODES_CHISEL_BIN;
1029
- if (override) {
1030
- if (existsSync5(override))
1031
- return override;
1032
- throw new Error(`LECODES_CHISEL_BIN points at a missing file: ${override}`);
1033
1399
  }
1034
- try {
1035
- const bin = createRequire2(import.meta.url)("@letary/chisel").binaryPath();
1036
- if (bin && existsSync5(bin))
1037
- return bin;
1038
- } catch {}
1039
- throw new Error("chisel binary not found. Install @letary/chisel, or set LECODES_CHISEL_BIN.");
1400
+ const ops = [];
1401
+ let i = 0, j = 0;
1402
+ while (i < n && j < m) {
1403
+ if (a[i] === b[j]) {
1404
+ ops.push({ tag: " ", line: a[i] });
1405
+ i++;
1406
+ j++;
1407
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) {
1408
+ ops.push({ tag: "-", line: a[i] });
1409
+ i++;
1410
+ } else {
1411
+ ops.push({ tag: "+", line: b[j] });
1412
+ j++;
1413
+ }
1414
+ }
1415
+ while (i < n)
1416
+ ops.push({ tag: "-", line: a[i++] });
1417
+ while (j < m)
1418
+ ops.push({ tag: "+", line: b[j++] });
1419
+ return ops;
1040
1420
  };
1041
- var bundleProjectChisel = async (entrypoint, files, format, sourcemap) => {
1042
- const entry = normalize(entrypoint);
1043
- const projFiles = {};
1044
- for (const [k, v] of Object.entries(files)) {
1045
- const path = normalize(k);
1046
- let src = v;
1047
- if (/\.[tj]sx?$/.test(path)) {
1048
- if (hasAssetMacro(src))
1049
- src = desugarAssetMacros(src, path);
1050
- if (hasAspectExtends(src))
1051
- src = injectAspectNames(src);
1421
+ var splitLines = (text) => {
1422
+ if (text === "")
1423
+ return [];
1424
+ const lines = text.split(`
1425
+ `);
1426
+ if (lines[lines.length - 1] === "")
1427
+ lines.pop();
1428
+ return lines;
1429
+ };
1430
+ var unifiedDiff = (oldText, newText, oldLabel, newLabel, context = 3) => {
1431
+ if (oldText === newText)
1432
+ return "";
1433
+ const a = splitLines(oldText);
1434
+ const b = splitLines(newText);
1435
+ if (a.length + b.length > MAX_LINES) {
1436
+ return c.dim(` (large file — ${a.length} → ${b.length} lines, diff omitted)`);
1437
+ }
1438
+ const ops = lcsDiff(a, b);
1439
+ const hunks = [];
1440
+ let aLine = 0, bLine = 0;
1441
+ let cur = null;
1442
+ let trailingContext = 0;
1443
+ for (let k = 0;k < ops.length; k++) {
1444
+ const op = ops[k];
1445
+ const isChange = op.tag !== " ";
1446
+ if (isChange) {
1447
+ if (!cur) {
1448
+ const back = [];
1449
+ let bi = k - 1;
1450
+ while (bi >= 0 && ops[bi].tag === " " && back.length < context) {
1451
+ back.unshift(ops[bi]);
1452
+ bi--;
1453
+ }
1454
+ cur = { aStart: aLine - back.length, bStart: bLine - back.length, lines: [...back] };
1455
+ }
1456
+ cur.lines.push(op);
1457
+ trailingContext = 0;
1458
+ } else if (cur) {
1459
+ cur.lines.push(op);
1460
+ if (++trailingContext >= context * 2) {
1461
+ cur.lines.length -= trailingContext - context;
1462
+ hunks.push(cur);
1463
+ cur = null;
1464
+ trailingContext = 0;
1465
+ }
1052
1466
  }
1053
- projFiles[path] = src;
1467
+ if (op.tag !== "+")
1468
+ aLine++;
1469
+ if (op.tag !== "-")
1470
+ bLine++;
1054
1471
  }
1055
- if (!projFiles[entry])
1056
- throw new Error(`Entrypoint not found: ${entry}`);
1057
- const sdkFiles = collectSdkFiles(resolveSdkSrc());
1058
- const minify = process.env.LECODES_CHISEL_MINIFY !== "0";
1059
- const input = JSON.stringify({
1060
- files: { ...sdkFiles, ...projFiles },
1061
- entry,
1062
- inject: ["/__sdk/inject.ts"],
1063
- format,
1064
- minify,
1065
- fuse: process.env.LECODES_CHISEL_FUSE === "1",
1066
- define: CHISEL_DEFINE,
1067
- keep: CHISEL_KEEP,
1068
- sourcemap
1069
- });
1070
- const bin = resolveChiselBin();
1071
- const r = spawnSync(bin, [], { input, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
1072
- if (r.status === 2 || r.error) {
1073
- throw new Error(`chisel failed: ${r.error?.message ?? r.stderr ?? "unknown error"}`);
1472
+ if (cur) {
1473
+ if (trailingContext > context)
1474
+ cur.lines.length -= trailingContext - context;
1475
+ hunks.push(cur);
1074
1476
  }
1075
- let parsed;
1076
- try {
1077
- parsed = JSON.parse(r.stdout || "{}");
1078
- } catch {
1079
- throw new Error(`chisel produced invalid output: ${(r.stdout || r.stderr || "").slice(0, 200)}`);
1477
+ const out = [c.bold(`--- ${oldLabel}`), c.bold(`+++ ${newLabel}`)];
1478
+ for (const h of hunks) {
1479
+ const aCount = h.lines.filter((o) => o.tag !== "+").length;
1480
+ const bCount = h.lines.filter((o) => o.tag !== "-").length;
1481
+ out.push(c.cyan(`@@ -${h.aStart + 1},${aCount} +${h.bStart + 1},${bCount} @@`));
1482
+ for (const o of h.lines) {
1483
+ if (o.tag === "+")
1484
+ out.push(c.green("+" + o.line));
1485
+ else if (o.tag === "-")
1486
+ out.push(c.red("-" + o.line));
1487
+ else
1488
+ out.push(c.dim(" " + o.line));
1489
+ }
1080
1490
  }
1081
- if (parsed.error)
1082
- throw new Error(`chisel: ${parsed.error}`);
1083
- if (parsed.code == null)
1084
- throw new Error("chisel produced no code");
1085
- return { code: parsed.code, map: parsed.map ?? null };
1491
+ return out.join(`
1492
+ `);
1086
1493
  };
1087
- var bundleProjectWithMap = async (entrypoint, files, format = "esm") => bundleProjectChisel(entrypoint, files, format, true);
1088
1494
 
1089
- // ../sdk/src/compile/header.ts
1090
- var buildHeader = (name, code, opts = {}) => {
1091
- let header = `// name: ${name.slice(0, 20)}
1092
- `;
1093
- if (opts.icon)
1094
- header += `// icon: ${opts.icon}
1095
- `;
1096
- header += `// last-updated: ${new Date().toISOString()}
1097
- `;
1098
- const localAssets = new Set;
1099
- for (const m of code.matchAll(/_creatorUtils\.fetchLocal\((.+?)\)/g)) {
1100
- localAssets.add(m[1].replaceAll(/['"]/g, "").trim());
1495
+ // src/commands/diff.ts
1496
+ var diff = async (args) => {
1497
+ const root = findProjectRoot(process.cwd());
1498
+ const manifest = readManifest(root);
1499
+ const config = loadConfig();
1500
+ const apiUrl = requireApiUrl(config);
1501
+ const token = requireToken(config);
1502
+ info(c.dim("Comparing with the server…"));
1503
+ const project = await getProject(apiUrl, token, manifest.uuid);
1504
+ const scan = scanProject(root, manifest);
1505
+ let entries = computeServerDiff(project, scan);
1506
+ if (args._.length > 0) {
1507
+ const keys = args._.map(toKey);
1508
+ entries = entries.filter((e) => keys.some((k) => e.path === k || e.path.startsWith(k + "/")));
1509
+ }
1510
+ if (entries.length === 0) {
1511
+ info(c.dim("No differences between local and server."));
1512
+ return;
1513
+ }
1514
+ if (flagBool(args, "name-only")) {
1515
+ for (const e of entries)
1516
+ log(statusLabel(e) + e.path + (e.binary ? c.dim(" (binary)") : ""));
1517
+ return;
1101
1518
  }
1102
- if (/_creator\.createGlb/.test(code))
1103
- localAssets.add("uberarchive");
1104
- if (/_creator\.captureImage/.test(code))
1105
- localAssets.add("capture");
1106
- if (localAssets.size > 0)
1107
- header += `// preload: ${[...localAssets].join(";")}
1108
- `;
1109
- if (!code.includes("_creator.createScene") && !code.includes("_creator.createEntity"))
1110
- header += `// gl: disable
1111
- `;
1112
- else if (code.includes("_creator.createEngine"))
1113
- header += `// gl: defer
1114
- `;
1115
- if (code.includes("_creator2d."))
1116
- header += `// 2d: enable
1117
- `;
1118
- if (code.includes("_creator.createARController"))
1119
- header += "// ar: enable";
1120
- header += `
1121
- `;
1122
- return header;
1123
- };
1124
-
1125
- // ../sdk/src/compile/sourcemap.ts
1126
- var offsetSourceMap = (mapJson, lines) => {
1127
- if (lines <= 0)
1128
- return mapJson;
1129
- const map = JSON.parse(mapJson);
1130
- map.mappings = ";".repeat(lines) + map.mappings;
1131
- return JSON.stringify(map);
1132
- };
1133
- var inlineSourceMapComment = (mapJson) => {
1134
- const b64 = Buffer.from(mapJson, "utf8").toString("base64");
1135
- return `//# lecodesMap=data:application/json;charset=utf-8;base64,${b64}`;
1136
- };
1137
-
1138
- // ../sdk/src/compile/compileProject.ts
1139
- var detectEntry = (entries) => {
1140
- let entry = "";
1141
- let max = -1;
1142
1519
  for (const e of entries) {
1143
- const ext2 = e.path.split(".").at(-1)?.toLowerCase();
1144
- if (e.type === "text" && e.text !== undefined && !e.text.includes("export") && (ext2 === "ts" || ext2 === "js")) {
1145
- if (e.text.length > max) {
1146
- entry = e.path;
1147
- max = e.text.length;
1148
- }
1520
+ if (e.binary) {
1521
+ log(statusLabel(e) + e.path + c.dim(" (binary — content not shown)"));
1522
+ continue;
1149
1523
  }
1524
+ const d = unifiedDiff(e.serverText ?? "", e.localText ?? "", `server${e.path}`, `local${e.path}`);
1525
+ if (d)
1526
+ log(`
1527
+ ` + d);
1150
1528
  }
1151
- return entry;
1152
1529
  };
1153
- var compileProject = async (opts) => {
1154
- const entry = opts.entryOverride ?? detectEntry(opts.entries);
1155
- if (!entry) {
1156
- throw new Error("Couldn't find an entrypoint (a top-level .ts/.js file with no `export`). Pass entryOverride.");
1530
+ var statusLabel = (e) => e.status === "added" ? c.green(" added ") : e.status === "removed" ? c.red(" removed ") : c.yellow(" modified ");
1531
+
1532
+ // src/commands/login.ts
1533
+ var login = async (args) => {
1534
+ const config = loadConfig();
1535
+ const apiUrl = normalizeApiUrl(flagStr(args, "api") ?? config.apiUrl ?? DEFAULT_API);
1536
+ const webBase = normalizeApiUrl(flagStr(args, "web") ?? apiUrl);
1537
+ const token = flagStr(args, "token") ?? await browserLogin(webBase);
1538
+ const account = await getAccount(apiUrl, token);
1539
+ saveConfig({ apiUrl, token });
1540
+ success(`Logged in as ${c.bold(account.email)} on ${apiUrl}`);
1541
+ };
1542
+
1543
+ // src/commands/preview.ts
1544
+ import { writeFileSync as writeFileSync8 } from "node:fs";
1545
+ import { resolve as resolve3 } from "node:path";
1546
+
1547
+ // src/compile/collect.ts
1548
+ import { readFileSync as readFileSync9 } from "node:fs";
1549
+ import { basename as basename4 } from "node:path";
1550
+ var collectEntries = (root, manifest, project) => {
1551
+ const pathById = buildPaths(project.assets);
1552
+ const serverByPath = new Map;
1553
+ for (const asset of project.assets) {
1554
+ if (asset.type === "folder")
1555
+ continue;
1556
+ serverByPath.set(pathById.get(asset.id), { fileSrc: asset.file?.src ?? undefined, shaders: asset.shaders ?? [] });
1157
1557
  }
1158
- const files = {};
1159
- for (const e of opts.entries) {
1160
- if (e.type === "resource") {
1161
- files[e.path] = `export default "${opts.publicUrl}${e.fileSrc ?? ""}"`;
1162
- } else if (e.type === "shader") {
1163
- const shaders = e.shaders ?? [];
1164
- if (shaders.length > 2) {
1165
- files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.subBackend ?? _creator.backend}")}\``;
1166
- } else if (shaders.length > 0) {
1167
- files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.backend}")}\``;
1168
- } else {
1169
- files[e.path] = `export default ""`;
1170
- }
1171
- } else if (e.path.toLowerCase().endsWith(".svg")) {
1172
- files[e.path] = `export default SvgSource(\`${e.text ?? ""}\`)`;
1558
+ const entries = [];
1559
+ const warnings = [];
1560
+ for (const file of scanLocal(root).files) {
1561
+ const type = manifest.files[file.path]?.type ?? classifyKind(basename4(file.path));
1562
+ if (type === "resource") {
1563
+ const src = serverByPath.get(file.path)?.fileSrc;
1564
+ if (!src)
1565
+ warnings.push(`No server URL for ${file.path} — push it so its URL resolves.`);
1566
+ entries.push({ path: file.path, type, fileSrc: src, absPath: file.absPath });
1567
+ } else if (type === "shader") {
1568
+ const shaders = serverByPath.get(file.path)?.shaders;
1569
+ if (!shaders?.length)
1570
+ warnings.push(`No compiled shader for ${file.path} — push it to compile the shader.`);
1571
+ entries.push({ path: file.path, type, shaders });
1173
1572
  } else {
1174
- files[e.path] = e.text ?? "";
1573
+ entries.push({ path: file.path, type, text: readFileSync9(file.absPath, "utf8") });
1175
1574
  }
1176
1575
  }
1177
- const { code, map } = await bundleProjectWithMap(entry, files, opts.format ?? "esm");
1178
- if (opts.header === false) {
1179
- return map ? `${code}
1180
- ${inlineSourceMapComment(map)}` : code;
1181
- }
1182
- const header = buildHeader(opts.name, code, { icon: opts.icon });
1183
- if (!map)
1184
- return header + code;
1185
- const headerLines = (header.match(/\n/g) ?? []).length;
1186
- return `${header}${code}
1187
- ${inlineSourceMapComment(offsetSourceMap(map, headerLines))}`;
1188
- };
1189
- // src/compile/projectCompile.ts
1190
- var here = dirname5(fileURLToPath2(import.meta.url));
1191
- var vendoredInject = join9(here, "..", "runtime", "sdk", "inject.ts");
1192
- if (existsSync6(vendoredInject))
1193
- process.env.LECODES_SDK_INJECT = vendoredInject;
1194
- var resolveChiselBin2 = () => {
1195
- if (process.env.LECODES_CHISEL_BIN)
1196
- return process.env.LECODES_CHISEL_BIN;
1197
- try {
1198
- return createRequire3(import.meta.url)("@letary/chisel").binaryPath() ?? null;
1199
- } catch {
1200
- return null;
1201
- }
1576
+ return { entries, warnings };
1202
1577
  };
1203
- var chiselBin = resolveChiselBin2();
1204
- if (chiselBin)
1205
- process.env.LECODES_CHISEL_BIN = chiselBin;
1206
- var compileProject2 = (opts) => compileProject({
1207
- entries: opts.entries.map((e) => ({ path: e.path, type: e.type, text: e.text, fileSrc: e.fileSrc, shaders: e.shaders })),
1208
- name: opts.name,
1209
- publicUrl: opts.publicUrl,
1210
- entryOverride: opts.entryOverride,
1211
- header: opts.header,
1212
- format: opts.format
1213
- });
1214
1578
 
1215
1579
  // src/commands/preview.ts
1216
1580
  var preview = async (args) => {
@@ -1236,7 +1600,7 @@ var preview = async (args) => {
1236
1600
  if (out && isNullSink(out)) {
1237
1601
  note(`Discarded output (${out})`);
1238
1602
  } else if (out) {
1239
- writeFileSync7(resolve3(out), js);
1603
+ writeFileSync8(resolve3(out), js);
1240
1604
  note(`Wrote ${out} (${js.length} bytes)`);
1241
1605
  } else {
1242
1606
  process.stdout.write(js);
@@ -1248,9 +1612,9 @@ var preview = async (args) => {
1248
1612
  };
1249
1613
 
1250
1614
  // src/commands/render.ts
1251
- import { writeFileSync as writeFileSync8 } from "node:fs";
1615
+ import { writeFileSync as writeFileSync9 } from "node:fs";
1252
1616
  import { resolve as resolve4 } from "node:path";
1253
- var LOCAL_ASSET_SCHEME = "lecodesfile://";
1617
+ var LOCAL_ASSET_SCHEME2 = "lecodesfile://";
1254
1618
  var DEVICE_SAFE_AREA = {
1255
1619
  none: [0, 0, 0, 0],
1256
1620
  iphone: [47, 0, 34, 0],
@@ -1296,7 +1660,7 @@ var render = async (args) => {
1296
1660
  const js = await compileProject2({
1297
1661
  entries: compileEntries,
1298
1662
  name: manifest.name,
1299
- publicUrl: useLocalAssets ? LOCAL_ASSET_SCHEME : publicUrlFlag ?? apiUrl,
1663
+ publicUrl: useLocalAssets ? LOCAL_ASSET_SCHEME2 : publicUrlFlag ?? apiUrl,
1300
1664
  entryOverride: flagStr(args, "entry"),
1301
1665
  header: false,
1302
1666
  format: "iife"
@@ -1345,7 +1709,7 @@ var render = async (args) => {
1345
1709
  if (isNullSink(pngFlag)) {
1346
1710
  note(`Discarded PNG (${pngFlag})`);
1347
1711
  } else {
1348
- writeFileSync8(resolve4(pngFlag), png);
1712
+ writeFileSync9(resolve4(pngFlag), png);
1349
1713
  note(`Wrote ${pngFlag} (${png.length} bytes, ${width}x${height})`);
1350
1714
  }
1351
1715
  return done((cb) => cb());
@@ -1360,7 +1724,7 @@ var render = async (args) => {
1360
1724
  return done((cb) => cb());
1361
1725
  }
1362
1726
  if (out) {
1363
- writeFileSync8(resolve4(out), result.text);
1727
+ writeFileSync9(resolve4(out), result.text);
1364
1728
  note(`Wrote ${out} (${result.text.length} bytes)`);
1365
1729
  return done((cb) => cb());
1366
1730
  }
@@ -1371,8 +1735,8 @@ var render = async (args) => {
1371
1735
  };
1372
1736
 
1373
1737
  // src/commands/pull.ts
1374
- import { existsSync as existsSync7, mkdirSync as mkdirSync7, rmSync, writeFileSync as writeFileSync9 } from "node:fs";
1375
- import { dirname as dirname6, join as join10 } from "node:path";
1738
+ import { existsSync as existsSync8, mkdirSync as mkdirSync8, rmSync, writeFileSync as writeFileSync10 } from "node:fs";
1739
+ import { dirname as dirname6, join as join11 } from "node:path";
1376
1740
  var pull = async (args) => {
1377
1741
  const root = findProjectRoot(process.cwd());
1378
1742
  const manifest = readManifest(root);
@@ -1392,8 +1756,8 @@ var pull = async (args) => {
1392
1756
  const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a) => a.id === id)?.type !== "folder").map(([, path]) => path));
1393
1757
  for (const path of Object.keys(manifest.files)) {
1394
1758
  if (!remotePaths.has(path)) {
1395
- const abs = join10(root, ...path.replace(/^\//, "").split("/"));
1396
- if (existsSync7(abs))
1759
+ const abs = join11(root, ...path.replace(/^\//, "").split("/"));
1760
+ if (existsSync8(abs))
1397
1761
  rmSync(abs);
1398
1762
  }
1399
1763
  }
@@ -1431,18 +1795,18 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
1431
1795
  const files = { ...manifest.files };
1432
1796
  for (const asset of targets) {
1433
1797
  const key = pathById.get(asset.id);
1434
- const abs = join10(root, ...key.replace(/^\//, "").split("/"));
1435
- mkdirSync7(dirname6(abs), { recursive: true });
1798
+ const abs = join11(root, ...key.replace(/^\//, "").split("/"));
1799
+ mkdirSync8(dirname6(abs), { recursive: true });
1436
1800
  let sha;
1437
1801
  if (asset.type === "file") {
1438
1802
  if (!asset.file?.src)
1439
1803
  continue;
1440
1804
  const bytes = await downloadBlob(apiUrl, asset.file.src);
1441
- writeFileSync9(abs, bytes);
1805
+ writeFileSync10(abs, bytes);
1442
1806
  sha = sha256(bytes);
1443
1807
  } else {
1444
1808
  const text = asset.text ?? "";
1445
- writeFileSync9(abs, text);
1809
+ writeFileSync10(abs, text);
1446
1810
  sha = sha256(text);
1447
1811
  }
1448
1812
  files[key] = { id: asset.id, type: kindOf2(asset.type), sha };
@@ -1454,8 +1818,8 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
1454
1818
  };
1455
1819
 
1456
1820
  // src/commands/push.ts
1457
- import { basename as basename3 } from "node:path";
1458
- import { readFileSync as readFileSync8 } from "node:fs";
1821
+ import { basename as basename5 } from "node:path";
1822
+ import { readFileSync as readFileSync10 } from "node:fs";
1459
1823
  var push = async (args) => {
1460
1824
  const root = findProjectRoot(process.cwd());
1461
1825
  const manifest = readManifest(root);
@@ -1509,14 +1873,14 @@ var push = async (args) => {
1509
1873
  let uploaded = 0;
1510
1874
  for (const file of scan.files) {
1511
1875
  const source = sourceFor(file.path);
1512
- const kind = source?.type ?? classifyKind(basename3(file.path));
1876
+ const kind = source?.type ?? classifyKind(basename5(file.path));
1513
1877
  const entry = { path: file.path, type: kind };
1514
1878
  if (source)
1515
1879
  entry.assetId = source.id;
1516
1880
  if (isTextKind(kind)) {
1517
- entry.text = readFileSync8(file.absPath, "utf8");
1881
+ entry.text = readFileSync10(file.absPath, "utf8");
1518
1882
  } else if (!source || source.sha !== file.sha) {
1519
- const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename3(file.path), readFileSync8(file.absPath));
1883
+ const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename5(file.path), readFileSync10(file.absPath));
1520
1884
  entry.blobFileId = blob.id;
1521
1885
  uploaded++;
1522
1886
  }
@@ -1640,13 +2004,25 @@ ${c.bold("Commands:")}
1640
2004
  --settle <ms> wait for promises/timers/fetch before snapshot
1641
2005
  --wait-network let data fetches hit the real network
1642
2006
  --logs forward the app's console output (to stderr)
2007
+ design [serve] LeCodes Design — a live canvas of screens for AI-driven prototyping
2008
+ (needs the optional 'lecodes-design' package; works inside a
2009
+ cloned project or standalone in any folder)
2010
+ --port <n> dev-server port (default 4477)
2011
+ --dir <folder> design folder (default design/)
2012
+ --no-open don't open the browser
2013
+ design init Scaffold the design folder (screens/, shared/, meta.json, CLAUDE.md)
2014
+ design snapshot [screen…] Render design screens headless (needs 'lecodes-renderer')
2015
+ one screen → JSON to stdout; else files in design/.snapshots/
2016
+ --png also rasterize PNGs (needs @napi-rs/canvas)
2017
+ --out-dir <dir> output folder override
2018
+ --logs forward the screens' console output (to stderr)
1643
2019
 
1644
2020
  Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKEN).`;
1645
- var commands = { login, create, clone, status, diff, pull, push, preview, render };
2021
+ var commands = { login, create, clone, status, diff, pull, push, preview, render, design };
1646
2022
  var readVersion = () => {
1647
2023
  try {
1648
2024
  const here2 = dirname7(fileURLToPath3(import.meta.url));
1649
- return JSON.parse(readFileSync9(join11(here2, "..", "package.json"), "utf8")).version ?? "unknown";
2025
+ return JSON.parse(readFileSync11(join12(here2, "..", "package.json"), "utf8")).version ?? "unknown";
1650
2026
  } catch {
1651
2027
  return "unknown";
1652
2028
  }