lecodes-cli 0.1.0 → 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/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");
@@ -38,16 +59,6 @@ var prompt = (question, fallback) => new Promise((resolve) => {
38
59
  resolve(answer.trim() || fallback || "");
39
60
  });
40
61
  });
41
- var promptHidden = (question) => new Promise((resolve) => {
42
- const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
43
- rl.question(`${question}: `, (answer) => {
44
- rl.close();
45
- process.stdout.write(`
46
- `);
47
- resolve(answer.trim());
48
- });
49
- rl._writeToOutput = () => {};
50
- });
51
62
  var parseArgs = (argv) => {
52
63
  const out = { _: [], flags: {} };
53
64
  for (let i = 0;i < argv.length; i++) {
@@ -133,8 +144,6 @@ var apiRequest = async (apiUrl, path, opts = {}) => {
133
144
  throw new CliError(errorMessage(parsed, resp.status));
134
145
  return parsed;
135
146
  };
136
- var login = (apiUrl, login2, password) => apiRequest(apiUrl, "/account/login", { body: { login: login2, password } });
137
- var createAccessToken = (apiUrl, jwt, name) => apiRequest(apiUrl, "/account/tokens", { token: jwt, body: { name } });
138
147
  var getAccount = (apiUrl, token) => apiRequest(apiUrl, "/account", { token });
139
148
  var getProject = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}`, { token });
140
149
  var getCommits = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}/commits`, { token });
@@ -226,9 +235,11 @@ var buildPaths = (assets) => {
226
235
  }
227
236
  return pathById;
228
237
  };
229
- var materializeAssets = async (root, project, apiUrl, token) => {
238
+ var materializeAssets = async (root, project, apiUrl, token, onProgress) => {
230
239
  const pathById = buildPaths(project.assets);
231
240
  const files = {};
241
+ const total = project.assets.filter((a) => a.type !== "folder").length;
242
+ let done = 0;
232
243
  for (const asset of project.assets) {
233
244
  const key = pathById.get(asset.id);
234
245
  const abs = absOf(root, key);
@@ -250,6 +261,7 @@ var materializeAssets = async (root, project, apiUrl, token) => {
250
261
  sha = sha256(text);
251
262
  }
252
263
  files[key] = { id: asset.id, type: kindOf(asset.type), sha };
264
+ onProgress?.(++done, total, key);
253
265
  }
254
266
  return files;
255
267
  };
@@ -322,8 +334,9 @@ var parseUuid = (input) => {
322
334
  throw new CliError("Usage: lecodes clone <project-uuid|url> [directory]");
323
335
  if (!input.includes("/"))
324
336
  return input;
325
- const noQuery = input.split(/[?#]/)[0].replace(/\/+$/, "");
326
- 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);
327
340
  };
328
341
  var slug = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
329
342
  var clone = async (args) => {
@@ -331,7 +344,6 @@ var clone = async (args) => {
331
344
  const apiUrl = requireApiUrl(config);
332
345
  const token = requireToken(config);
333
346
  const uuid = parseUuid(args._[0]);
334
- info(c.dim(`Fetching project ${uuid}…`));
335
347
  const project = await getProject(apiUrl, token, uuid);
336
348
  const dir = args._[1] ?? slug(project.name);
337
349
  const root = resolve(dir);
@@ -339,8 +351,9 @@ var clone = async (args) => {
339
351
  throw new CliError(`Directory "${dir}" already exists and is not empty.`);
340
352
  }
341
353
  mkdirSync5(root, { recursive: true });
342
- info(c.dim(`Writing ${project.assets.filter((a) => a.type !== "folder").length} files…`));
343
- 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();
344
357
  if (!flagBool(args, "no-types")) {
345
358
  info(c.dim("Fetching type definitions…"));
346
359
  await materializeTypes(root, apiUrl);
@@ -348,32 +361,106 @@ var clone = async (args) => {
348
361
  }
349
362
  const commits = await getCommits(apiUrl, token, uuid);
350
363
  writeManifest(root, { uuid, apiUrl, name: project.name, baseHeadId: commits.headId, files });
351
- 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)`);
352
365
  info(c.dim(` cd ${dir} — edit, then \`lecodes push -m "your message"\``));
353
366
  };
354
367
 
355
- // src/commands/login.ts
368
+ // src/browserAuth.ts
369
+ import { createServer } from "node:http";
370
+ import { spawn } from "node:child_process";
371
+ import { randomBytes } from "node:crypto";
356
372
  import { hostname } from "node:os";
357
- var login2 = async (args) => {
358
- const config = loadConfig();
359
- const apiUrl = normalizeApiUrl(flagStr(args, "api") ?? await prompt("Server URL", config.apiUrl ?? DEFAULT_API));
360
- let token = flagStr(args, "token");
361
- if (!token) {
362
- const email = flagStr(args, "email") ?? await prompt("Email");
363
- const password = await promptHidden("Password");
364
- info(c.dim("Authenticating…"));
365
- const { accessToken } = await login(apiUrl, email, password);
366
- const created = await createAccessToken(apiUrl, accessToken, `lecodes CLI (${hostname()})`);
367
- token = created.token;
373
+ var SUCCESS_HTML = `<!doctype html><meta charset="utf-8"><title>lecodes</title>
374
+ <body style="font-family:system-ui,sans-serif;background:#0f1115;color:#e6e6e6;display:grid;place-items:center;height:100vh;margin:0">
375
+ <div style="text-align:center;max-width:420px;padding:32px">
376
+ <h1 style="font-size:20px;margin:0 0 8px">&#10003; Authorized</h1>
377
+ <p style="color:#9aa0aa;margin:0;font-size:14px">You can close this tab and return to your terminal.</p></div>`;
378
+ var errorHtml = (msg) => `<!doctype html><meta charset="utf-8"><title>lecodes</title>
379
+ <body style="font-family:system-ui,sans-serif;background:#0f1115;color:#e6e6e6;display:grid;place-items:center;height:100vh;margin:0">
380
+ <div style="text-align:center;max-width:420px;padding:32px">
381
+ <h1 style="font-size:20px;margin:0 0 8px">Authorization failed</h1>
382
+ <p style="color:#9aa0aa;margin:0;font-size:14px">${msg}</p></div>`;
383
+ var openBrowser = (url) => {
384
+ let cmd;
385
+ let args;
386
+ if (process.platform === "win32") {
387
+ cmd = "rundll32";
388
+ args = ["url.dll,FileProtocolHandler", url];
389
+ } else if (process.platform === "darwin") {
390
+ cmd = "open";
391
+ args = [url];
392
+ } else {
393
+ cmd = "xdg-open";
394
+ args = [url];
395
+ }
396
+ try {
397
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
398
+ child.on("error", () => {});
399
+ child.unref();
400
+ } catch {}
401
+ };
402
+ var browserLogin = async (webBase, timeoutMs = 5 * 60 * 1000, open = openBrowser) => {
403
+ const state = randomBytes(16).toString("hex");
404
+ let resolve2;
405
+ let reject;
406
+ const tokenPromise = new Promise((res, rej) => {
407
+ resolve2 = res;
408
+ reject = rej;
409
+ });
410
+ const server = createServer((req, res) => {
411
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
412
+ if (url.pathname !== "/") {
413
+ res.writeHead(404);
414
+ res.end();
415
+ return;
416
+ }
417
+ const token = url.searchParams.get("token");
418
+ if (!token || url.searchParams.get("state") !== state) {
419
+ res.writeHead(400, { "content-type": "text/html" });
420
+ res.end(errorHtml("Invalid or expired authorization."));
421
+ return;
422
+ }
423
+ res.writeHead(200, { "content-type": "text/html" });
424
+ res.end(SUCCESS_HTML);
425
+ resolve2(token);
426
+ });
427
+ await new Promise((res, rej) => {
428
+ server.once("error", rej);
429
+ server.listen(0, "127.0.0.1", res);
430
+ });
431
+ const port = server.address().port;
432
+ const timer = setTimeout(() => reject(new CliError("Timed out waiting for browser authorization.")), timeoutMs);
433
+ const authUrl = `${webBase}/app/cli-auth?port=${port}&state=${state}&name=${encodeURIComponent(hostname())}`;
434
+ info("Opening your browser to authorize the CLI…");
435
+ log(c.dim(" " + authUrl));
436
+ log(c.dim(" (waiting — finish in the browser, or press Ctrl+C to cancel)"));
437
+ open(authUrl);
438
+ try {
439
+ return await tokenPromise;
440
+ } finally {
441
+ clearTimeout(timer);
442
+ server.close();
368
443
  }
444
+ };
445
+
446
+ // src/commands/login.ts
447
+ var login = async (args) => {
448
+ const config = loadConfig();
449
+ const apiUrl = normalizeApiUrl(flagStr(args, "api") ?? config.apiUrl ?? DEFAULT_API);
450
+ const webBase = normalizeApiUrl(flagStr(args, "web") ?? apiUrl);
451
+ const token = flagStr(args, "token") ?? await browserLogin(webBase);
369
452
  const account = await getAccount(apiUrl, token);
370
453
  saveConfig({ apiUrl, token });
371
454
  success(`Logged in as ${c.bold(account.email)} on ${apiUrl}`);
372
455
  };
373
456
 
374
- // src/commands/pull.ts
375
- import { existsSync as existsSync3, rmSync } from "node:fs";
376
- 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";
377
464
 
378
465
  // src/localFiles.ts
379
466
  import { readdirSync as readdirSync2, readFileSync as readFileSync3 } from "node:fs";
@@ -462,7 +549,369 @@ var diffLocal = (manifest, scan) => {
462
549
  };
463
550
  var hasChanges = (diff) => diff.added.length + diff.modified.length + diff.deleted.length > 0;
464
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
+
465
912
  // src/commands/pull.ts
913
+ import { existsSync as existsSync4, rmSync } from "node:fs";
914
+ import { join as join7 } from "node:path";
466
915
  var pull = async (args) => {
467
916
  const root = findProjectRoot(process.cwd());
468
917
  const manifest = readManifest(root);
@@ -477,12 +926,13 @@ var pull = async (args) => {
477
926
  const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a) => a.id === id)?.type !== "folder").map(([, path]) => path));
478
927
  for (const path of Object.keys(manifest.files)) {
479
928
  if (!remotePaths.has(path)) {
480
- const abs = join6(root, ...path.replace(/^\//, "").split("/"));
481
- if (existsSync3(abs))
929
+ const abs = join7(root, ...path.replace(/^\//, "").split("/"));
930
+ if (existsSync4(abs))
482
931
  rmSync(abs);
483
932
  }
484
933
  }
485
- const files = await materializeAssets(root, project, apiUrl, token);
934
+ const files = await materializeAssets(root, project, apiUrl, token, progress);
935
+ endProgress();
486
936
  if (!flagBool(args, "no-types")) {
487
937
  await materializeTypes(root, apiUrl);
488
938
  writeTsconfig(root);
@@ -493,8 +943,8 @@ var pull = async (args) => {
493
943
  };
494
944
 
495
945
  // src/commands/push.ts
496
- import { basename } from "node:path";
497
- import { readFileSync as readFileSync4 } from "node:fs";
946
+ import { basename as basename2 } from "node:path";
947
+ import { readFileSync as readFileSync5 } from "node:fs";
498
948
  var push = async (args) => {
499
949
  const root = findProjectRoot(process.cwd());
500
950
  const manifest = readManifest(root);
@@ -519,14 +969,14 @@ var push = async (args) => {
519
969
  let uploaded = 0;
520
970
  for (const file of scan.files) {
521
971
  const source = sourceFor(file.path);
522
- const kind = source?.type ?? classifyKind(basename(file.path));
972
+ const kind = source?.type ?? classifyKind(basename2(file.path));
523
973
  const entry = { path: file.path, type: kind };
524
974
  if (source)
525
975
  entry.assetId = source.id;
526
976
  if (isTextKind(kind)) {
527
- entry.text = readFileSync4(file.absPath, "utf8");
977
+ entry.text = readFileSync5(file.absPath, "utf8");
528
978
  } else if (!source || source.sha !== file.sha) {
529
- 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));
530
980
  entry.blobFileId = blob.id;
531
981
  uploaded++;
532
982
  }
@@ -586,9 +1036,10 @@ var HELP = `${c.bold("lecodes")} — clone, edit and push LeCodes projects from
586
1036
  ${c.bold("Usage:")} lecodes <command> [options]
587
1037
 
588
1038
  ${c.bold("Commands:")}
589
- login Store a personal access token (prompts email/password)
590
- --token <pat> use an existing token instead
591
- --api <url> set the server URL
1039
+ login Authorize in the browser and store an access token
1040
+ --token <pat> store a token from the web UI instead
1041
+ --api <url> API origin (default https://le.codes)
1042
+ --web <url> app origin for the browser flow
592
1043
  clone <uuid|url> [dir] Download a project's files + types into a new folder
593
1044
  --no-types skip the .d.ts / tsconfig generation
594
1045
  status Show local changes vs the last sync
@@ -597,9 +1048,22 @@ ${c.bold("Commands:")}
597
1048
  push Push local changes as one new checkpoint
598
1049
  -m, --message checkpoint message
599
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
600
1064
 
601
1065
  Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKEN).`;
602
- var commands = { login: login2, clone, status, pull, push };
1066
+ var commands = { login, clone, status, pull, push, preview, render };
603
1067
  var main = async () => {
604
1068
  const argv = process.argv.slice(2);
605
1069
  const command = argv[0];