lecodes-cli 0.3.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/index.js +889 -507
- package/package.json +9 -4
- package/runtime/sdk/canvas/Canvas.ts +105 -0
- package/runtime/sdk/compile/aspectMacro.ts +42 -0
- package/runtime/sdk/compile/assetMacro.ts +44 -0
- package/runtime/sdk/compile/bundler.ts +173 -0
- package/runtime/sdk/compile/compileProject.ts +96 -0
- package/runtime/sdk/compile/header.ts +46 -0
- package/runtime/sdk/compile/index.ts +12 -0
- package/runtime/sdk/compile/sourcemap.ts +25 -0
- package/runtime/sdk/core/Aspect.ts +2 -2
- package/runtime/sdk/core/signals.ts +277 -0
- package/runtime/sdk/g2/CharacterController2D.ts +5 -4
- package/runtime/sdk/g2/Node2D.ts +1 -1
- package/runtime/sdk/g2/Physics2D.ts +22 -12
- package/runtime/sdk/g2/Scene2D.ts +12 -0
- package/runtime/sdk/g2/{Shape.ts → Shape2D.ts} +10 -10
- package/runtime/sdk/g2/Sprite.ts +7 -2
- package/runtime/sdk/g2/Texture2D.ts +4 -2
- package/runtime/sdk/g2/{Trigger.ts → Trigger2D.ts} +9 -9
- package/runtime/sdk/gl/CharacterController.ts +101 -0
- package/runtime/sdk/gl/Node.ts +11 -2
- package/runtime/sdk/gl/Physics.ts +65 -41
- package/runtime/sdk/gl/Scene.ts +18 -0
- package/runtime/sdk/gl/Shape.ts +98 -0
- package/runtime/sdk/gl/Texture.ts +2 -2
- package/runtime/sdk/gl/Trigger.ts +46 -0
- package/runtime/sdk/gl/physicsEvents.ts +20 -0
- package/runtime/sdk/inject.ts +12 -5
- package/runtime/sdk/runtime/camera.ts +31 -0
- package/runtime/sdk/runtime/device.ts +26 -0
- package/runtime/sdk/runtime/share.ts +9 -0
- package/runtime/sdk/ui/UI.ts +5 -1
- package/runtime/sdk/ui/UIButton.ts +11 -5
- package/runtime/sdk/ui/UIContainer.ts +28 -13
- package/runtime/sdk/ui/UIImage.ts +5 -0
- package/runtime/sdk/ui/UIInput.ts +10 -0
- package/runtime/sdk/ui/UINode.ts +206 -8
- package/runtime/sdk/ui/UIScreen.ts +10 -5
- package/runtime/sdk/ui/UIScrollable.ts +10 -5
- package/runtime/sdk/ui/UISpacer.ts +4 -0
- package/runtime/sdk/ui/UIText.ts +21 -5
- package/runtime/sdk/ui/UIVideo.ts +5 -0
- package/runtime/sdk/ui/UIVirtualizedList.ts +5 -0
- package/runtime/sdk/ui/UIWidget.ts +10 -5
- package/runtime/sdk/canvas/Label.ts +0 -57
- 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
|
|
7
|
-
import { dirname as dirname7, join as
|
|
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,358 @@ 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/
|
|
596
|
-
import {
|
|
597
|
-
import {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
-
|
|
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/
|
|
642
|
-
var
|
|
643
|
-
var
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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
|
-
|
|
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
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
-
|
|
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
|
-
|
|
747
|
-
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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 (
|
|
762
|
-
|
|
763
|
-
|
|
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
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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
|
+
if (opts.localAssets) {
|
|
858
|
+
const name = e.path.split(/[\\/]/).pop() ?? e.path;
|
|
859
|
+
files[e.path] = `export default ("id:" + _creatorUtils.fetchLocal(${JSON.stringify(name)}))`;
|
|
860
|
+
} else {
|
|
861
|
+
files[e.path] = `export default "${opts.publicUrl}${e.fileSrc ?? ""}"`;
|
|
862
|
+
}
|
|
863
|
+
} else if (e.type === "shader") {
|
|
864
|
+
const shaders = e.shaders ?? [];
|
|
865
|
+
if (shaders.length > 2) {
|
|
866
|
+
files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.subBackend ?? _creator.backend}")}\``;
|
|
867
|
+
} else if (shaders.length > 0) {
|
|
868
|
+
files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.backend}")}\``;
|
|
869
|
+
} else {
|
|
870
|
+
files[e.path] = `export default ""`;
|
|
871
|
+
}
|
|
872
|
+
} else if (e.path.toLowerCase().endsWith(".svg")) {
|
|
873
|
+
files[e.path] = `export default SvgSource(\`${e.text ?? ""}\`)`;
|
|
874
|
+
} else {
|
|
875
|
+
files[e.path] = e.text ?? "";
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
const { code, map } = await bundleProjectWithMap(entry, files, opts.format ?? "esm");
|
|
879
|
+
if (opts.header === false) {
|
|
880
|
+
return map ? `${code}
|
|
881
|
+
${inlineSourceMapComment(map)}` : code;
|
|
882
|
+
}
|
|
883
|
+
const header = buildHeader(opts.name, code, { icon: opts.icon });
|
|
884
|
+
if (!map)
|
|
885
|
+
return header + code;
|
|
886
|
+
const headerLines = (header.match(/\n/g) ?? []).length;
|
|
887
|
+
return `${header}${code}
|
|
888
|
+
${inlineSourceMapComment(offsetSourceMap(map, headerLines))}`;
|
|
889
|
+
};
|
|
890
|
+
// src/compile/projectCompile.ts
|
|
891
|
+
var here = dirname5(fileURLToPath2(import.meta.url));
|
|
892
|
+
var vendoredInject = join9(here, "..", "runtime", "sdk", "inject.ts");
|
|
893
|
+
if (existsSync6(vendoredInject))
|
|
894
|
+
process.env.LECODES_SDK_INJECT = vendoredInject;
|
|
895
|
+
var resolveChiselBin2 = () => {
|
|
896
|
+
if (process.env.LECODES_CHISEL_BIN)
|
|
897
|
+
return process.env.LECODES_CHISEL_BIN;
|
|
898
|
+
try {
|
|
899
|
+
return createRequire3(import.meta.url)("@letary/chisel").binaryPath() ?? null;
|
|
900
|
+
} catch {
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
var chiselBin = resolveChiselBin2();
|
|
905
|
+
if (chiselBin)
|
|
906
|
+
process.env.LECODES_CHISEL_BIN = chiselBin;
|
|
907
|
+
var compileProject2 = (opts) => compileProject({
|
|
908
|
+
entries: opts.entries.map((e) => ({ path: e.path, type: e.type, text: e.text, fileSrc: e.fileSrc, shaders: e.shaders })),
|
|
909
|
+
name: opts.name,
|
|
910
|
+
publicUrl: opts.publicUrl,
|
|
911
|
+
entryOverride: opts.entryOverride,
|
|
912
|
+
header: opts.header,
|
|
913
|
+
format: opts.format,
|
|
914
|
+
localAssets: opts.localAssets
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
// src/compile/designCompile.ts
|
|
918
|
+
var LOCAL_ASSET_SCHEME = "lecodesfile://";
|
|
919
|
+
var compileDesignScreen = async (opts) => {
|
|
920
|
+
const { root, manifest, designKey, screenId } = opts;
|
|
921
|
+
const entries = [];
|
|
922
|
+
const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest?.files ?? {})) });
|
|
923
|
+
for (const file of scan.files) {
|
|
924
|
+
const type = manifest?.files[file.path]?.type ?? classifyKind(basename(file.path));
|
|
925
|
+
if (type === "resource") {
|
|
926
|
+
entries.push({
|
|
927
|
+
path: file.path,
|
|
928
|
+
type,
|
|
929
|
+
fileSrc: opts.assets === "disk" ? encodeURIComponent(file.absPath) : encodeURI(file.path),
|
|
930
|
+
absPath: file.absPath
|
|
931
|
+
});
|
|
932
|
+
} else if (type === "shader") {
|
|
933
|
+
entries.push({ path: file.path, type, shaders: [] });
|
|
934
|
+
} else {
|
|
935
|
+
entries.push({ path: file.path, type, text: readFileSync6(file.absPath, "utf8") });
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
const entryPath = `${designKey}/.screen-entry.ts`;
|
|
939
|
+
entries.push({
|
|
940
|
+
path: entryPath,
|
|
941
|
+
type: "text",
|
|
942
|
+
text: `import screen from "./screens/${screenId}"
|
|
943
|
+
screen.open()
|
|
944
|
+
`
|
|
945
|
+
});
|
|
946
|
+
return compileProject2({
|
|
947
|
+
entries,
|
|
948
|
+
name: opts.name,
|
|
949
|
+
publicUrl: opts.assets === "disk" ? LOCAL_ASSET_SCHEME : "/project",
|
|
950
|
+
entryOverride: entryPath,
|
|
951
|
+
header: false,
|
|
952
|
+
format: "iife"
|
|
953
|
+
});
|
|
780
954
|
};
|
|
781
|
-
var statusLabel = (e) => e.status === "added" ? c.green(" added ") : e.status === "removed" ? c.red(" removed ") : c.yellow(" modified ");
|
|
782
955
|
|
|
783
956
|
// src/browserAuth.ts
|
|
784
957
|
import { createServer } from "node:http";
|
|
@@ -858,359 +1031,556 @@ var browserLogin = async (webBase, timeoutMs = 5 * 60 * 1000, open = openBrowser
|
|
|
858
1031
|
}
|
|
859
1032
|
};
|
|
860
1033
|
|
|
861
|
-
// src/commands/
|
|
862
|
-
var
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
1034
|
+
// src/commands/designTemplates.ts
|
|
1035
|
+
var META_JSON = `{
|
|
1036
|
+
"screens": {
|
|
1037
|
+
"home": { "pos": [0, 0], "desc": "Example screen — replace me" }
|
|
1038
|
+
},
|
|
1039
|
+
"edges": []
|
|
1040
|
+
}
|
|
1041
|
+
`;
|
|
1042
|
+
var TOKENS_TS = `// Design tokens shared by every screen. Colors are hex strings (the engine parses hex only).
|
|
1043
|
+
export const colors = {
|
|
1044
|
+
bg: "#101114",
|
|
1045
|
+
card: "#1c1e24",
|
|
1046
|
+
text: "#f2f3f5",
|
|
1047
|
+
muted: "#8a919e",
|
|
1048
|
+
accent: "#5b8cff",
|
|
1049
|
+
danger: "#e5484d",
|
|
1050
|
+
}
|
|
871
1051
|
|
|
872
|
-
|
|
873
|
-
import { writeFileSync as writeFileSync7 } from "node:fs";
|
|
874
|
-
import { resolve as resolve3 } from "node:path";
|
|
1052
|
+
export const spacing = { xs: 4, sm: 8, md: 16, lg: 24 }
|
|
875
1053
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
import {
|
|
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
|
-
};
|
|
1054
|
+
export const radius = { sm: 8, md: 12, lg: 20 }
|
|
1055
|
+
`;
|
|
1056
|
+
var HOME_SCREEN_TS = `import { colors, spacing, radius } from "../shared/tokens"
|
|
907
1057
|
|
|
908
|
-
//
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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";
|
|
1058
|
+
// Mock data: the domain shown on realistic values. Inline, per screen.
|
|
1059
|
+
const items = [
|
|
1060
|
+
{ title: "Design the home screen", done: true },
|
|
1061
|
+
{ title: "Sketch the flow on the canvas", done: false },
|
|
1062
|
+
{ title: "Ask Claude to add a details screen", done: false },
|
|
1063
|
+
]
|
|
919
1064
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1065
|
+
export default UIScreen([
|
|
1066
|
+
UIText("Today").style({ fontSize: 28, fontWeight: 700, color: colors.text, mt: spacing.lg }),
|
|
1067
|
+
UIText("3 tasks").style({ fontSize: 14, color: colors.muted, mt: spacing.xs }),
|
|
1068
|
+
UIColumn(items.map((item) =>
|
|
1069
|
+
UIRow([
|
|
1070
|
+
UIColumn(item.done ? [ UIText("✓").style({ fontSize: 12, color: colors.text }) ] : []).style({
|
|
1071
|
+
width: 22,
|
|
1072
|
+
height: 22,
|
|
1073
|
+
borderRadius: 11,
|
|
1074
|
+
justifyContent: "center",
|
|
1075
|
+
alignItems: "center",
|
|
1076
|
+
border: item.done ? undefined : \`2px solid \${colors.muted}\`,
|
|
1077
|
+
bgColor: item.done ? colors.accent : undefined,
|
|
1078
|
+
}),
|
|
1079
|
+
UIText(item.title).style({ fontSize: 16, color: item.done ? colors.muted : colors.text, flexGrow: 1 }),
|
|
1080
|
+
]).style({
|
|
1081
|
+
bgColor: colors.card,
|
|
1082
|
+
borderRadius: radius.md,
|
|
1083
|
+
p: spacing.md,
|
|
1084
|
+
gap: spacing.md,
|
|
1085
|
+
alignItems: "center",
|
|
1086
|
+
}),
|
|
1087
|
+
)).style({ gap: spacing.sm, mt: spacing.lg }),
|
|
1088
|
+
UISpacer(),
|
|
1089
|
+
UIButton([
|
|
1090
|
+
UIText("Add task").style({ fontSize: 16, fontWeight: 600, color: colors.text }),
|
|
1091
|
+
]).style({
|
|
1092
|
+
name: "add-task",
|
|
1093
|
+
bgColor: colors.accent,
|
|
1094
|
+
borderRadius: radius.lg,
|
|
1095
|
+
p: spacing.md,
|
|
1096
|
+
mb: spacing.lg,
|
|
1097
|
+
}),
|
|
1098
|
+
]).style({ bgColor: colors.bg, p: spacing.md, pt: "safe-top" })
|
|
1099
|
+
`;
|
|
1100
|
+
var CLAUDE_MD = `# LeCodes Design — screen prototyping canvas
|
|
1101
|
+
|
|
1102
|
+
This folder is a **design canvas** for a LeCodes app: each file in \`screens/\` is one live,
|
|
1103
|
+
executable screen shown on a spatial canvas in the browser (\`lecodes design\` runs the dev server).
|
|
1104
|
+
You (Claude) write the screens; the developer views them on the canvas and gives feedback.
|
|
1105
|
+
This is an intent-capture stage — screens run on inline mock data, before any backend exists.
|
|
1106
|
+
|
|
1107
|
+
## Read this first, then meta.json
|
|
1108
|
+
|
|
1109
|
+
\`meta.json\` is the app map: every screen (position, description) and the edges between them
|
|
1110
|
+
(navigation intent). **Read it before opening any screen file.** It is the human's file — the
|
|
1111
|
+
canvas and the developer own positions and descriptions. You MAY add entries for new screens you
|
|
1112
|
+
create (pick a free position: screens are 390×844, leave ~160px gaps) and add edges when the flow
|
|
1113
|
+
is clear, but never reposition or rewrite existing human-authored descriptions.
|
|
1114
|
+
|
|
1115
|
+
\`\`\`json
|
|
1116
|
+
{
|
|
1117
|
+
"screens": { "login": { "pos": [0, 0], "desc": "Phone login" } },
|
|
1118
|
+
"edges": [ { "from": "login", "to": "code", "label": "after phone entry", "activator": "submit" } ]
|
|
1119
|
+
}
|
|
1120
|
+
\`\`\`
|
|
1121
|
+
|
|
1122
|
+
- An edge's optional \`activator\` names the element (its \`name\` style option) that triggers the
|
|
1123
|
+
transition. Give a \`name\` to interactive elements an edge refers to — nothing else needs names.
|
|
1124
|
+
|
|
1125
|
+
## Screen rules
|
|
1126
|
+
|
|
1127
|
+
- **One file = one screen.** \`screens/<id>.ts\` default-exports an already-computed screen:
|
|
1128
|
+
\`export default UIScreen([ … ]).style({ … })\`. The filename is the screen's identity.
|
|
1129
|
+
- **Styles go through the chained \`.style({ … })\` method** — e.g.
|
|
1130
|
+
\`UIText("Today").style({ fontSize: 28, fontWeight: 700 })\`. Passing a style object as the
|
|
1131
|
+
first argument is a legacy form.
|
|
1132
|
+
- **Screens are islands.** A screen file imports ONLY from \`../shared/\` — never from another
|
|
1133
|
+
screen, and screens take no props. All relational info lives in meta.json.
|
|
1134
|
+
- **Mock data is the domain.** Put realistic inline mock data at the top of the screen file
|
|
1135
|
+
(entities + fields ARE the future schema — make them meaningful, not "Lorem ipsum").
|
|
1136
|
+
- **Reuse via shared/.** Design tokens and reusable components live in \`shared/\`. Use them in
|
|
1137
|
+
every screen. Promoting a repeated pattern into \`shared/\` is an explicit, deliberate step.
|
|
1138
|
+
- The LeCodes UI API (\`UIScreen\`, \`UIColumn\`, \`UIRow\`, \`UIBox\`, \`UIText\`, \`UIImage\`,
|
|
1139
|
+
\`UIButton\`, \`UIInput\`, \`UISpacer\`, \`UIScrollable\`, …) is **global** — no imports needed.
|
|
1140
|
+
Colors must be hex strings.
|
|
1141
|
+
|
|
1142
|
+
## See what you built
|
|
1143
|
+
|
|
1144
|
+
The developer watches the canvas live (screens hot-reload on save). To see a screen yourself:
|
|
1145
|
+
|
|
1146
|
+
\`\`\`sh
|
|
1147
|
+
lecodes design snapshot <id> # semantic JSON of the laid-out screen → stdout
|
|
1148
|
+
lecodes design snapshot <id> --png # + PNG screenshot → design/.snapshots/<id>.png
|
|
1149
|
+
lecodes design snapshot # all screens → design/.snapshots/
|
|
1150
|
+
\`\`\`
|
|
1151
|
+
|
|
1152
|
+
Snapshot after every screen you create or change, and fix what looks wrong (overflow, overlap,
|
|
1153
|
+
unreadable contrast, off-screen content) before reporting back.
|
|
1154
|
+
|
|
1155
|
+
## Workflow
|
|
1156
|
+
|
|
1157
|
+
1. Read \`meta.json\`, then the \`shared/\` signatures, then only the screens you need.
|
|
1158
|
+
2. Create/edit screen files; register new screens (and clear edges) in \`meta.json\`.
|
|
1159
|
+
3. Snapshot → verify → fix.
|
|
1160
|
+
4. When the developer edits a description or the flow on the canvas, treat meta.json as the
|
|
1161
|
+
updated spec.
|
|
1162
|
+
`;
|
|
1163
|
+
var README_HINT = `# design/
|
|
1164
|
+
|
|
1165
|
+
LeCodes Design canvas — run \`lecodes design\` here and open the printed URL.
|
|
1166
|
+
See CLAUDE.md for the conventions (it's written for AI assistants, but it's the spec).
|
|
1167
|
+
`;
|
|
1168
|
+
|
|
1169
|
+
// src/commands/design.ts
|
|
1170
|
+
var DEFAULT_DIR = "design";
|
|
1171
|
+
var DEFAULT_PORT = 4477;
|
|
1172
|
+
var getContext = (args) => {
|
|
1173
|
+
let root;
|
|
1174
|
+
let manifest;
|
|
1175
|
+
try {
|
|
1176
|
+
root = findProjectRoot(process.cwd());
|
|
1177
|
+
manifest = readManifest(root);
|
|
1178
|
+
} catch {
|
|
1179
|
+
root = process.cwd();
|
|
1180
|
+
manifest = null;
|
|
936
1181
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1182
|
+
const dirName = (flagStr(args, "dir") ?? DEFAULT_DIR).replace(/[/\\]+$/, "");
|
|
1183
|
+
return {
|
|
1184
|
+
root,
|
|
1185
|
+
manifest,
|
|
1186
|
+
name: manifest?.name ?? basename2(root),
|
|
1187
|
+
dirName,
|
|
1188
|
+
designDir: join10(root, dirName),
|
|
1189
|
+
designKey: "/" + dirName.replace(/\\/g, "/")
|
|
1190
|
+
};
|
|
943
1191
|
};
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1192
|
+
var listScreens = (designDir) => {
|
|
1193
|
+
const dir = join10(designDir, "screens");
|
|
1194
|
+
if (!existsSync7(dir))
|
|
1195
|
+
return [];
|
|
1196
|
+
return readdirSync5(dir).filter((f) => f.endsWith(".ts") && !f.startsWith(".")).map((f) => f.slice(0, -3)).sort();
|
|
1197
|
+
};
|
|
1198
|
+
var readMetaSizes = (designDir) => {
|
|
1199
|
+
try {
|
|
1200
|
+
const meta = JSON.parse(readFileSync7(join10(designDir, "meta.json"), "utf8"));
|
|
1201
|
+
return meta?.screens ?? {};
|
|
1202
|
+
} catch {
|
|
1203
|
+
return {};
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
var init = async (ctx) => {
|
|
1207
|
+
const files = [
|
|
1208
|
+
["meta.json", META_JSON],
|
|
1209
|
+
["README.md", README_HINT],
|
|
1210
|
+
["CLAUDE.md", CLAUDE_MD],
|
|
1211
|
+
[join10("shared", "tokens.ts"), TOKENS_TS],
|
|
1212
|
+
[join10("screens", "home.ts"), HOME_SCREEN_TS]
|
|
1213
|
+
];
|
|
1214
|
+
mkdirSync7(join10(ctx.designDir, "screens"), { recursive: true });
|
|
1215
|
+
mkdirSync7(join10(ctx.designDir, "shared"), { recursive: true });
|
|
1216
|
+
let created = 0;
|
|
1217
|
+
for (const [rel, content] of files) {
|
|
1218
|
+
const abs = join10(ctx.designDir, rel);
|
|
1219
|
+
if (existsSync7(abs)) {
|
|
1220
|
+
note(`kept ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
writeFileSync7(abs, content);
|
|
1224
|
+
log(`${c.green("created")} ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
|
|
1225
|
+
created++;
|
|
1226
|
+
}
|
|
1227
|
+
const ignorePath = join10(ctx.root, ".lecodesignore");
|
|
1228
|
+
const ignoreLine = `${ctx.dirName}/.snapshots/`;
|
|
1229
|
+
const current = existsSync7(ignorePath) ? readFileSync7(ignorePath, "utf8") : "";
|
|
1230
|
+
if (!current.split(/\r?\n/).includes(ignoreLine)) {
|
|
1231
|
+
appendFileSync(ignorePath, (current && !current.endsWith(`
|
|
1232
|
+
`) ? `
|
|
1233
|
+
` : "") + ignoreLine + `
|
|
1234
|
+
`);
|
|
1235
|
+
log(`${c.green("ignored")} ${ignoreLine} (in .lecodesignore)`);
|
|
1236
|
+
}
|
|
1237
|
+
log("");
|
|
1238
|
+
log(created ? "Design canvas ready." : "Design canvas already initialized.");
|
|
1239
|
+
log(` ${c.bold("lecodes design")} start the canvas (browser)`);
|
|
1240
|
+
log(` ${c.bold("lecodes design snapshot")} render screens headless (for AI feedback)`);
|
|
1241
|
+
log(` Point Claude Code at ${c.bold(ctx.dirName + "/CLAUDE.md")} — it documents the conventions.`);
|
|
1242
|
+
};
|
|
1243
|
+
var snapshot = async (ctx, args) => {
|
|
1244
|
+
const all = listScreens(ctx.designDir);
|
|
1245
|
+
if (all.length === 0)
|
|
1246
|
+
throw new CliError(`No screens in ${ctx.dirName}/screens/ — run "lecodes design init" first.`);
|
|
1247
|
+
const requested = args._.slice(1);
|
|
1248
|
+
for (const id of requested) {
|
|
1249
|
+
if (!all.includes(id))
|
|
1250
|
+
throw new CliError(`No screen "${id}" (have: ${all.join(", ")}).`);
|
|
1251
|
+
}
|
|
1252
|
+
const targets = requested.length ? requested : all;
|
|
1253
|
+
let renderer;
|
|
1254
|
+
try {
|
|
1255
|
+
renderer = await import("lecodes-renderer/headless");
|
|
1256
|
+
} catch {
|
|
1257
|
+
throw new CliError("The renderer isn't installed. Run: npm i -D lecodes-renderer");
|
|
1258
|
+
}
|
|
1259
|
+
const killer = setTimeout(() => {
|
|
1260
|
+
warnErr("Snapshot timed out after 120s.");
|
|
1261
|
+
process.exit(1);
|
|
1262
|
+
}, 120000);
|
|
1263
|
+
killer.unref?.();
|
|
1264
|
+
const png = flagBool(args, "png") || flagStr(args, "png") !== undefined;
|
|
1265
|
+
const sizes = readMetaSizes(ctx.designDir);
|
|
1266
|
+
const outDir = flagStr(args, "out-dir") ?? join10(ctx.designDir, ".snapshots");
|
|
1267
|
+
const single = targets.length === 1 && requested.length === 1 && !png && !flagStr(args, "out-dir");
|
|
1268
|
+
const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
|
|
1269
|
+
if (!single)
|
|
1270
|
+
mkdirSync7(outDir, { recursive: true });
|
|
1271
|
+
for (const id of targets) {
|
|
1272
|
+
note(`Rendering ${id}…`);
|
|
1273
|
+
const js = await compileDesignScreen({
|
|
1274
|
+
root: ctx.root,
|
|
1275
|
+
manifest: ctx.manifest,
|
|
1276
|
+
name: ctx.name,
|
|
1277
|
+
designKey: ctx.designKey,
|
|
1278
|
+
screenId: id,
|
|
1279
|
+
assets: "disk"
|
|
1280
|
+
});
|
|
1281
|
+
const [width, height] = sizes[id]?.size ?? [390, 844];
|
|
1282
|
+
const result = await renderer.renderToJson(js, { width, height, localAssets: true, onConsole });
|
|
1283
|
+
for (const w of result.warnings)
|
|
1284
|
+
warnErr(`${id}: ${w}`);
|
|
1285
|
+
if (single) {
|
|
1286
|
+
const text = result.text.endsWith(`
|
|
1287
|
+
`) ? result.text : result.text + `
|
|
1288
|
+
`;
|
|
1289
|
+
process.stdout.write(text);
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
writeFileSync7(join10(outDir, `${id}.json`), result.text);
|
|
1293
|
+
log(`${c.green("✓")} ${id} → ${join10(outDir, `${id}.json`)}`);
|
|
1294
|
+
if (png) {
|
|
1295
|
+
const pngResult = await renderer.renderToPng(js, { width, height, localAssets: true, scale: 2, onConsole });
|
|
1296
|
+
for (const w of pngResult.warnings)
|
|
1297
|
+
warnErr(`${id}: ${w}`);
|
|
1298
|
+
writeFileSync7(join10(outDir, `${id}.png`), pngResult.png);
|
|
1299
|
+
log(`${c.green("✓")} ${id} → ${join10(outDir, `${id}.png`)}`);
|
|
958
1300
|
}
|
|
959
1301
|
}
|
|
960
|
-
return last === 0 ? src : out + src.slice(last);
|
|
961
1302
|
};
|
|
962
|
-
var
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
for (;i < src.length && depth > 0; i++) {
|
|
966
|
-
const c2 = src[i];
|
|
967
|
-
if (c2 === "{")
|
|
968
|
-
depth++;
|
|
969
|
-
else if (c2 === "}")
|
|
970
|
-
depth--;
|
|
1303
|
+
var serve = async (ctx, args) => {
|
|
1304
|
+
if (!existsSync7(ctx.designDir)) {
|
|
1305
|
+
throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
|
|
971
1306
|
}
|
|
972
|
-
|
|
1307
|
+
let designServer;
|
|
1308
|
+
try {
|
|
1309
|
+
designServer = await import("lecodes-design/server");
|
|
1310
|
+
} catch {
|
|
1311
|
+
throw new CliError("The design canvas isn't installed. Run: npm i -D lecodes-design");
|
|
1312
|
+
}
|
|
1313
|
+
const port = Number(flagStr(args, "port")) || DEFAULT_PORT;
|
|
1314
|
+
const server = await designServer.startDesignServer({
|
|
1315
|
+
root: ctx.root,
|
|
1316
|
+
designDir: ctx.designDir,
|
|
1317
|
+
name: ctx.name,
|
|
1318
|
+
port,
|
|
1319
|
+
log: note,
|
|
1320
|
+
compileScreen: async (id) => ({
|
|
1321
|
+
js: await compileDesignScreen({
|
|
1322
|
+
root: ctx.root,
|
|
1323
|
+
manifest: ctx.manifest,
|
|
1324
|
+
name: ctx.name,
|
|
1325
|
+
designKey: ctx.designKey,
|
|
1326
|
+
screenId: id,
|
|
1327
|
+
assets: "server"
|
|
1328
|
+
}),
|
|
1329
|
+
warnings: []
|
|
1330
|
+
})
|
|
1331
|
+
});
|
|
1332
|
+
log(`${c.bold("LeCodes Design")} — ${ctx.name}`);
|
|
1333
|
+
log(`Canvas: ${c.bold(server.url)}`);
|
|
1334
|
+
log(`Watching: ${ctx.dirName}/ (screens hot-reload on save; Ctrl+C to stop)`);
|
|
1335
|
+
if (!flagBool(args, "no-open"))
|
|
1336
|
+
openBrowser(server.url);
|
|
1337
|
+
};
|
|
1338
|
+
var design = async (args) => {
|
|
1339
|
+
const sub = args._[0] ?? "serve";
|
|
1340
|
+
const ctx = getContext(args);
|
|
1341
|
+
if (sub === "serve")
|
|
1342
|
+
return serve(ctx, args);
|
|
1343
|
+
if (sub === "init")
|
|
1344
|
+
return init(ctx);
|
|
1345
|
+
if (sub === "snapshot")
|
|
1346
|
+
return snapshot(ctx, args);
|
|
1347
|
+
throw new CliError(`Unknown design subcommand "${sub}" (serve | init | snapshot).`);
|
|
973
1348
|
};
|
|
974
1349
|
|
|
975
|
-
//
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
const
|
|
981
|
-
const
|
|
982
|
-
|
|
983
|
-
|
|
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 === ".")
|
|
1350
|
+
// src/serverDiff.ts
|
|
1351
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
1352
|
+
import { basename as basename3 } from "node:path";
|
|
1353
|
+
var isTextAsset = (t) => t === "text" || t === "shader" || t === "scene";
|
|
1354
|
+
var computeServerDiff = (project, scan) => {
|
|
1355
|
+
const pathById = buildPaths(project.assets);
|
|
1356
|
+
const serverByPath = new Map;
|
|
1357
|
+
for (const asset of project.assets) {
|
|
1358
|
+
if (asset.type === "folder")
|
|
990
1359
|
continue;
|
|
991
|
-
|
|
992
|
-
parts.pop();
|
|
993
|
-
else
|
|
994
|
-
parts.push(part);
|
|
1360
|
+
serverByPath.set(pathById.get(asset.id), asset);
|
|
995
1361
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1362
|
+
const localByPath = new Map(scan.files.map((f) => [f.path, f]));
|
|
1363
|
+
const entries = [];
|
|
1364
|
+
const paths = new Set([...serverByPath.keys(), ...localByPath.keys()]);
|
|
1365
|
+
for (const path of [...paths].sort()) {
|
|
1366
|
+
const server = serverByPath.get(path);
|
|
1367
|
+
const local = localByPath.get(path);
|
|
1368
|
+
if (local && !server) {
|
|
1369
|
+
const text = isTextKind(classifyKind(basename3(path)));
|
|
1370
|
+
entries.push({ path, status: "added", binary: !text, serverText: "", localText: text ? readFileSync8(local.absPath, "utf8") : undefined });
|
|
1371
|
+
continue;
|
|
1372
|
+
}
|
|
1373
|
+
if (server && !local) {
|
|
1374
|
+
const text = isTextAsset(server.type);
|
|
1375
|
+
entries.push({ path, status: "removed", binary: !text, serverText: text ? server.text ?? "" : undefined, localText: "" });
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
if (server && local) {
|
|
1379
|
+
if (isTextAsset(server.type)) {
|
|
1380
|
+
const serverText = server.text ?? "";
|
|
1381
|
+
const localText = readFileSync8(local.absPath, "utf8");
|
|
1382
|
+
if (serverText !== localText)
|
|
1383
|
+
entries.push({ path, status: "modified", binary: false, serverText, localText });
|
|
1384
|
+
} else {
|
|
1385
|
+
const serverSize = server.file?.size;
|
|
1386
|
+
const localSize = Buffer.byteLength(readFileSync8(local.absPath));
|
|
1387
|
+
if (serverSize !== undefined && serverSize !== localSize) {
|
|
1388
|
+
entries.push({ path, status: "modified", binary: true });
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1004
1392
|
}
|
|
1005
|
-
return
|
|
1393
|
+
return entries;
|
|
1006
1394
|
};
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
var
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
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");
|
|
1395
|
+
|
|
1396
|
+
// src/textDiff.ts
|
|
1397
|
+
var MAX_LINES = 4000;
|
|
1398
|
+
var lcsDiff = (a, b) => {
|
|
1399
|
+
const n = a.length, m = b.length;
|
|
1400
|
+
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
1401
|
+
for (let i2 = n - 1;i2 >= 0; i2--) {
|
|
1402
|
+
for (let j2 = m - 1;j2 >= 0; j2--) {
|
|
1403
|
+
dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
|
|
1021
1404
|
}
|
|
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
1405
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1406
|
+
const ops = [];
|
|
1407
|
+
let i = 0, j = 0;
|
|
1408
|
+
while (i < n && j < m) {
|
|
1409
|
+
if (a[i] === b[j]) {
|
|
1410
|
+
ops.push({ tag: " ", line: a[i] });
|
|
1411
|
+
i++;
|
|
1412
|
+
j++;
|
|
1413
|
+
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
1414
|
+
ops.push({ tag: "-", line: a[i] });
|
|
1415
|
+
i++;
|
|
1416
|
+
} else {
|
|
1417
|
+
ops.push({ tag: "+", line: b[j] });
|
|
1418
|
+
j++;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
while (i < n)
|
|
1422
|
+
ops.push({ tag: "-", line: a[i++] });
|
|
1423
|
+
while (j < m)
|
|
1424
|
+
ops.push({ tag: "+", line: b[j++] });
|
|
1425
|
+
return ops;
|
|
1040
1426
|
};
|
|
1041
|
-
var
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1427
|
+
var splitLines = (text) => {
|
|
1428
|
+
if (text === "")
|
|
1429
|
+
return [];
|
|
1430
|
+
const lines = text.split(`
|
|
1431
|
+
`);
|
|
1432
|
+
if (lines[lines.length - 1] === "")
|
|
1433
|
+
lines.pop();
|
|
1434
|
+
return lines;
|
|
1435
|
+
};
|
|
1436
|
+
var unifiedDiff = (oldText, newText, oldLabel, newLabel, context = 3) => {
|
|
1437
|
+
if (oldText === newText)
|
|
1438
|
+
return "";
|
|
1439
|
+
const a = splitLines(oldText);
|
|
1440
|
+
const b = splitLines(newText);
|
|
1441
|
+
if (a.length + b.length > MAX_LINES) {
|
|
1442
|
+
return c.dim(` (large file — ${a.length} → ${b.length} lines, diff omitted)`);
|
|
1443
|
+
}
|
|
1444
|
+
const ops = lcsDiff(a, b);
|
|
1445
|
+
const hunks = [];
|
|
1446
|
+
let aLine = 0, bLine = 0;
|
|
1447
|
+
let cur = null;
|
|
1448
|
+
let trailingContext = 0;
|
|
1449
|
+
for (let k = 0;k < ops.length; k++) {
|
|
1450
|
+
const op = ops[k];
|
|
1451
|
+
const isChange = op.tag !== " ";
|
|
1452
|
+
if (isChange) {
|
|
1453
|
+
if (!cur) {
|
|
1454
|
+
const back = [];
|
|
1455
|
+
let bi = k - 1;
|
|
1456
|
+
while (bi >= 0 && ops[bi].tag === " " && back.length < context) {
|
|
1457
|
+
back.unshift(ops[bi]);
|
|
1458
|
+
bi--;
|
|
1459
|
+
}
|
|
1460
|
+
cur = { aStart: aLine - back.length, bStart: bLine - back.length, lines: [...back] };
|
|
1461
|
+
}
|
|
1462
|
+
cur.lines.push(op);
|
|
1463
|
+
trailingContext = 0;
|
|
1464
|
+
} else if (cur) {
|
|
1465
|
+
cur.lines.push(op);
|
|
1466
|
+
if (++trailingContext >= context * 2) {
|
|
1467
|
+
cur.lines.length -= trailingContext - context;
|
|
1468
|
+
hunks.push(cur);
|
|
1469
|
+
cur = null;
|
|
1470
|
+
trailingContext = 0;
|
|
1471
|
+
}
|
|
1052
1472
|
}
|
|
1053
|
-
|
|
1473
|
+
if (op.tag !== "+")
|
|
1474
|
+
aLine++;
|
|
1475
|
+
if (op.tag !== "-")
|
|
1476
|
+
bLine++;
|
|
1054
1477
|
}
|
|
1055
|
-
if (
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
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"}`);
|
|
1478
|
+
if (cur) {
|
|
1479
|
+
if (trailingContext > context)
|
|
1480
|
+
cur.lines.length -= trailingContext - context;
|
|
1481
|
+
hunks.push(cur);
|
|
1074
1482
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1483
|
+
const out = [c.bold(`--- ${oldLabel}`), c.bold(`+++ ${newLabel}`)];
|
|
1484
|
+
for (const h of hunks) {
|
|
1485
|
+
const aCount = h.lines.filter((o) => o.tag !== "+").length;
|
|
1486
|
+
const bCount = h.lines.filter((o) => o.tag !== "-").length;
|
|
1487
|
+
out.push(c.cyan(`@@ -${h.aStart + 1},${aCount} +${h.bStart + 1},${bCount} @@`));
|
|
1488
|
+
for (const o of h.lines) {
|
|
1489
|
+
if (o.tag === "+")
|
|
1490
|
+
out.push(c.green("+" + o.line));
|
|
1491
|
+
else if (o.tag === "-")
|
|
1492
|
+
out.push(c.red("-" + o.line));
|
|
1493
|
+
else
|
|
1494
|
+
out.push(c.dim(" " + o.line));
|
|
1495
|
+
}
|
|
1080
1496
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
if (parsed.code == null)
|
|
1084
|
-
throw new Error("chisel produced no code");
|
|
1085
|
-
return { code: parsed.code, map: parsed.map ?? null };
|
|
1497
|
+
return out.join(`
|
|
1498
|
+
`);
|
|
1086
1499
|
};
|
|
1087
|
-
var bundleProjectWithMap = async (entrypoint, files, format = "esm") => bundleProjectChisel(entrypoint, files, format, true);
|
|
1088
1500
|
|
|
1089
|
-
//
|
|
1090
|
-
var
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
|
|
1501
|
+
// src/commands/diff.ts
|
|
1502
|
+
var diff = async (args) => {
|
|
1503
|
+
const root = findProjectRoot(process.cwd());
|
|
1504
|
+
const manifest = readManifest(root);
|
|
1505
|
+
const config = loadConfig();
|
|
1506
|
+
const apiUrl = requireApiUrl(config);
|
|
1507
|
+
const token = requireToken(config);
|
|
1508
|
+
info(c.dim("Comparing with the server…"));
|
|
1509
|
+
const project = await getProject(apiUrl, token, manifest.uuid);
|
|
1510
|
+
const scan = scanProject(root, manifest);
|
|
1511
|
+
let entries = computeServerDiff(project, scan);
|
|
1512
|
+
if (args._.length > 0) {
|
|
1513
|
+
const keys = args._.map(toKey);
|
|
1514
|
+
entries = entries.filter((e) => keys.some((k) => e.path === k || e.path.startsWith(k + "/")));
|
|
1515
|
+
}
|
|
1516
|
+
if (entries.length === 0) {
|
|
1517
|
+
info(c.dim("No differences between local and server."));
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
if (flagBool(args, "name-only")) {
|
|
1521
|
+
for (const e of entries)
|
|
1522
|
+
log(statusLabel(e) + e.path + (e.binary ? c.dim(" (binary)") : ""));
|
|
1523
|
+
return;
|
|
1101
1524
|
}
|
|
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
1525
|
for (const e of entries) {
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
entry = e.path;
|
|
1147
|
-
max = e.text.length;
|
|
1148
|
-
}
|
|
1526
|
+
if (e.binary) {
|
|
1527
|
+
log(statusLabel(e) + e.path + c.dim(" (binary — content not shown)"));
|
|
1528
|
+
continue;
|
|
1149
1529
|
}
|
|
1530
|
+
const d = unifiedDiff(e.serverText ?? "", e.localText ?? "", `server${e.path}`, `local${e.path}`);
|
|
1531
|
+
if (d)
|
|
1532
|
+
log(`
|
|
1533
|
+
` + d);
|
|
1150
1534
|
}
|
|
1151
|
-
return entry;
|
|
1152
1535
|
};
|
|
1153
|
-
var
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1536
|
+
var statusLabel = (e) => e.status === "added" ? c.green(" added ") : e.status === "removed" ? c.red(" removed ") : c.yellow(" modified ");
|
|
1537
|
+
|
|
1538
|
+
// src/commands/login.ts
|
|
1539
|
+
var login = async (args) => {
|
|
1540
|
+
const config = loadConfig();
|
|
1541
|
+
const apiUrl = normalizeApiUrl(flagStr(args, "api") ?? config.apiUrl ?? DEFAULT_API);
|
|
1542
|
+
const webBase = normalizeApiUrl(flagStr(args, "web") ?? apiUrl);
|
|
1543
|
+
const token = flagStr(args, "token") ?? await browserLogin(webBase);
|
|
1544
|
+
const account = await getAccount(apiUrl, token);
|
|
1545
|
+
saveConfig({ apiUrl, token });
|
|
1546
|
+
success(`Logged in as ${c.bold(account.email)} on ${apiUrl}`);
|
|
1547
|
+
};
|
|
1548
|
+
|
|
1549
|
+
// src/commands/preview.ts
|
|
1550
|
+
import { writeFileSync as writeFileSync8 } from "node:fs";
|
|
1551
|
+
import { resolve as resolve3 } from "node:path";
|
|
1552
|
+
|
|
1553
|
+
// src/compile/collect.ts
|
|
1554
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
1555
|
+
import { basename as basename4 } from "node:path";
|
|
1556
|
+
var collectEntries = (root, manifest, project) => {
|
|
1557
|
+
const pathById = buildPaths(project.assets);
|
|
1558
|
+
const serverByPath = new Map;
|
|
1559
|
+
for (const asset of project.assets) {
|
|
1560
|
+
if (asset.type === "folder")
|
|
1561
|
+
continue;
|
|
1562
|
+
serverByPath.set(pathById.get(asset.id), { fileSrc: asset.file?.src ?? undefined, shaders: asset.shaders ?? [] });
|
|
1157
1563
|
}
|
|
1158
|
-
const
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
const
|
|
1164
|
-
if (
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
files[e.path] = `export default SvgSource(\`${e.text ?? ""}\`)`;
|
|
1564
|
+
const entries = [];
|
|
1565
|
+
const warnings = [];
|
|
1566
|
+
for (const file of scanLocal(root).files) {
|
|
1567
|
+
const type = manifest.files[file.path]?.type ?? classifyKind(basename4(file.path));
|
|
1568
|
+
if (type === "resource") {
|
|
1569
|
+
const src = serverByPath.get(file.path)?.fileSrc;
|
|
1570
|
+
if (!src)
|
|
1571
|
+
warnings.push(`No server URL for ${file.path} — push it so its URL resolves.`);
|
|
1572
|
+
entries.push({ path: file.path, type, fileSrc: src, absPath: file.absPath });
|
|
1573
|
+
} else if (type === "shader") {
|
|
1574
|
+
const shaders = serverByPath.get(file.path)?.shaders;
|
|
1575
|
+
if (!shaders?.length)
|
|
1576
|
+
warnings.push(`No compiled shader for ${file.path} — push it to compile the shader.`);
|
|
1577
|
+
entries.push({ path: file.path, type, shaders });
|
|
1173
1578
|
} else {
|
|
1174
|
-
|
|
1579
|
+
entries.push({ path: file.path, type, text: readFileSync9(file.absPath, "utf8") });
|
|
1175
1580
|
}
|
|
1176
1581
|
}
|
|
1177
|
-
|
|
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
|
-
}
|
|
1582
|
+
return { entries, warnings };
|
|
1202
1583
|
};
|
|
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
1584
|
|
|
1215
1585
|
// src/commands/preview.ts
|
|
1216
1586
|
var preview = async (args) => {
|
|
@@ -1236,7 +1606,7 @@ var preview = async (args) => {
|
|
|
1236
1606
|
if (out && isNullSink(out)) {
|
|
1237
1607
|
note(`Discarded output (${out})`);
|
|
1238
1608
|
} else if (out) {
|
|
1239
|
-
|
|
1609
|
+
writeFileSync8(resolve3(out), js);
|
|
1240
1610
|
note(`Wrote ${out} (${js.length} bytes)`);
|
|
1241
1611
|
} else {
|
|
1242
1612
|
process.stdout.write(js);
|
|
@@ -1248,9 +1618,9 @@ var preview = async (args) => {
|
|
|
1248
1618
|
};
|
|
1249
1619
|
|
|
1250
1620
|
// src/commands/render.ts
|
|
1251
|
-
import { writeFileSync as
|
|
1621
|
+
import { writeFileSync as writeFileSync9 } from "node:fs";
|
|
1252
1622
|
import { resolve as resolve4 } from "node:path";
|
|
1253
|
-
var
|
|
1623
|
+
var LOCAL_ASSET_SCHEME2 = "lecodesfile://";
|
|
1254
1624
|
var DEVICE_SAFE_AREA = {
|
|
1255
1625
|
none: [0, 0, 0, 0],
|
|
1256
1626
|
iphone: [47, 0, 34, 0],
|
|
@@ -1296,7 +1666,7 @@ var render = async (args) => {
|
|
|
1296
1666
|
const js = await compileProject2({
|
|
1297
1667
|
entries: compileEntries,
|
|
1298
1668
|
name: manifest.name,
|
|
1299
|
-
publicUrl: useLocalAssets ?
|
|
1669
|
+
publicUrl: useLocalAssets ? LOCAL_ASSET_SCHEME2 : publicUrlFlag ?? apiUrl,
|
|
1300
1670
|
entryOverride: flagStr(args, "entry"),
|
|
1301
1671
|
header: false,
|
|
1302
1672
|
format: "iife"
|
|
@@ -1345,7 +1715,7 @@ var render = async (args) => {
|
|
|
1345
1715
|
if (isNullSink(pngFlag)) {
|
|
1346
1716
|
note(`Discarded PNG (${pngFlag})`);
|
|
1347
1717
|
} else {
|
|
1348
|
-
|
|
1718
|
+
writeFileSync9(resolve4(pngFlag), png);
|
|
1349
1719
|
note(`Wrote ${pngFlag} (${png.length} bytes, ${width}x${height})`);
|
|
1350
1720
|
}
|
|
1351
1721
|
return done((cb) => cb());
|
|
@@ -1360,7 +1730,7 @@ var render = async (args) => {
|
|
|
1360
1730
|
return done((cb) => cb());
|
|
1361
1731
|
}
|
|
1362
1732
|
if (out) {
|
|
1363
|
-
|
|
1733
|
+
writeFileSync9(resolve4(out), result.text);
|
|
1364
1734
|
note(`Wrote ${out} (${result.text.length} bytes)`);
|
|
1365
1735
|
return done((cb) => cb());
|
|
1366
1736
|
}
|
|
@@ -1371,8 +1741,8 @@ var render = async (args) => {
|
|
|
1371
1741
|
};
|
|
1372
1742
|
|
|
1373
1743
|
// src/commands/pull.ts
|
|
1374
|
-
import { existsSync as
|
|
1375
|
-
import { dirname as dirname6, join as
|
|
1744
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync8, rmSync, writeFileSync as writeFileSync10 } from "node:fs";
|
|
1745
|
+
import { dirname as dirname6, join as join11 } from "node:path";
|
|
1376
1746
|
var pull = async (args) => {
|
|
1377
1747
|
const root = findProjectRoot(process.cwd());
|
|
1378
1748
|
const manifest = readManifest(root);
|
|
@@ -1392,8 +1762,8 @@ var pull = async (args) => {
|
|
|
1392
1762
|
const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a) => a.id === id)?.type !== "folder").map(([, path]) => path));
|
|
1393
1763
|
for (const path of Object.keys(manifest.files)) {
|
|
1394
1764
|
if (!remotePaths.has(path)) {
|
|
1395
|
-
const abs =
|
|
1396
|
-
if (
|
|
1765
|
+
const abs = join11(root, ...path.replace(/^\//, "").split("/"));
|
|
1766
|
+
if (existsSync8(abs))
|
|
1397
1767
|
rmSync(abs);
|
|
1398
1768
|
}
|
|
1399
1769
|
}
|
|
@@ -1431,18 +1801,18 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
|
1431
1801
|
const files = { ...manifest.files };
|
|
1432
1802
|
for (const asset of targets) {
|
|
1433
1803
|
const key = pathById.get(asset.id);
|
|
1434
|
-
const abs =
|
|
1435
|
-
|
|
1804
|
+
const abs = join11(root, ...key.replace(/^\//, "").split("/"));
|
|
1805
|
+
mkdirSync8(dirname6(abs), { recursive: true });
|
|
1436
1806
|
let sha;
|
|
1437
1807
|
if (asset.type === "file") {
|
|
1438
1808
|
if (!asset.file?.src)
|
|
1439
1809
|
continue;
|
|
1440
1810
|
const bytes = await downloadBlob(apiUrl, asset.file.src);
|
|
1441
|
-
|
|
1811
|
+
writeFileSync10(abs, bytes);
|
|
1442
1812
|
sha = sha256(bytes);
|
|
1443
1813
|
} else {
|
|
1444
1814
|
const text = asset.text ?? "";
|
|
1445
|
-
|
|
1815
|
+
writeFileSync10(abs, text);
|
|
1446
1816
|
sha = sha256(text);
|
|
1447
1817
|
}
|
|
1448
1818
|
files[key] = { id: asset.id, type: kindOf2(asset.type), sha };
|
|
@@ -1454,8 +1824,8 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
|
1454
1824
|
};
|
|
1455
1825
|
|
|
1456
1826
|
// src/commands/push.ts
|
|
1457
|
-
import { basename as
|
|
1458
|
-
import { readFileSync as
|
|
1827
|
+
import { basename as basename5 } from "node:path";
|
|
1828
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
1459
1829
|
var push = async (args) => {
|
|
1460
1830
|
const root = findProjectRoot(process.cwd());
|
|
1461
1831
|
const manifest = readManifest(root);
|
|
@@ -1509,14 +1879,14 @@ var push = async (args) => {
|
|
|
1509
1879
|
let uploaded = 0;
|
|
1510
1880
|
for (const file of scan.files) {
|
|
1511
1881
|
const source = sourceFor(file.path);
|
|
1512
|
-
const kind = source?.type ?? classifyKind(
|
|
1882
|
+
const kind = source?.type ?? classifyKind(basename5(file.path));
|
|
1513
1883
|
const entry = { path: file.path, type: kind };
|
|
1514
1884
|
if (source)
|
|
1515
1885
|
entry.assetId = source.id;
|
|
1516
1886
|
if (isTextKind(kind)) {
|
|
1517
|
-
entry.text =
|
|
1887
|
+
entry.text = readFileSync10(file.absPath, "utf8");
|
|
1518
1888
|
} else if (!source || source.sha !== file.sha) {
|
|
1519
|
-
const blob = await uploadBlob(apiUrl, token, manifest.uuid,
|
|
1889
|
+
const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename5(file.path), readFileSync10(file.absPath));
|
|
1520
1890
|
entry.blobFileId = blob.id;
|
|
1521
1891
|
uploaded++;
|
|
1522
1892
|
}
|
|
@@ -1640,13 +2010,25 @@ ${c.bold("Commands:")}
|
|
|
1640
2010
|
--settle <ms> wait for promises/timers/fetch before snapshot
|
|
1641
2011
|
--wait-network let data fetches hit the real network
|
|
1642
2012
|
--logs forward the app's console output (to stderr)
|
|
2013
|
+
design [serve] LeCodes Design — a live canvas of screens for AI-driven prototyping
|
|
2014
|
+
(needs the optional 'lecodes-design' package; works inside a
|
|
2015
|
+
cloned project or standalone in any folder)
|
|
2016
|
+
--port <n> dev-server port (default 4477)
|
|
2017
|
+
--dir <folder> design folder (default design/)
|
|
2018
|
+
--no-open don't open the browser
|
|
2019
|
+
design init Scaffold the design folder (screens/, shared/, meta.json, CLAUDE.md)
|
|
2020
|
+
design snapshot [screen…] Render design screens headless (needs 'lecodes-renderer')
|
|
2021
|
+
one screen → JSON to stdout; else files in design/.snapshots/
|
|
2022
|
+
--png also rasterize PNGs (needs @napi-rs/canvas)
|
|
2023
|
+
--out-dir <dir> output folder override
|
|
2024
|
+
--logs forward the screens' console output (to stderr)
|
|
1643
2025
|
|
|
1644
2026
|
Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKEN).`;
|
|
1645
|
-
var commands = { login, create, clone, status, diff, pull, push, preview, render };
|
|
2027
|
+
var commands = { login, create, clone, status, diff, pull, push, preview, render, design };
|
|
1646
2028
|
var readVersion = () => {
|
|
1647
2029
|
try {
|
|
1648
2030
|
const here2 = dirname7(fileURLToPath3(import.meta.url));
|
|
1649
|
-
return JSON.parse(
|
|
2031
|
+
return JSON.parse(readFileSync11(join12(here2, "..", "package.json"), "utf8")).version ?? "unknown";
|
|
1650
2032
|
} catch {
|
|
1651
2033
|
return "unknown";
|
|
1652
2034
|
}
|