lecodes-cli 0.1.1 → 0.1.2

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 CHANGED
@@ -22,6 +22,7 @@ cd myapp
22
22
  # …edit files in VS Code (IntelliSense works out of the box)…
23
23
  lecodes status # see what changed
24
24
  lecodes push -m "Tweak the menu" # push as a new checkpoint
25
+ lecodes preview > app.js # compile the local project to JS
25
26
  ```
26
27
 
27
28
  ## Commands
@@ -33,6 +34,8 @@ lecodes push -m "Tweak the menu" # push as a new checkpoint
33
34
  | `status` | Show added / modified / moved / deleted files vs the last sync. |
34
35
  | `pull` | Overwrite local files with the project's current remote state. `--force` to discard local edits. |
35
36
  | `push` | Push all local changes as **one** new checkpoint. `-m`/`--message` for the message, `--force` to override the "server moved on" guard. |
37
+ | `preview` | Compile the local project to a single JS bundle, printed to **stdout** (pipeable). `-o`/`--out <file>` to write a file, `--entry <path>` to override the entrypoint, `--no-header` to omit the metadata header, `--public-url <url>` for the asset URL prefix. Diagnostics go to stderr. |
38
+ | `render` | Render the local **UI** project. By default emits a semantic JSON description (great for AI/automated checks) to stdout or `-o`/`--out <file>`; `--png [file]` rasterizes a PNG (default `screenshot.png`, `--scale <n>` for DPR). `--width`/`--height` set the viewport (default 390×844). Requires the optional `lecodes-render` package (`npm i -D lecodes-render`); PNG also needs `@napi-rs/canvas`. 3D scenes aren't rendered. |
36
39
 
37
40
  ## How it works
38
41
 
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
2
4
 
3
5
  // src/commands/clone.ts
4
6
  import { existsSync as existsSync2, mkdirSync as mkdirSync5, readdirSync } from "node:fs";
@@ -21,6 +23,25 @@ var log = (msg = "") => process.stdout.write(msg + `
21
23
  `);
22
24
  var info = (msg) => log(msg);
23
25
  var success = (msg) => log(c.green("✓ ") + msg);
26
+ var logErr = (msg = "") => process.stderr.write(msg + `
27
+ `);
28
+ var note = (msg) => logErr(c.dim(msg));
29
+ var warnErr = (msg) => logErr(c.yellow("! ") + msg);
30
+ var ellipsize = (s, max) => s.length <= max ? s : "…" + s.slice(s.length - max + 1);
31
+ var progress = (done, total, label = "") => {
32
+ if (!process.stdout.isTTY)
33
+ return;
34
+ const pct = total > 0 ? Math.round(done / total * 100) : 100;
35
+ process.stdout.clearLine(0);
36
+ process.stdout.cursorTo(0);
37
+ process.stdout.write(` ${c.cyan(`${pct}`.padStart(3) + "%")} ${c.dim(`(${done}/${total})`)} ${c.dim(ellipsize(label, 48))}`);
38
+ };
39
+ var endProgress = () => {
40
+ if (process.stdout.isTTY)
41
+ process.stdout.write(`
42
+ `);
43
+ };
44
+
24
45
  class CliError extends Error {
25
46
  }
26
47
  var sha256 = (data) => createHash("sha256").update(data).digest("hex");
@@ -214,9 +235,11 @@ var buildPaths = (assets) => {
214
235
  }
215
236
  return pathById;
216
237
  };
217
- var materializeAssets = async (root, project, apiUrl, token) => {
238
+ var materializeAssets = async (root, project, apiUrl, token, onProgress) => {
218
239
  const pathById = buildPaths(project.assets);
219
240
  const files = {};
241
+ const total = project.assets.filter((a) => a.type !== "folder").length;
242
+ let done = 0;
220
243
  for (const asset of project.assets) {
221
244
  const key = pathById.get(asset.id);
222
245
  const abs = absOf(root, key);
@@ -238,6 +261,7 @@ var materializeAssets = async (root, project, apiUrl, token) => {
238
261
  sha = sha256(text);
239
262
  }
240
263
  files[key] = { id: asset.id, type: kindOf(asset.type), sha };
264
+ onProgress?.(++done, total, key);
241
265
  }
242
266
  return files;
243
267
  };
@@ -310,8 +334,9 @@ var parseUuid = (input) => {
310
334
  throw new CliError("Usage: lecodes clone <project-uuid|url> [directory]");
311
335
  if (!input.includes("/"))
312
336
  return input;
313
- const noQuery = input.split(/[?#]/)[0].replace(/\/+$/, "");
314
- return noQuery.slice(noQuery.lastIndexOf("/") + 1);
337
+ const clean = input.split(/[?#]/)[0].replace(/\/+$/, "");
338
+ const match = clean.match(/\/(?:projects|view)\/([^/]+)/);
339
+ return match ? match[1] : clean.slice(clean.lastIndexOf("/") + 1);
315
340
  };
316
341
  var slug = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
317
342
  var clone = async (args) => {
@@ -319,7 +344,6 @@ var clone = async (args) => {
319
344
  const apiUrl = requireApiUrl(config);
320
345
  const token = requireToken(config);
321
346
  const uuid = parseUuid(args._[0]);
322
- info(c.dim(`Fetching project ${uuid}…`));
323
347
  const project = await getProject(apiUrl, token, uuid);
324
348
  const dir = args._[1] ?? slug(project.name);
325
349
  const root = resolve(dir);
@@ -327,8 +351,9 @@ var clone = async (args) => {
327
351
  throw new CliError(`Directory "${dir}" already exists and is not empty.`);
328
352
  }
329
353
  mkdirSync5(root, { recursive: true });
330
- info(c.dim(`Writing ${project.assets.filter((a) => a.type !== "folder").length} files…`));
331
- const files = await materializeAssets(root, project, apiUrl, token);
354
+ info(c.dim(`Fetching project ${c.bold(project.name)}…`));
355
+ const files = await materializeAssets(root, project, apiUrl, token, progress);
356
+ endProgress();
332
357
  if (!flagBool(args, "no-types")) {
333
358
  info(c.dim("Fetching type definitions…"));
334
359
  await materializeTypes(root, apiUrl);
@@ -336,7 +361,7 @@ var clone = async (args) => {
336
361
  }
337
362
  const commits = await getCommits(apiUrl, token, uuid);
338
363
  writeManifest(root, { uuid, apiUrl, name: project.name, baseHeadId: commits.headId, files });
339
- success(`Cloned ${c.bold(project.name)} into ${c.bold(dir)}/`);
364
+ success(`Cloned ${c.bold(project.name)} into ${c.bold(dir)}/ (${Object.keys(files).length} files)`);
340
365
  info(c.dim(` cd ${dir} — edit, then \`lecodes push -m "your message"\``));
341
366
  };
342
367
 
@@ -429,9 +454,13 @@ var login = async (args) => {
429
454
  success(`Logged in as ${c.bold(account.email)} on ${apiUrl}`);
430
455
  };
431
456
 
432
- // src/commands/pull.ts
433
- import { existsSync as existsSync3, rmSync } from "node:fs";
434
- import { join as join6 } from "node:path";
457
+ // src/commands/preview.ts
458
+ import { writeFileSync as writeFileSync5 } from "node:fs";
459
+ import { resolve as resolve2 } from "node:path";
460
+
461
+ // src/compile/collect.ts
462
+ import { readFileSync as readFileSync4 } from "node:fs";
463
+ import { basename } from "node:path";
435
464
 
436
465
  // src/localFiles.ts
437
466
  import { readdirSync as readdirSync2, readFileSync as readFileSync3 } from "node:fs";
@@ -520,7 +549,369 @@ var diffLocal = (manifest, scan) => {
520
549
  };
521
550
  var hasChanges = (diff) => diff.added.length + diff.modified.length + diff.deleted.length > 0;
522
551
 
552
+ // src/compile/collect.ts
553
+ var collectEntries = (root, manifest, project) => {
554
+ const pathById = buildPaths(project.assets);
555
+ const serverByPath = new Map;
556
+ for (const asset of project.assets) {
557
+ if (asset.type === "folder")
558
+ continue;
559
+ serverByPath.set(pathById.get(asset.id), { fileSrc: asset.file?.src ?? undefined, shaders: asset.shaders ?? [] });
560
+ }
561
+ const entries = [];
562
+ const warnings = [];
563
+ for (const file of scanLocal(root).files) {
564
+ const type = manifest.files[file.path]?.type ?? classifyKind(basename(file.path));
565
+ if (type === "resource") {
566
+ const src = serverByPath.get(file.path)?.fileSrc;
567
+ if (!src)
568
+ warnings.push(`No server URL for ${file.path} — push it so its URL resolves.`);
569
+ entries.push({ path: file.path, type, fileSrc: src });
570
+ } else if (type === "shader") {
571
+ const shaders = serverByPath.get(file.path)?.shaders;
572
+ if (!shaders?.length)
573
+ warnings.push(`No compiled shader for ${file.path} — push it to compile the shader.`);
574
+ entries.push({ path: file.path, type, shaders });
575
+ } else {
576
+ entries.push({ path: file.path, type, text: readFileSync4(file.absPath, "utf8") });
577
+ }
578
+ }
579
+ return { entries, warnings };
580
+ };
581
+
582
+ // src/compile/bundler.ts
583
+ import * as esbuild from "esbuild";
584
+ import { existsSync as existsSync3 } from "node:fs";
585
+ import { dirname as dirname3, join as join6 } from "node:path";
586
+ import { fileURLToPath } from "node:url";
587
+
588
+ // src/compile/assetMacro.ts
589
+ var ASSET_RE = /\basset\(\s*(["'])((?:[^"'\\]|\\.)*)\1\s*\)/g;
590
+ var hasAssetMacro = (src) => src.includes("asset(");
591
+ var desugarAssetMacros = (src, filePath) => {
592
+ const byPath = new Map;
593
+ let i = 0;
594
+ const out = src.replace(ASSET_RE, (_match, _quote, path) => {
595
+ let name = byPath.get(path);
596
+ if (!name) {
597
+ name = `__asset_${i++}`;
598
+ byPath.set(path, name);
599
+ }
600
+ return name;
601
+ });
602
+ if (/\basset\(/.test(out)) {
603
+ throw new Error(`asset() expects a string literal, found a dynamic argument in: ${filePath}`);
604
+ }
605
+ if (byPath.size === 0)
606
+ return src;
607
+ const header = [...byPath].map(([path, name]) => `import ${name} from ${JSON.stringify(path)};`).join(`
608
+ `);
609
+ return header + `
610
+ ` + out;
611
+ };
612
+
613
+ // src/compile/bundler.ts
614
+ var GL_MATRIX_HEADER = `
615
+ import * as glMatrix from "gl-matrix/common";
616
+ import * as mat2 from "gl-matrix/mat2";
617
+ import * as mat2d from "gl-matrix/mat2d";
618
+ import * as mat3 from "gl-matrix/mat3";
619
+ import * as mat4 from "gl-matrix/mat4";
620
+ import * as quat from "gl-matrix/quat";
621
+ import * as quat2 from "gl-matrix/quat2";
622
+ import * as vec2 from "gl-matrix/vec2";
623
+ import * as vec3 from "gl-matrix/vec3";
624
+ import * as vec4 from "gl-matrix/vec4";
625
+ `;
626
+ var resolveRuntime = () => {
627
+ const here = dirname3(fileURLToPath(import.meta.url));
628
+ const vendored = join6(here, "..", "runtime");
629
+ if (existsSync3(join6(vendored, "worker", "creator.js"))) {
630
+ const w = join6(vendored, "worker");
631
+ return {
632
+ alias: {
633
+ creator: join6(w, "creator.js"),
634
+ ui: join6(w, "ui.js"),
635
+ animate: join6(w, "animate.js"),
636
+ utils: join6(w, "utils.js"),
637
+ "gl-matrix": join6(vendored, "gl-matrix")
638
+ },
639
+ inject: [join6(w, "globals.js")]
640
+ };
641
+ }
642
+ const repoRoot = join6(here, "..", "..", "..", "..");
643
+ const ws = join6(repoRoot, "packages", "worker", "src");
644
+ if (existsSync3(join6(ws, "index.ts"))) {
645
+ return {
646
+ alias: {
647
+ creator: join6(ws, "index.ts"),
648
+ ui: join6(ws, "ui.ts"),
649
+ animate: join6(ws, "animate.ts"),
650
+ utils: join6(ws, "utils.ts"),
651
+ "gl-matrix": join6(repoRoot, "packages", "backend", "src", "assets", "dependencies", "gl-matrix")
652
+ },
653
+ inject: [join6(ws, "globals.ts")]
654
+ };
655
+ }
656
+ throw new CliError("Could not locate the compile runtime — try reinstalling lecodes-cli.");
657
+ };
658
+ function normalize(path) {
659
+ const parts = [];
660
+ for (const part of path.split("/")) {
661
+ if (!part || part === ".")
662
+ continue;
663
+ if (part === "..")
664
+ parts.pop();
665
+ else
666
+ parts.push(part);
667
+ }
668
+ return "/" + parts.join("/");
669
+ }
670
+ function dirOf(path) {
671
+ const i = path.lastIndexOf("/");
672
+ return i <= 0 ? "/" : path.slice(0, i);
673
+ }
674
+ var hasExtension = (path) => /\.[a-zA-Z0-9]+$/.test(path);
675
+ function resolveBase(importer, specifier) {
676
+ if (specifier.startsWith("/"))
677
+ return normalize(specifier);
678
+ if (specifier.startsWith("./") || specifier.startsWith("../"))
679
+ return normalize(dirOf(importer) + "/" + specifier);
680
+ throw new Error(`Bare import is not allowed here: ${specifier}`);
681
+ }
682
+ function resolveWithExtensions(files, importer, specifier) {
683
+ const base = resolveBase(importer, specifier);
684
+ if (hasExtension(base)) {
685
+ if (files.has(base))
686
+ return base;
687
+ throw new Error(`Module not found: ${base}`);
688
+ }
689
+ for (const candidate of [base, `${base}.ts`, `${base}.js`]) {
690
+ if (files.has(candidate))
691
+ return candidate;
692
+ }
693
+ throw new Error(`Module not found: ${specifier} from ${importer}`);
694
+ }
695
+ var getLoader = (path) => path.endsWith(".ts") ? "ts" : "js";
696
+ async function buildInMemory(entrypoint, inputFiles, format = "esm") {
697
+ const runtime = resolveRuntime();
698
+ const files = new Map(Object.entries(inputFiles).map(([k, v]) => [normalize(k), v]));
699
+ const entry = normalize(entrypoint);
700
+ if (!files.has(entry))
701
+ throw new CliError(`Entrypoint not found: ${entry}`);
702
+ const plugin = {
703
+ name: "memfs",
704
+ setup(build2) {
705
+ build2.onResolve({ filter: /^entry$/ }, () => ({ path: entry, namespace: "mem" }));
706
+ build2.onResolve({ filter: /.*/, namespace: "mem" }, (args) => {
707
+ const isRelative = args.path.startsWith("./") || args.path.startsWith("../");
708
+ const isAbsolute = args.path.startsWith("/");
709
+ if (!isRelative && !isAbsolute)
710
+ return null;
711
+ try {
712
+ return { path: resolveWithExtensions(files, args.importer || entry, args.path), namespace: "mem" };
713
+ } catch (error) {
714
+ return { errors: [{ text: error instanceof Error ? error.message : String(error) }] };
715
+ }
716
+ });
717
+ build2.onLoad({ filter: /.*/, namespace: "mem" }, (args) => {
718
+ const contents = files.get(args.path);
719
+ if (contents == null)
720
+ return { errors: [{ text: `Module not found: ${args.path}` }] };
721
+ const finalContents = /\.[tj]sx?$/.test(args.path) && hasAssetMacro(contents) ? desugarAssetMacros(contents, args.path) : contents;
722
+ return { contents: finalContents, loader: getLoader(args.path), resolveDir: dirOf(args.path) };
723
+ });
724
+ }
725
+ };
726
+ const result = await esbuild.build({
727
+ stdin: { contents: `import "entry";`, loader: "js", sourcefile: "bootstrap.js" },
728
+ alias: runtime.alias,
729
+ define: { RAD2DEG: "57.29577951308232", DEG2RAD: "0.017453292519943295" },
730
+ inject: runtime.inject,
731
+ bundle: true,
732
+ write: false,
733
+ format,
734
+ platform: "browser",
735
+ plugins: [plugin],
736
+ charset: "utf8"
737
+ });
738
+ const output = result.outputFiles?.[0];
739
+ if (!output)
740
+ throw new CliError("Compilation produced no output");
741
+ return output.text;
742
+ }
743
+
744
+ // src/compile/projectCompile.ts
745
+ var buildHeader = (name, code) => {
746
+ let header = `// name: ${name.slice(0, 20)}
747
+ `;
748
+ header += `// last-updated: ${new Date().toISOString()}
749
+ `;
750
+ const localAssets = new Set;
751
+ for (const m of code.matchAll(/_creatorUtils\.fetchLocal\((.+?)\)/g)) {
752
+ localAssets.add(m[1].replaceAll(/['"]/g, "").trim());
753
+ }
754
+ if (/_creator\.createGlb/.test(code))
755
+ localAssets.add("uberarchive");
756
+ if (/_creator\.captureImage/.test(code))
757
+ localAssets.add("capture");
758
+ if (localAssets.size > 0)
759
+ header += `// preload: ${[...localAssets].join(";")}
760
+ `;
761
+ if (!code.includes("_creator.createScene") && !code.includes("_creator.createEntity"))
762
+ header += `// gl: disable
763
+ `;
764
+ else if (code.includes("_creator.createEngine"))
765
+ header += `// gl: defer
766
+ `;
767
+ if (code.includes("_creator.createARController"))
768
+ header += "// ar: enable";
769
+ header += `
770
+ `;
771
+ return header;
772
+ };
773
+ var detectEntry = (entries) => {
774
+ let entry = "";
775
+ let max = -1;
776
+ for (const e of entries) {
777
+ const ext2 = e.path.split(".").at(-1)?.toLowerCase();
778
+ if (e.type === "text" && e.text !== undefined && !e.text.includes("export") && (ext2 === "ts" || ext2 === "js")) {
779
+ if (e.text.length > max) {
780
+ entry = e.path;
781
+ max = e.text.length;
782
+ }
783
+ }
784
+ }
785
+ return entry;
786
+ };
787
+ var compileProject = async (opts) => {
788
+ const entry = opts.entryOverride ?? detectEntry(opts.entries);
789
+ if (!entry) {
790
+ throw new CliError("Couldn't find an entrypoint (a top-level .ts/.js file with no `export`). Pass --entry <path>.");
791
+ }
792
+ const files = {};
793
+ for (const e of opts.entries) {
794
+ if (e.type === "resource") {
795
+ files[e.path] = `export default "${opts.publicUrl}${e.fileSrc ?? ""}"`;
796
+ } else if (e.type === "shader") {
797
+ const shaders = e.shaders ?? [];
798
+ if (shaders.length > 2) {
799
+ files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.subBackend ?? _creator.backend}")}\``;
800
+ } else if (shaders.length > 0) {
801
+ files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.backend}")}\``;
802
+ } else {
803
+ files[e.path] = `export default ""`;
804
+ }
805
+ } else if (e.path.toLowerCase().endsWith(".svg")) {
806
+ files[e.path] = `export default SvgSource(\`${e.text ?? ""}\`)`;
807
+ } else {
808
+ files[e.path] = GL_MATRIX_HEADER + (e.text ?? "");
809
+ }
810
+ }
811
+ const code = await buildInMemory(entry, files, opts.format ?? "esm");
812
+ return opts.header === false ? code : buildHeader(opts.name, code) + code;
813
+ };
814
+
815
+ // src/commands/preview.ts
816
+ var preview = async (args) => {
817
+ const root = findProjectRoot(process.cwd());
818
+ const manifest = readManifest(root);
819
+ const config = loadConfig();
820
+ const apiUrl = requireApiUrl(config);
821
+ const token = requireToken(config);
822
+ note("Resolving project assets…");
823
+ const project = await getProject(apiUrl, token, manifest.uuid);
824
+ const { entries, warnings } = collectEntries(root, manifest, project);
825
+ note("Compiling…");
826
+ const js = await compileProject({
827
+ entries,
828
+ name: manifest.name,
829
+ publicUrl: flagStr(args, "public-url") ?? apiUrl,
830
+ entryOverride: flagStr(args, "entry"),
831
+ header: !flagBool(args, "no-header")
832
+ });
833
+ for (const w of warnings)
834
+ warnErr(w);
835
+ const out = flagStr(args, "o", "out");
836
+ if (out) {
837
+ writeFileSync5(resolve2(out), js);
838
+ note(`Wrote ${out} (${js.length} bytes)`);
839
+ } else {
840
+ process.stdout.write(js);
841
+ if (!js.endsWith(`
842
+ `))
843
+ process.stdout.write(`
844
+ `);
845
+ }
846
+ };
847
+
848
+ // src/commands/render.ts
849
+ import { writeFileSync as writeFileSync6 } from "node:fs";
850
+ import { resolve as resolve3 } from "node:path";
851
+ var render = async (args) => {
852
+ const root = findProjectRoot(process.cwd());
853
+ const manifest = readManifest(root);
854
+ const config = loadConfig();
855
+ const apiUrl = requireApiUrl(config);
856
+ const token = requireToken(config);
857
+ note("Resolving project assets…");
858
+ const project = await getProject(apiUrl, token, manifest.uuid);
859
+ const { entries, warnings } = collectEntries(root, manifest, project);
860
+ note("Compiling…");
861
+ const js = await compileProject({
862
+ entries,
863
+ name: manifest.name,
864
+ publicUrl: flagStr(args, "public-url") ?? apiUrl,
865
+ entryOverride: flagStr(args, "entry"),
866
+ header: false,
867
+ format: "iife"
868
+ });
869
+ if (js.includes("_creator.createScene") || js.includes("_creator.createEntity")) {
870
+ warnings.push("This looks like a 3D/GL project — only the UI layer is rendered.");
871
+ }
872
+ let renderer;
873
+ try {
874
+ renderer = await import("lecodes-render");
875
+ } catch {
876
+ throw new CliError("The renderer isn't installed. Run: npm i -D lecodes-render");
877
+ }
878
+ const width = Number(flagStr(args, "width")) || 390;
879
+ const height = Number(flagStr(args, "height")) || 844;
880
+ const pngFlag = flagStr(args, "png") ?? (flagBool(args, "png") ? "screenshot.png" : undefined);
881
+ if (pngFlag !== undefined) {
882
+ note("Rendering PNG…");
883
+ const { png, warnings: pngWarnings } = await renderer.renderToPng(js, {
884
+ width,
885
+ height,
886
+ scale: Number(flagStr(args, "scale")) || 2
887
+ });
888
+ const pngPath = resolve3(pngFlag);
889
+ writeFileSync6(pngPath, png);
890
+ for (const w of [...warnings, ...pngWarnings])
891
+ warnErr(w);
892
+ note(`Wrote ${pngFlag} (${png.length} bytes, ${width}x${height})`);
893
+ return;
894
+ }
895
+ note("Rendering…");
896
+ const result = await renderer.renderToJson(js, { width, height });
897
+ for (const w of [...warnings, ...result.warnings])
898
+ warnErr(w);
899
+ const out = flagStr(args, "o", "out");
900
+ if (out) {
901
+ writeFileSync6(resolve3(out), result.text);
902
+ note(`Wrote ${out} (${result.text.length} bytes)`);
903
+ } else {
904
+ process.stdout.write(result.text);
905
+ if (!result.text.endsWith(`
906
+ `))
907
+ process.stdout.write(`
908
+ `);
909
+ }
910
+ };
911
+
523
912
  // src/commands/pull.ts
913
+ import { existsSync as existsSync4, rmSync } from "node:fs";
914
+ import { join as join7 } from "node:path";
524
915
  var pull = async (args) => {
525
916
  const root = findProjectRoot(process.cwd());
526
917
  const manifest = readManifest(root);
@@ -535,12 +926,13 @@ var pull = async (args) => {
535
926
  const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a) => a.id === id)?.type !== "folder").map(([, path]) => path));
536
927
  for (const path of Object.keys(manifest.files)) {
537
928
  if (!remotePaths.has(path)) {
538
- const abs = join6(root, ...path.replace(/^\//, "").split("/"));
539
- if (existsSync3(abs))
929
+ const abs = join7(root, ...path.replace(/^\//, "").split("/"));
930
+ if (existsSync4(abs))
540
931
  rmSync(abs);
541
932
  }
542
933
  }
543
- const files = await materializeAssets(root, project, apiUrl, token);
934
+ const files = await materializeAssets(root, project, apiUrl, token, progress);
935
+ endProgress();
544
936
  if (!flagBool(args, "no-types")) {
545
937
  await materializeTypes(root, apiUrl);
546
938
  writeTsconfig(root);
@@ -551,8 +943,8 @@ var pull = async (args) => {
551
943
  };
552
944
 
553
945
  // src/commands/push.ts
554
- import { basename } from "node:path";
555
- import { readFileSync as readFileSync4 } from "node:fs";
946
+ import { basename as basename2 } from "node:path";
947
+ import { readFileSync as readFileSync5 } from "node:fs";
556
948
  var push = async (args) => {
557
949
  const root = findProjectRoot(process.cwd());
558
950
  const manifest = readManifest(root);
@@ -577,14 +969,14 @@ var push = async (args) => {
577
969
  let uploaded = 0;
578
970
  for (const file of scan.files) {
579
971
  const source = sourceFor(file.path);
580
- const kind = source?.type ?? classifyKind(basename(file.path));
972
+ const kind = source?.type ?? classifyKind(basename2(file.path));
581
973
  const entry = { path: file.path, type: kind };
582
974
  if (source)
583
975
  entry.assetId = source.id;
584
976
  if (isTextKind(kind)) {
585
- entry.text = readFileSync4(file.absPath, "utf8");
977
+ entry.text = readFileSync5(file.absPath, "utf8");
586
978
  } else if (!source || source.sha !== file.sha) {
587
- const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename(file.path), readFileSync4(file.absPath));
979
+ const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename2(file.path), readFileSync5(file.absPath));
588
980
  entry.blobFileId = blob.id;
589
981
  uploaded++;
590
982
  }
@@ -656,9 +1048,22 @@ ${c.bold("Commands:")}
656
1048
  push Push local changes as one new checkpoint
657
1049
  -m, --message checkpoint message
658
1050
  --force push even if the server moved on
1051
+ preview Compile the local project to JS (prints to stdout)
1052
+ -o, --out <file> write to a file instead of stdout
1053
+ --entry <path> override the entrypoint
1054
+ --no-header omit the metadata header
1055
+ --public-url <url> asset URL prefix (default: the API origin)
1056
+ render Render the local UI project to a semantic JSON description
1057
+ (needs the optional 'lecodes-render' package; UI projects only)
1058
+ -o, --out <file> write JSON to a file instead of stdout
1059
+ --png [file] rasterize a PNG (default screenshot.png;
1060
+ needs @napi-rs/canvas)
1061
+ --scale <n> PNG device-pixel scale (default 2)
1062
+ --width/--height viewport size (default 390x844)
1063
+ --entry <path> override the entrypoint
659
1064
 
660
1065
  Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKEN).`;
661
- var commands = { login, clone, status, pull, push };
1066
+ var commands = { login, clone, status, pull, push, preview, render };
662
1067
  var main = async () => {
663
1068
  const argv = process.argv.slice(2);
664
1069
  const command = argv[0];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lecodes-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Clone, edit and push LeCodes projects from your machine.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -13,15 +13,31 @@
13
13
  },
14
14
  "files": [
15
15
  "dist",
16
+ "runtime",
16
17
  "README.md"
17
18
  ],
18
19
  "engines": {
19
20
  "node": ">=18"
20
21
  },
21
22
  "scripts": {
22
- "build": "bun build src/index.ts --target=node --outfile dist/index.js --banner=\"#!/usr/bin/env node\"",
23
+ "vendor-runtime": "bun scripts/vendor-runtime.ts",
24
+ "build": "bun scripts/vendor-runtime.ts && bun build src/index.ts --target=node --outfile dist/index.js --external esbuild --external lecodes-render --banner=\"#!/usr/bin/env node\"",
23
25
  "typecheck": "tsc --noEmit",
24
26
  "dev": "bun run src/index.ts",
25
27
  "prepublishOnly": "bun run build"
28
+ },
29
+ "dependencies": {
30
+ "esbuild": "^0.27.3"
31
+ },
32
+ "peerDependencies": {
33
+ "lecodes-render": "*"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "lecodes-render": {
37
+ "optional": true
38
+ }
39
+ },
40
+ "devDependencies": {
41
+ "lecodes-render": "workspace:*"
26
42
  }
27
43
  }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Common utilities
3
+ * @module glMatrix
4
+ */
5
+
6
+ // Configuration Constants
7
+ export var EPSILON = 0.000001;
8
+ export var RANDOM = Math.random;
9
+ export var ANGLE_ORDER = "zyx";
10
+
11
+ /**
12
+ * Symmetric round
13
+ * see https://www.npmjs.com/package/round-half-up-symmetric#user-content-detailed-background
14
+ *
15
+ * @param {Number} a value to round
16
+ */
17
+ export function round(a) {
18
+ if (a >= 0) return Math.round(a);
19
+ return a % 0.5 === 0 ? Math.floor(a) : Math.round(a);
20
+ }
21
+
22
+
23
+ /**
24
+ * Convert Degree To Radian
25
+ *
26
+ * @param {Number} a Angle in Degrees
27
+ */
28
+ export function toRadian(a) {
29
+ return a * 0.017453292519943295;
30
+ }
31
+
32
+ /**
33
+ * Convert Radian To Degree
34
+ *
35
+ * @param {Number} a Angle in Radians
36
+ */
37
+ export function toDegree(a) {
38
+ return a * 57.29577951308232;
39
+ }
40
+
41
+ /**
42
+ * Tests whether or not the arguments have approximately the same value, within an absolute
43
+ * or relative tolerance of glMatrix.EPSILON (an absolute tolerance is used for values less
44
+ * than or equal to 1.0, and a relative tolerance is used for larger values)
45
+ *
46
+ * @param {Number} a The first number to test.
47
+ * @param {Number} b The second number to test.
48
+ * @param {Number} tolerance Absolute or relative tolerance (default glMatrix.EPSILON)
49
+ * @returns {Boolean} True if the numbers are approximately equal, false otherwise.
50
+ */
51
+ export function equals(a, b) {
52
+ var tolerance = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : EPSILON;
53
+ return Math.abs(a - b) <= tolerance * Math.max(1, Math.abs(a), Math.abs(b));
54
+ }
@@ -0,0 +1,12 @@
1
+ import * as glMatrix from "gl-matrix/common";
2
+ import * as mat2 from "gl-matrix/mat2";
3
+ import * as mat2d from "gl-matrix/mat2d";
4
+ import * as mat3 from "gl-matrix/mat3";
5
+ import * as mat4 from "gl-matrix/mat4";
6
+ import * as quat from "gl-matrix/quat";
7
+ import * as quat2 from "gl-matrix/quat2";
8
+ import * as vec2 from "gl-matrix/vec2";
9
+ import * as vec3 from "gl-matrix/vec3";
10
+ import * as vec4 from "gl-matrix/vec4";
11
+
12
+ export { glMatrix, mat2, mat2d, mat3, mat4, quat, quat2, vec2, vec3, vec4 };
@@ -0,0 +1,11 @@
1
+ import * as glMatrix from "./common.js";
2
+ import * as mat2 from "./mat2.js";
3
+ import * as mat2d from "./mat2d.js";
4
+ import * as mat3 from "./mat3.js";
5
+ import * as mat4 from "./mat4.js";
6
+ import * as quat from "./quat.js";
7
+ import * as quat2 from "./quat2.js";
8
+ import * as vec2 from "./vec2.js";
9
+ import * as vec3 from "./vec3.js";
10
+ import * as vec4 from "./vec4.js";
11
+ export { glMatrix, mat2, mat2d, mat3, mat4, quat, quat2, vec2, vec3, vec4 };