@genex-ai/cli-demo 0.7.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +13 -12
  2. package/dist/index.js +410 -154
  3. package/package.json +10 -11
  4. package/templates/README.md +8 -7
  5. package/templates/skills/genex-ai-model/SKILL.md +5 -5
  6. package/templates/skills/genex-ai-sfx/SKILL.md +1 -1
  7. package/templates/skills/genex-ai-skybox/SKILL.md +2 -2
  8. package/templates/skills/genex-ai-texture/SKILL.md +1 -1
  9. package/templates/skills/genex-getting-started/SKILL.md +10 -6
  10. package/templates/skills/genex-threejs-embed-auth/SKILL.md +174 -0
  11. package/templates/skills/genex-threejs-multiplayer/SKILL.md +193 -100
  12. package/templates/skills/genex-threejs-multiplayer/references/genre-recipes.md +115 -0
  13. package/templates/skills/genex-threejs-multiplayer/references/realtime-patterns.md +138 -123
  14. package/templates/skills/genex-threejs-precipitation-surfaces/SKILL.md +59 -0
  15. package/templates/skills/genex-threejs-precipitation-surfaces/references/precipitation-surfaces.md +181 -0
  16. package/templates/skills/genex-threejs-procedural-architecture/SKILL.md +2 -1
  17. package/templates/skills/genex-threejs-procedural-architecture/references/architecture-systems.md +1 -1
  18. package/templates/skills/genex-threejs-procedural-fields/SKILL.md +0 -5
  19. package/templates/skills/genex-threejs-procedural-geometry/SKILL.md +0 -5
  20. package/templates/skills/genex-threejs-procedural-materials/SKILL.md +2 -17
  21. package/templates/skills/genex-threejs-procedural-vegetation/SKILL.md +11 -1
  22. package/templates/skills/genex-threejs-procedural-vfx/SKILL.md +3 -1
  23. package/templates/skills/genex-threejs-shadow-systems/SKILL.md +0 -5
  24. package/templates/skills/genex-threejs-skill-router/SKILL.md +10 -5
  25. package/templates/skills/genex-threejs-skill-router/references/routing-map.md +9 -6
  26. package/templates/skills/genex-threejs-spectral-ocean/SKILL.md +11 -1
  27. package/templates/skills/genex-threejs-temporal-surfaces/SKILL.md +3 -1
  28. package/templates/skills/genex-threejs-water-optics/SKILL.md +14 -2
package/dist/index.js CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { readFileSync } from "fs";
5
- import path10 from "path";
5
+ import path12 from "path";
6
6
  import { fileURLToPath as fileURLToPath2 } from "url";
7
7
 
8
8
  // src/commands/init.ts
9
- import path6 from "path";
9
+ import path7 from "path";
10
10
 
11
11
  // src/config.ts
12
12
  import fs from "fs";
@@ -125,7 +125,7 @@ import http from "http";
125
125
  import crypto from "crypto";
126
126
  import readline from "readline";
127
127
  import { spawn } from "child_process";
128
- import { URL } from "url";
128
+ import { URL as URL2 } from "url";
129
129
  var SUCCESS_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>Genex</title>
130
130
  <style>body{font-family:system-ui,sans-serif;background:#0b0b0f;color:#eaeaea;display:grid;place-items:center;height:100vh;margin:0}
131
131
  .card{text-align:center;padding:2rem 3rem;border:1px solid #26262e;border-radius:14px;background:#13131a}
@@ -216,7 +216,7 @@ async function authorize(authBaseUrl, options) {
216
216
  });
217
217
  }
218
218
  function buildAuthUrl(authBaseUrl, redirectUri, state) {
219
- const url = new URL(`${authBaseUrl}/cli/auth`);
219
+ const url = new URL2(`${authBaseUrl}/cli/auth`);
220
220
  url.searchParams.set("redirect_uri", redirectUri);
221
221
  url.searchParams.set("state", state);
222
222
  return url.toString();
@@ -225,7 +225,7 @@ async function handleCallback(req, res, expectedState, log) {
225
225
  res.setHeader("Access-Control-Allow-Origin", "*");
226
226
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
227
227
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
228
- const reqUrl = new URL(req.url ?? "/", "http://127.0.0.1");
228
+ const reqUrl = new URL2(req.url ?? "/", "http://127.0.0.1");
229
229
  if (req.method === "OPTIONS") {
230
230
  res.writeHead(204);
231
231
  res.end();
@@ -438,7 +438,10 @@ async function createDraftProject(opts) {
438
438
  apiUrl,
439
439
  colyseusUrl,
440
440
  playUrl: project.playUrl ?? void 0,
441
- status: "draft"
441
+ status: "draft",
442
+ // Derived client-side from the auth/web origin the CLI just authorized
443
+ // against (respects --auth-url overrides) — no API contract change.
444
+ dashboardOrigins: [new URL(dashboardUrl).origin]
442
445
  };
443
446
  }
444
447
  log.warn("Couldn't create a project with a unique name. Try `--name <unique>`.");
@@ -639,6 +642,70 @@ async function writeProject(meta, cwd = process.cwd()) {
639
642
  return { path: file };
640
643
  }
641
644
 
645
+ // src/lib/game-config.ts
646
+ import fs6 from "fs/promises";
647
+ import path6 from "path";
648
+ var DEFAULT_DASHBOARD_ORIGIN = new URL(DEFAULT_AUTH_URL).origin;
649
+ function renderGenexConfig() {
650
+ return `// src/genex.config.ts \u2014 written by \`genex init\`. DO NOT hardcode URLs here.
651
+ //
652
+ // Values come from Vite env:
653
+ // .env -> VITE_GENEX_SLUG (this game's identity; committed)
654
+ // .env.development.local -> local-stack URL overrides (dev mode ONLY \u2014 a
655
+ // production build always uses the defaults below;
656
+ // gitignored via *.local, never committed)
657
+ export const GENEX = {
658
+ slug: import.meta.env.VITE_GENEX_SLUG as string,
659
+ apiUrl: (import.meta.env.VITE_GENEX_API_URL as string | undefined) ?? "${DEFAULT_API_URL}",
660
+ colyseusUrl: (import.meta.env.VITE_GENEX_COLYSEUS_URL as string | undefined) ?? "${DEFAULT_COLYSEUS_URL}",
661
+ dashboardOrigins: ((import.meta.env.VITE_GENEX_DASHBOARD_ORIGINS as string | undefined) ?? "${DEFAULT_DASHBOARD_ORIGIN}").split(","),
662
+ } as const;
663
+ `;
664
+ }
665
+ function renderSlugEnv(slug) {
666
+ return `# Genex game identity \u2014 committed; environment-independent.
667
+ VITE_GENEX_SLUG=${slug}
668
+ `;
669
+ }
670
+ function renderDevOverrides(meta) {
671
+ const dashboardOrigins = meta.dashboardOrigins?.join(",") ?? DEFAULT_DASHBOARD_ORIGIN;
672
+ const overrides = [];
673
+ if (meta.apiUrl !== DEFAULT_API_URL) overrides.push(`VITE_GENEX_API_URL=${meta.apiUrl}`);
674
+ if (meta.colyseusUrl !== DEFAULT_COLYSEUS_URL) {
675
+ overrides.push(`VITE_GENEX_COLYSEUS_URL=${meta.colyseusUrl}`);
676
+ }
677
+ if (dashboardOrigins !== DEFAULT_DASHBOARD_ORIGIN) {
678
+ overrides.push(`VITE_GENEX_DASHBOARD_ORIGINS=${dashboardOrigins}`);
679
+ }
680
+ if (overrides.length === 0) return null;
681
+ return `# Local-stack overrides \u2014 DEV MODE ONLY (vite build ignores this file).
682
+ # Written by \`genex init\` because it ran against a non-default stack.
683
+ ${overrides.join("\n")}
684
+ `;
685
+ }
686
+ async function writeIfAbsent(file, content, log) {
687
+ try {
688
+ await fs6.writeFile(file, content, { flag: "wx" });
689
+ log.dim(` wrote ${c.cyan(file)}`);
690
+ return true;
691
+ } catch (err) {
692
+ if (err.code === "EEXIST") {
693
+ log.dim(` kept existing ${c.cyan(file)}`);
694
+ return false;
695
+ }
696
+ throw err;
697
+ }
698
+ }
699
+ async function writeGameConfigFiles(meta, log, cwd = process.cwd()) {
700
+ await fs6.mkdir(path6.join(cwd, "src"), { recursive: true });
701
+ await writeIfAbsent(path6.join(cwd, "src", "genex.config.ts"), renderGenexConfig(), log);
702
+ await writeIfAbsent(path6.join(cwd, ".env"), renderSlugEnv(meta.slug), log);
703
+ const overrides = renderDevOverrides(meta);
704
+ if (overrides) {
705
+ await writeIfAbsent(path6.join(cwd, ".env.development.local"), overrides, log);
706
+ }
707
+ }
708
+
642
709
  // src/utils/logger.ts
643
710
  function createLogger(opts = {}) {
644
711
  const out = (s) => {
@@ -671,8 +738,8 @@ async function runInit(opts) {
671
738
  let totalNew = 0;
672
739
  let totalUpdated = 0;
673
740
  for (const t of targets) {
674
- const src = t.full ? templatesDir : path6.join(templatesDir, "skills");
675
- const dest = t.full ? t.baseDir : path6.join(t.baseDir, "skills");
741
+ const src = t.full ? templatesDir : path7.join(templatesDir, "skills");
742
+ const dest = t.full ? t.baseDir : path7.join(t.baseDir, "skills");
676
743
  const { copied, updated } = await copyTemplates(src, dest, { force: opts.force });
677
744
  const added = copied.length - updated.length;
678
745
  totalNew += added;
@@ -716,7 +783,7 @@ async function runInit(opts) {
716
783
  }
717
784
  const apiUrl = getApiUrl(opts.apiUrl);
718
785
  const colyseusUrl = getColyseusUrl(opts.colyseusUrl);
719
- const projectName = opts.name?.trim() || path6.basename(process.cwd());
786
+ const projectName = opts.name?.trim() || path7.basename(process.cwd());
720
787
  const meta = await createDraftProject({
721
788
  apiUrl,
722
789
  token,
@@ -729,16 +796,22 @@ async function runInit(opts) {
729
796
  if (meta) {
730
797
  const { path: metaPath } = await writeProject(meta);
731
798
  log.dim(` saved ${c.cyan(metaPath)}`);
799
+ await writeGameConfigFiles(meta, log);
732
800
  }
733
801
  log.plain("");
734
802
  log.success("All set. \u{1F680}");
735
803
  }
736
804
 
805
+ // src/commands/publish.ts
806
+ import fs8 from "fs/promises";
807
+ import path9 from "path";
808
+
737
809
  // src/lib/deploy.ts
738
810
  import { spawn as spawn4 } from "child_process";
739
- import fs6 from "fs/promises";
811
+ import crypto3 from "crypto";
812
+ import fs7 from "fs/promises";
740
813
  import os2 from "os";
741
- import path7 from "path";
814
+ import path8 from "path";
742
815
  function run(cmd, args, env) {
743
816
  return new Promise((resolve) => {
744
817
  let child;
@@ -756,7 +829,8 @@ function run(cmd, args, env) {
756
829
  child.on("close", (code2) => resolve({ code: code2 ?? -1, out, err }));
757
830
  });
758
831
  }
759
- async function deployGame(sshUrl, opts, log) {
832
+ var EXCLUDE_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".genex", "dist"]);
833
+ async function deployGame(ctx, opts, log) {
760
834
  const cwd = process.cwd();
761
835
  if (!opts.noBuild && await hasBuildScript(cwd)) {
762
836
  log.step("Building the production bundle\u2026");
@@ -769,25 +843,164 @@ async function deployGame(sshUrl, opts, log) {
769
843
  }
770
844
  log.success("Built.");
771
845
  }
772
- const distDir = path7.join(cwd, "dist");
846
+ const distDir = path8.join(cwd, "dist");
773
847
  const siteDir = await isDir2(distDir) ? distDir : cwd;
774
- if (siteDir === cwd) {
775
- await writeGitignore(cwd, log);
848
+ const rel = path8.relative(cwd, siteDir) || ".";
849
+ if (siteDir === cwd) await writeGitignore(cwd, log);
850
+ const files = await collectFiles(siteDir);
851
+ if (files.some((f) => /(^|\/)genex_key(\.pub)?$/.test(f.relPath))) {
852
+ log.error(`Refusing to deploy: ${KEY_NAME} is in ${rel}. Add it to .gitignore and retry.`);
853
+ return false;
854
+ }
855
+ if (files.length === 0) {
856
+ log.warn("Nothing to deploy \u2014 the build produced no files.");
857
+ return false;
858
+ }
859
+ if (!files.some((f) => f.relPath === "index.html")) {
860
+ log.error(`No index.html in ${rel} \u2014 a game needs one to serve. Nothing deployed.`);
861
+ return false;
862
+ }
863
+ const commit = contentCommit(files);
864
+ log.step("Requesting an upload token\u2026");
865
+ const grant = await getUploadToken(ctx, commit, log);
866
+ if (!grant) return false;
867
+ const totalBytes = files.reduce((n, f) => n + f.bytes.length, 0);
868
+ const mb = (n) => (n / 1048576).toFixed(n < 1048576 ? 2 : 1);
869
+ if (files.length > grant.limits.maxFiles) {
870
+ log.error(`Too many files (${files.length} > ${grant.limits.maxFiles}). Trim the build and retry.`);
871
+ return false;
872
+ }
873
+ const oversized = files.find((f) => f.bytes.length > grant.limits.maxFileBytes);
874
+ if (oversized) {
875
+ log.error(`${oversized.relPath} is ${mb(oversized.bytes.length)} MB \u2014 over the ${mb(grant.limits.maxFileBytes)} MB per-file limit.`);
876
+ return false;
877
+ }
878
+ if (totalBytes > grant.limits.maxTotalBytes) {
879
+ log.error(`Build is ${mb(totalBytes)} MB \u2014 over the ${mb(grant.limits.maxTotalBytes)} MB limit.`);
880
+ return false;
881
+ }
882
+ log.step(`Uploading ${files.length} files\u2026`);
883
+ if (!await uploadAll(grant.uploadUrl, grant.token, files, log)) {
884
+ log.error("Couldn't upload your game \u2014 please try again.");
885
+ return false;
886
+ }
887
+ const keyPath = path8.resolve(cwd, KEY_NAME);
888
+ if (!await fileExists(keyPath)) {
889
+ log.error(`No deploy key (${KEY_NAME}) here \u2014 run \`genex init\` in this folder first.`);
890
+ return false;
776
891
  }
777
- const rel = path7.relative(cwd, siteDir) || ".";
892
+ if (!await pushSource(cwd, ctx.sshUrl, keyPath, log)) return false;
893
+ log.step("Publishing\u2026");
894
+ if (!await callPublish(ctx, commit, log)) return false;
895
+ const index = files.find((f) => f.relPath === "index.html");
896
+ await waitUntilLive(grant.playUrl, fingerprintOf(index.bytes.toString("utf8")), opts.liveTimeoutMs ?? 2e4, log);
897
+ return true;
898
+ }
899
+ async function hasBuildScript(cwd) {
778
900
  try {
779
- await fs6.access(path7.join(siteDir, "index.html"));
901
+ const pkg = JSON.parse(await fs7.readFile(path8.join(cwd, "package.json"), "utf8"));
902
+ return Boolean(pkg.scripts?.build);
780
903
  } catch {
781
- log.warn(`No index.html in ${rel} \u2014 GitHub Pages needs one to serve the game.`);
904
+ return false;
782
905
  }
783
- await warnIfAbsolutePaths(siteDir, log);
784
- await fs6.writeFile(path7.join(siteDir, ".nojekyll"), "");
785
- const keyPath = path7.resolve(cwd, KEY_NAME);
786
- if (!await fileExists(keyPath)) {
787
- log.warn(`No deploy key (${KEY_NAME}) here \u2014 run \`genex init\` in this folder first.`);
906
+ }
907
+ async function collectFiles(root) {
908
+ const out = [];
909
+ const walk2 = async (dir, prefix) => {
910
+ for (const e of await fs7.readdir(dir, { withFileTypes: true })) {
911
+ const relPath = prefix ? `${prefix}/${e.name}` : e.name;
912
+ if (e.isDirectory()) {
913
+ if (!EXCLUDE_DIRS.has(e.name)) await walk2(path8.join(dir, e.name), relPath);
914
+ } else if (e.isFile() && e.name !== KEY_NAME && e.name !== `${KEY_NAME}.pub`) {
915
+ out.push({ relPath, bytes: await fs7.readFile(path8.join(dir, e.name)) });
916
+ }
917
+ }
918
+ };
919
+ await walk2(root, "");
920
+ return out;
921
+ }
922
+ function contentCommit(files) {
923
+ const h = crypto3.createHash("sha1");
924
+ for (const f of [...files].sort((a, b) => a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0)) {
925
+ h.update(f.relPath, "utf8");
926
+ h.update("\0");
927
+ h.update(f.bytes);
928
+ }
929
+ return h.digest("hex");
930
+ }
931
+ async function getUploadToken(ctx, commit, log) {
932
+ let res;
933
+ try {
934
+ res = await fetch(`${ctx.apiUrl}/api/games/${ctx.projectId}/upload-token`, {
935
+ method: "POST",
936
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${ctx.token}` },
937
+ body: JSON.stringify({ commit })
938
+ });
939
+ } catch (err) {
940
+ log.error(`Couldn't reach the API at ${ctx.apiUrl}: ${String(err)}`);
941
+ return null;
942
+ }
943
+ if (!res.ok) {
944
+ log.error(`Couldn't get an upload token (HTTP ${res.status}).`);
945
+ return null;
946
+ }
947
+ return await res.json();
948
+ }
949
+ async function uploadAll(uploadUrl, uploadToken, files, log) {
950
+ const base = uploadUrl.replace(/\/+$/, "");
951
+ let failed = false;
952
+ let next = 0;
953
+ const worker = async () => {
954
+ while (!failed) {
955
+ const i = next++;
956
+ if (i >= files.length) return;
957
+ const f = files[i];
958
+ const encoded = f.relPath.split("/").map(encodeURIComponent).join("/");
959
+ let res = null;
960
+ try {
961
+ res = await fetch(`${base}/${encoded}`, {
962
+ method: "PUT",
963
+ headers: { Authorization: `Bearer ${uploadToken}` },
964
+ body: f.bytes
965
+ });
966
+ } catch {
967
+ res = null;
968
+ }
969
+ if (!res || !res.ok) {
970
+ failed = true;
971
+ log.warn(`Upload failed for ${f.relPath}${res ? ` (HTTP ${res.status})` : " (network error)"}.`);
972
+ }
973
+ }
974
+ };
975
+ await Promise.all(Array.from({ length: Math.min(8, files.length) }, () => worker()));
976
+ return !failed;
977
+ }
978
+ async function callPublish(ctx, commit, log) {
979
+ let res;
980
+ try {
981
+ res = await fetch(`${ctx.apiUrl}/api/games/${ctx.projectId}/publish`, {
982
+ method: "POST",
983
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${ctx.token}` },
984
+ body: JSON.stringify({ commit })
985
+ });
986
+ } catch (err) {
987
+ log.error(`Couldn't reach the API at ${ctx.apiUrl}: ${String(err)}`);
988
+ return false;
989
+ }
990
+ if (!res.ok) {
991
+ const detail = await res.text().catch(() => "");
992
+ log.error(`Publish failed (HTTP ${res.status})${detail ? `: ${detail}` : ""}.`);
788
993
  return false;
789
994
  }
790
- const gitDir = await fs6.mkdtemp(path7.join(os2.tmpdir(), "genex-deploy-"));
995
+ return true;
996
+ }
997
+ async function pushSource(cwd, sshUrl, keyPath, log) {
998
+ const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
999
+ const failed = () => {
1000
+ log.error("Couldn't save your game's source \u2014 please try again.");
1001
+ return false;
1002
+ };
1003
+ const gitDir = await fs7.mkdtemp(path8.join(os2.tmpdir(), "genex-source-"));
791
1004
  const base = { GIT_DIR: gitDir };
792
1005
  const ident = {
793
1006
  GIT_AUTHOR_NAME: "genex",
@@ -795,121 +1008,65 @@ async function deployGame(sshUrl, opts, log) {
795
1008
  GIT_COMMITTER_NAME: "genex",
796
1009
  GIT_COMMITTER_EMAIL: "agent@genex.local"
797
1010
  };
798
- const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
799
1011
  try {
800
- if ((await run("git", ["init", "-q"], base)).code !== 0) {
801
- log.warn("git init failed \u2014 the game was not pushed.");
802
- return false;
803
- }
804
- await fs6.writeFile(
805
- path7.join(gitDir, "info", "exclude"),
1012
+ if ((await run("git", ["init", "-q"], base)).code !== 0) return failed();
1013
+ await fs7.writeFile(
1014
+ path8.join(gitDir, "info", "exclude"),
806
1015
  ["node_modules/", "dist/", ".git/", KEY_NAME, `${KEY_NAME}.pub`, ".genex/", ""].join("\n")
807
1016
  );
808
- const stage = async (workTree, indexName) => {
809
- const env = { ...base, GIT_WORK_TREE: workTree, GIT_INDEX_FILE: path7.join(gitDir, indexName) };
810
- await run("git", ["add", "-A"], env);
811
- const tracked = await run("git", ["ls-files"], env);
812
- if (/(^|\/)genex_key(\.pub)?$/m.test(tracked.out)) throw new Error("KEY_STAGED");
813
- if (!tracked.out.trim()) return null;
814
- const tree = (await run("git", ["write-tree"], env)).out.trim();
815
- return tree || null;
816
- };
817
- const commitTree = async (tree, message) => (await run("git", ["commit-tree", tree, "-m", message], { ...base, ...ident })).out.trim();
818
- let mainTree;
819
- try {
820
- mainTree = await stage(siteDir, "index-main");
821
- } catch {
822
- log.error(`Refusing to deploy: ${KEY_NAME} is staged. Add it to .gitignore and retry.`);
1017
+ const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE: path8.join(gitDir, "index-source") };
1018
+ await run("git", ["add", "-A"], env);
1019
+ const tracked = await run("git", ["ls-files"], env);
1020
+ if (/(^|\/)genex_key(\.pub)?$/m.test(tracked.out)) {
1021
+ log.error(`Refusing to publish: ${KEY_NAME} is in your project. Add it to .gitignore and retry.`);
823
1022
  return false;
824
1023
  }
825
- if (!mainTree || mainTree === EMPTY_TREE) {
826
- log.warn("Nothing to commit \u2014 the build produced no files.");
1024
+ const tree = tracked.out.trim() ? (await run("git", ["write-tree"], env)).out.trim() : "";
1025
+ if (!tree || tree === EMPTY_TREE) {
1026
+ log.error("Nothing to publish \u2014 the project has no files.");
827
1027
  return false;
828
1028
  }
829
- await run("git", ["update-ref", "refs/heads/main", await commitTree(mainTree, "build")], base);
830
- let pushSource = false;
831
- if (siteDir !== cwd) {
832
- try {
833
- const srcTree = await stage(cwd, "index-source");
834
- if (srcTree && srcTree !== EMPTY_TREE) {
835
- await run("git", ["update-ref", "refs/heads/source", await commitTree(srcTree, "source")], base);
836
- pushSource = true;
837
- }
838
- } catch {
839
- log.dim(" (Couldn't publish source for remixing \u2014 the deploy key was in the way.)");
840
- }
841
- }
842
- log.step("Pushing your game over SSH\u2026");
843
- const refspecs = ["+refs/heads/main:main"];
844
- if (pushSource) refspecs.push("+refs/heads/source:source");
845
- const push = await run(
846
- "git",
847
- ["push", "-q", ...pushSource ? ["--atomic"] : [], sshUrl, ...refspecs],
848
- {
849
- ...base,
850
- // Quote the key path: git splits GIT_SSH_COMMAND with shell-like rules, so a
851
- // project folder with a space (e.g. "fly drone") otherwise breaks the `-i`
852
- // argument and ssh fails with "Could not resolve hostname …".
853
- GIT_SSH_COMMAND: `ssh -i "${keyPath}" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new`
854
- }
855
- );
856
- if (push.code === 0) {
857
- log.success("Pushed.");
858
- if (opts.playUrl) {
859
- await waitUntilLive(opts.playUrl, await buildFingerprint(siteDir), opts.liveTimeoutMs ?? 12e4, log);
860
- }
861
- return true;
862
- }
863
- log.warn("git push failed \u2014 your live game was NOT updated.");
864
- const tail = push.err.trim().split("\n").slice(-2).join(" ");
865
- if (tail) log.dim(` ${tail}`);
866
- return false;
867
- } finally {
868
- await fs6.rm(gitDir, { recursive: true, force: true }).catch(() => {
1029
+ const commit = (await run("git", ["commit-tree", tree, "-m", "source"], { ...base, ...ident })).out.trim();
1030
+ await run("git", ["update-ref", "refs/heads/main", commit], base);
1031
+ const push = await run("git", ["push", "-q", sshUrl, "+refs/heads/main:main"], {
1032
+ ...base,
1033
+ // Quote the key path: git splits GIT_SSH_COMMAND shell-like, so a folder with a
1034
+ // space would otherwise break the `-i` argument.
1035
+ GIT_SSH_COMMAND: `ssh -i "${keyPath}" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new`
869
1036
  });
870
- }
871
- }
872
- async function hasBuildScript(cwd) {
873
- try {
874
- const pkg = JSON.parse(await fs6.readFile(path7.join(cwd, "package.json"), "utf8"));
875
- return Boolean(pkg.scripts?.build);
1037
+ return push.code === 0 ? true : failed();
876
1038
  } catch {
877
- return false;
1039
+ return failed();
1040
+ } finally {
1041
+ await fs7.rm(gitDir, { recursive: true, force: true }).catch(() => {
1042
+ });
878
1043
  }
879
1044
  }
880
1045
  async function isDir2(p) {
881
1046
  try {
882
- return (await fs6.stat(p)).isDirectory();
1047
+ return (await fs7.stat(p)).isDirectory();
883
1048
  } catch {
884
1049
  return false;
885
1050
  }
886
1051
  }
887
1052
  async function fileExists(p) {
888
1053
  try {
889
- await fs6.access(p);
1054
+ await fs7.access(p);
890
1055
  return true;
891
1056
  } catch {
892
1057
  return false;
893
1058
  }
894
1059
  }
895
1060
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
896
- async function buildFingerprint(siteDir) {
897
- let html;
898
- try {
899
- html = await fs6.readFile(path7.join(siteDir, "index.html"), "utf8");
900
- } catch {
901
- return null;
902
- }
1061
+ function fingerprintOf(html) {
903
1062
  const m = html.match(/[A-Za-z0-9_]+-[A-Za-z0-9_-]{8}\.(?:js|css)/);
904
1063
  return m ? m[0] : null;
905
1064
  }
906
1065
  async function waitUntilLive(playUrl, fingerprint, timeoutMs, log) {
907
1066
  if (!fingerprint) {
908
- log.dim(` ${playUrl}`);
909
- log.dim(" (GitHub Pages rebuilds ~30\u201390s after a push.)");
1067
+ log.success(`Live. \u{1F310} ${playUrl}`);
910
1068
  return;
911
1069
  }
912
- log.step("Waiting for GitHub Pages to serve the new build\u2026");
913
1070
  const deadline = Date.now() + timeoutMs;
914
1071
  let n = 0;
915
1072
  while (Date.now() < deadline) {
@@ -921,25 +1078,23 @@ async function waitUntilLive(playUrl, fingerprint, timeoutMs, log) {
921
1078
  }
922
1079
  } catch {
923
1080
  }
924
- await sleep(4e3);
1081
+ await sleep(1500);
925
1082
  }
926
- log.warn("Still publishing \u2014 GitHub Pages can take a bit longer.");
927
- log.dim(` ${playUrl}`);
928
- log.dim(" (Open it in ~30\u201360s; the new build is on its way.)");
1083
+ log.success(`Published. \u{1F310} ${playUrl}`);
1084
+ log.dim(" (If the previous build shows, hard-refresh in a few seconds.)");
929
1085
  }
930
- async function warnIfAbsolutePaths(siteDir, log) {
931
- let html;
1086
+
1087
+ // src/commands/publish.ts
1088
+ async function detectEmbedSdkVersion(cwd = process.cwd()) {
932
1089
  try {
933
- html = await fs6.readFile(path7.join(siteDir, "index.html"), "utf8");
1090
+ const raw = await fs8.readFile(path9.join(cwd, "package.json"), "utf8");
1091
+ const pkg = JSON.parse(raw);
1092
+ const version = pkg.dependencies?.["@genex-ai/embed-sdk"] ?? pkg.devDependencies?.["@genex-ai/embed-sdk"];
1093
+ return typeof version === "string" && version ? version : null;
934
1094
  } catch {
935
- return;
936
- }
937
- if (/(?:src|href)="\/(?!\/)/.test(html)) {
938
- log.warn("Built index.html uses absolute paths \u2014 set `base: './'` in vite.config so assets load under the Pages subpath.");
1095
+ return null;
939
1096
  }
940
1097
  }
941
-
942
- // src/commands/publish.ts
943
1098
  async function runPublish(opts) {
944
1099
  const log = createLogger({ quiet: opts.quiet });
945
1100
  log.plain(c.bold("genex publish"));
@@ -957,11 +1112,18 @@ async function runPublish(opts) {
957
1112
  return;
958
1113
  }
959
1114
  const apiUrl = getApiUrl(opts.apiUrl ?? meta.apiUrl);
960
- let pushed = null;
961
- if (opts.noPush) {
962
- log.info("Skipping build + git push (--no-push).");
1115
+ if (!opts.noPush) {
1116
+ const ok = await deployGame(
1117
+ { projectId: meta.id, sshUrl: meta.sshUrl, apiUrl, token },
1118
+ { noBuild: opts.noBuild },
1119
+ log
1120
+ );
1121
+ if (!ok) {
1122
+ process.exitCode = 1;
1123
+ return;
1124
+ }
963
1125
  } else {
964
- pushed = await deployGame(meta.sshUrl, { noBuild: opts.noBuild, playUrl: meta.playUrl }, log);
1126
+ log.info("Skipping build + deploy (--no-push).");
965
1127
  }
966
1128
  log.step("Listing it in the gallery\u2026");
967
1129
  let res;
@@ -970,33 +1132,26 @@ async function runPublish(opts) {
970
1132
  if (opts.title) body.title = opts.title;
971
1133
  if (opts.description) body.description = opts.description;
972
1134
  if (opts.regenerateCover) body.regenerateCover = true;
1135
+ const embedSdkVersion = await detectEmbedSdkVersion();
1136
+ if (embedSdkVersion) body.embedSdkVersion = embedSdkVersion;
973
1137
  res = await fetch(`${apiUrl}/api/projects/${meta.id}/publish`, {
974
1138
  method: "POST",
975
1139
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
976
1140
  body: JSON.stringify(body)
977
1141
  });
978
- } catch (err) {
979
- log.error(`Couldn't reach the API at ${apiUrl}: ${String(err)}`);
1142
+ } catch {
1143
+ log.error("Couldn't reach Genex \u2014 please try again.");
980
1144
  process.exitCode = 1;
981
1145
  return;
982
1146
  }
983
1147
  if (!res.ok) {
984
- log.error(`Publish failed (HTTP ${res.status}).`);
1148
+ log.error("Publish failed \u2014 please try again.");
985
1149
  process.exitCode = 1;
986
1150
  return;
987
1151
  }
988
1152
  log.plain("");
989
- if (pushed === false) {
990
- log.warn(
991
- "Listed in the gallery, but the game wasn't pushed \u2014 the play URL won't work until the push succeeds."
992
- );
993
- log.dim(" Fix the error above and re-run `genex publish`.");
994
- } else {
995
- log.success("Published. \u{1F389}");
996
- }
997
- if (meta.playUrl) {
998
- log.dim(` play: ${meta.playUrl}`);
999
- }
1153
+ log.success("Published. \u{1F389}");
1154
+ if (meta.playUrl) log.dim(` play: ${meta.playUrl}`);
1000
1155
  }
1001
1156
 
1002
1157
  // src/commands/preview.ts
@@ -1010,26 +1165,32 @@ async function runPreview(opts) {
1010
1165
  process.exitCode = 1;
1011
1166
  return;
1012
1167
  }
1013
- const pushed = await deployGame(
1014
- meta.sshUrl,
1015
- { noBuild: opts.noBuild, playUrl: meta.playUrl },
1168
+ const token = await readUserToken(opts.envPath);
1169
+ if (!token) {
1170
+ log.error("Not authorized. Run `genex init` first to sign in.");
1171
+ process.exitCode = 1;
1172
+ return;
1173
+ }
1174
+ const apiUrl = getApiUrl(meta.apiUrl);
1175
+ const ok = await deployGame(
1176
+ { projectId: meta.id, sshUrl: meta.sshUrl, apiUrl, token },
1177
+ { noBuild: opts.noBuild },
1016
1178
  log
1017
1179
  );
1018
- log.plain("");
1019
- if (!pushed) {
1020
- log.warn("Preview not updated \u2014 see the error above.");
1180
+ if (!ok) {
1021
1181
  process.exitCode = 1;
1022
1182
  return;
1023
1183
  }
1184
+ log.plain("");
1024
1185
  log.success("Preview deployed \u2014 unlisted draft (run `genex publish` to list it).");
1025
1186
  }
1026
1187
 
1027
1188
  // src/commands/generate.ts
1028
- import path9 from "path";
1189
+ import path11 from "path";
1029
1190
 
1030
1191
  // src/lib/assets.ts
1031
- import fs7 from "fs/promises";
1032
- import path8 from "path";
1192
+ import fs9 from "fs/promises";
1193
+ import path10 from "path";
1033
1194
  function slugify(input) {
1034
1195
  const s = input.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50).replace(/-+$/g, "");
1035
1196
  return s || "asset";
@@ -1038,11 +1199,50 @@ async function downloadToFile(url, dest, headers) {
1038
1199
  const res = await fetch(url, { headers });
1039
1200
  if (!res.ok) throw new Error(`download failed (HTTP ${res.status}) for ${url}`);
1040
1201
  const buf = Buffer.from(await res.arrayBuffer());
1041
- await fs7.mkdir(path8.dirname(dest), { recursive: true });
1042
- await fs7.writeFile(dest, buf);
1202
+ await fs9.mkdir(path10.dirname(dest), { recursive: true });
1203
+ await fs9.writeFile(dest, buf);
1043
1204
  return buf.byteLength;
1044
1205
  }
1045
1206
 
1207
+ // src/lib/sse.ts
1208
+ async function* readSSE(body) {
1209
+ const decoder = new TextDecoder();
1210
+ const reader = body.getReader();
1211
+ let buf = "";
1212
+ let event = "";
1213
+ let data = [];
1214
+ try {
1215
+ while (true) {
1216
+ const { done, value } = await reader.read();
1217
+ if (done) break;
1218
+ buf += decoder.decode(value, { stream: true });
1219
+ while (true) {
1220
+ const nl = buf.search(/[\r\n]/);
1221
+ if (nl === -1) break;
1222
+ if (buf[nl] === "\r" && nl === buf.length - 1) break;
1223
+ const sep = buf[nl] === "\r" && buf[nl + 1] === "\n" ? 2 : 1;
1224
+ const line = buf.slice(0, nl);
1225
+ buf = buf.slice(nl + sep);
1226
+ if (line === "") {
1227
+ if (data.length > 0) yield { event: event || "message", data: data.join("\n") };
1228
+ event = "";
1229
+ data = [];
1230
+ continue;
1231
+ }
1232
+ if (line.startsWith(":")) continue;
1233
+ const colon = line.indexOf(":");
1234
+ const field = colon === -1 ? line : line.slice(0, colon);
1235
+ let value2 = colon === -1 ? "" : line.slice(colon + 1);
1236
+ if (value2.startsWith(" ")) value2 = value2.slice(1);
1237
+ if (field === "event") event = value2;
1238
+ else if (field === "data") data.push(value2);
1239
+ }
1240
+ }
1241
+ } finally {
1242
+ reader.releaseLock();
1243
+ }
1244
+ }
1245
+
1046
1246
  // src/commands/generate.ts
1047
1247
  var PUBLIC_PREFIX = "public/";
1048
1248
  var KIND_DIR = {
@@ -1112,7 +1312,10 @@ async function runGenerate(kind, opts) {
1112
1312
  return;
1113
1313
  }
1114
1314
  log.step("Generating\u2026 (this can take up to a minute)");
1115
- const view = await poll(apiUrl, token, id, (p) => log.dim(` ${p}%`));
1315
+ const onProgress = (p) => log.dim(` ${p}%`);
1316
+ const deadline = Date.now() + WAIT_TIMEOUT_MS;
1317
+ const streamed = await waitViaSSE(apiUrl, token, id, onProgress, deadline);
1318
+ const view = streamed === "unsupported" ? await poll(apiUrl, token, id, onProgress) : streamed;
1116
1319
  if (!view) {
1117
1320
  log.error("Timed out waiting for the generation.");
1118
1321
  process.exitCode = 1;
@@ -1129,7 +1332,7 @@ async function runGenerate(kind, opts) {
1129
1332
  for (const f of view.files ?? []) {
1130
1333
  const rel = fileDest(kind, slug, f);
1131
1334
  const url = `${apiUrl}/api/generations/${id}/asset/${f.role}`;
1132
- const bytes = await downloadToFile(url, path9.join(process.cwd(), rel), {
1335
+ const bytes = await downloadToFile(url, path11.join(process.cwd(), rel), {
1133
1336
  Authorization: `Bearer ${token}`
1134
1337
  });
1135
1338
  written.push(rel);
@@ -1143,8 +1346,61 @@ async function runGenerate(kind, opts) {
1143
1346
  log.plain("");
1144
1347
  printHint(kind, written, log);
1145
1348
  }
1349
+ var WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
1350
+ var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed"]);
1351
+ async function waitViaSSE(apiUrl, token, id, onProgress, deadline) {
1352
+ let connectFailures = 0;
1353
+ let last = -1;
1354
+ while (Date.now() < deadline) {
1355
+ const abort = new AbortController();
1356
+ const timer = setTimeout(() => abort.abort(), deadline - Date.now());
1357
+ try {
1358
+ let res;
1359
+ try {
1360
+ res = await fetch(`${apiUrl}/api/generations/${id}/events`, {
1361
+ headers: { Authorization: `Bearer ${token}`, Accept: "text/event-stream" },
1362
+ signal: abort.signal
1363
+ });
1364
+ } catch {
1365
+ if (++connectFailures >= 3) return "unsupported";
1366
+ await sleep2(2e3);
1367
+ continue;
1368
+ }
1369
+ if (res.status === 404 || res.status === 405) return "unsupported";
1370
+ const contentType = res.headers.get("content-type") ?? "";
1371
+ if (!res.ok || !res.body || !contentType.includes("text/event-stream")) {
1372
+ if (++connectFailures >= 3) return "unsupported";
1373
+ await sleep2(2e3);
1374
+ continue;
1375
+ }
1376
+ connectFailures = 0;
1377
+ try {
1378
+ for await (const ev of readSSE(res.body)) {
1379
+ if (ev.event !== "status") continue;
1380
+ let view;
1381
+ try {
1382
+ view = JSON.parse(ev.data);
1383
+ } catch {
1384
+ continue;
1385
+ }
1386
+ if (view.progress !== last) {
1387
+ last = view.progress;
1388
+ onProgress(view.progress);
1389
+ }
1390
+ if (TERMINAL.has(view.status)) return view;
1391
+ }
1392
+ } catch {
1393
+ }
1394
+ await sleep2(1e3);
1395
+ } finally {
1396
+ clearTimeout(timer);
1397
+ abort.abort();
1398
+ }
1399
+ }
1400
+ return null;
1401
+ }
1146
1402
  async function poll(apiUrl, token, id, onProgress) {
1147
- const deadline = Date.now() + 10 * 60 * 1e3;
1403
+ const deadline = Date.now() + WAIT_TIMEOUT_MS;
1148
1404
  let last = -1;
1149
1405
  while (Date.now() < deadline) {
1150
1406
  try {
@@ -1185,9 +1441,9 @@ function printHint(kind, written, log) {
1185
1441
  var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "texture"]);
1186
1442
  function getVersion() {
1187
1443
  try {
1188
- const here = path10.dirname(fileURLToPath2(import.meta.url));
1444
+ const here = path12.dirname(fileURLToPath2(import.meta.url));
1189
1445
  const pkg = JSON.parse(
1190
- readFileSync(path10.resolve(here, "..", "package.json"), "utf8")
1446
+ readFileSync(path12.resolve(here, "..", "package.json"), "utf8")
1191
1447
  );
1192
1448
  return pkg.version ?? "0.0.0";
1193
1449
  } catch {