@genex-ai/cli-demo 0.20.0 → 0.22.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.
package/README.md CHANGED
@@ -8,10 +8,10 @@ genex init <name> # authorize + create the draft project
8
8
  genex link <slug> # re-link this folder to an existing game of yours (recovery)
9
9
  genex preview # build + push to the hosted draft URL (unlisted)
10
10
  genex publish # build + push, then list the game in the gallery
11
- genex model "<prompt>" # generate a 3D model → assets/models/
12
- genex skybox "<prompt>" # generate a 360° sky → assets/skybox/
13
- genex sfx "<prompt>" # generate a sound fx → assets/sfx/
14
- genex texture "<prompt>" # generate a texture → assets/textures/
11
+ genex model "<prompt>" # generate a 3D model → prints an asset URL
12
+ genex skybox "<prompt>" # generate a 360° sky → prints an asset URL
13
+ genex sfx "<prompt>" # generate a sound fx → prints an asset URL
14
+ genex texture "<prompt>" # generate a texture → prints an asset URL
15
15
  genex controller <type> # install a tuned character|car|drone controller → src/controllers/
16
16
  ```
17
17
 
@@ -70,22 +70,22 @@ Defaults: API `https://demo-api.glotech.world`, auth site
70
70
  ## Generating assets
71
71
 
72
72
  `genex model | skybox | sfx | texture "<prompt>"` turn a prompt into a real,
73
- game-ready asset and download it into the project's `./assets/<kind>/` folder. The
74
- command blocks until the asset is ready (poll loop), then writes the file(s):
73
+ game-ready asset stored in R2. The command blocks until the asset is ready (live SSE
74
+ stream), then prints its public URL the game loads it directly, nothing is
75
+ downloaded or committed:
75
76
 
76
- | Command | Provider | Writes |
77
+ | Command | Provider | Prints (role) |
77
78
  | --- | --- | --- |
78
- | `genex model "<prompt>"` | Tripo | `assets/models/<slug>.glb` |
79
- | `genex skybox "<prompt>"` | Blockade Labs | `assets/skybox/<slug>.jpg` |
80
- | `genex sfx "<prompt>" [--duration <s>]` | ElevenLabs | `assets/sfx/<slug>.mp3` |
81
- | `genex texture "<prompt>" [--terrain]` | Gemini | `assets/textures/<slug>/basecolor.<ext>` |
82
-
83
- Because the file lands in the project and `genex publish` uploads the whole built
84
- directory, generated assets **ship inside the published R2 bundle** games serve
85
- at the domain root (`https://<slug>.genex.technology/`), so both absolute
86
- (`/assets/...`) and relative (`./assets/...`) paths resolve, and players get them
87
- from the same origin (no CORS, no runtime dependency on this API). Each kind has a scaffolded
88
- `genex-ai-<kind>` skill with the exact Three.js loader code.
79
+ | `genex model "<prompt>"` | Tripo | `…/generations/<id>/model-glb` |
80
+ | `genex skybox "<prompt>"` | Blockade Labs | `…/generations/<id>/skybox-equirect` |
81
+ | `genex sfx "<prompt>" [--duration <s>]` | ElevenLabs | `…/generations/<id>/audio-sfx` |
82
+ | `genex texture "<prompt>" [--terrain]` | Gemini | `…/generations/<id>/texture-basecolor` |
83
+
84
+ Each URL is a permanent `https://assets.genex.technology/...` address served straight
85
+ from R2 (with CORS, so three.js loads it cross-origin without tainting). It resolves
86
+ the same in local dev, the published game, and remixes, and the bytes never pass back
87
+ through this API. Each kind has a scaffolded `genex-ai-<kind>` skill with the exact
88
+ Three.js loader code.
89
89
 
90
90
  Auth reuses the existing `GENEX_TOKEN` (run `genex init` first). Server-side, each
91
91
  provider is keyed by an env var (`TRIPO_API_KEY`, `BLOCKADE_LABS_API_KEY`,
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { readFileSync } from "fs";
5
- import path14 from "path";
5
+ import path12 from "path";
6
6
  import { fileURLToPath as fileURLToPath2 } from "url";
7
7
 
8
8
  // src/commands/init.ts
@@ -1399,6 +1399,9 @@ async function runPublish(opts) {
1399
1399
  if (opts.description) body.description = opts.description;
1400
1400
  if (opts.regenerateCover) body.regenerateCover = true;
1401
1401
  if (opts.categories?.length) body.categories = opts.categories;
1402
+ if (opts.sourceRepoUrl) body.sourceRepoUrl = opts.sourceRepoUrl;
1403
+ if (opts.sourceAuthor) body.sourceAuthor = opts.sourceAuthor;
1404
+ if (opts.license) body.license = opts.license;
1402
1405
  if (detections.embedSdkVersion) body.embedSdkVersion = detections.embedSdkVersion;
1403
1406
  body.multiplayer = detections.multiplayer;
1404
1407
  body.matchmaking = detections.matchmaking ?? null;
@@ -1465,25 +1468,6 @@ async function runPreview(opts) {
1465
1468
  log.success("Preview deployed \u2014 unlisted draft (run `genex publish` to list it).");
1466
1469
  }
1467
1470
 
1468
- // src/commands/generate.ts
1469
- import path12 from "path";
1470
-
1471
- // src/lib/assets.ts
1472
- import fs10 from "fs/promises";
1473
- import path11 from "path";
1474
- function slugify(input) {
1475
- const s = input.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50).replace(/-+$/g, "");
1476
- return s || "asset";
1477
- }
1478
- async function downloadToFile(url, dest, headers) {
1479
- const res = await fetch(url, { headers });
1480
- if (!res.ok) throw new Error(`download failed (HTTP ${res.status}) for ${url}`);
1481
- const buf = Buffer.from(await res.arrayBuffer());
1482
- await fs10.mkdir(path11.dirname(dest), { recursive: true });
1483
- await fs10.writeFile(dest, buf);
1484
- return buf.byteLength;
1485
- }
1486
-
1487
1471
  // src/lib/sse.ts
1488
1472
  async function* readSSE(body) {
1489
1473
  const decoder = new TextDecoder();
@@ -1524,24 +1508,7 @@ async function* readSSE(body) {
1524
1508
  }
1525
1509
 
1526
1510
  // src/commands/generate.ts
1527
- var PUBLIC_PREFIX = "public/";
1528
- var KIND_DIR = {
1529
- model: "public/assets/models",
1530
- skybox: "public/assets/skybox",
1531
- sfx: "public/assets/sfx",
1532
- texture: "public/assets/textures"
1533
- };
1534
- function runtimePath(rel) {
1535
- return `./${rel.replace(PUBLIC_PREFIX, "")}`;
1536
- }
1537
1511
  var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
1538
- function fileDest(kind, slug, f) {
1539
- if (kind === "texture") {
1540
- const name = f.role.replace(/^texture-/, "");
1541
- return `public/assets/textures/${slug}/${name}.${f.ext}`;
1542
- }
1543
- return `${KIND_DIR[kind]}/${slug}.${f.ext}`;
1544
- }
1545
1512
  async function runGenerate(kind, opts) {
1546
1513
  const log = createLogger({ quiet: opts.quiet });
1547
1514
  const prompt = opts.prompt?.trim();
@@ -1588,7 +1555,7 @@ async function runGenerate(kind, opts) {
1588
1555
  return;
1589
1556
  }
1590
1557
  if (opts.noWait) {
1591
- log.success(`Queued (${id}). Re-run without --no-wait to fetch the finished asset.`);
1558
+ log.success(`Queued (${id}). Re-run without --no-wait to get the finished asset URL.`);
1592
1559
  return;
1593
1560
  }
1594
1561
  log.step("Generating\u2026 (this can take up to a minute)");
@@ -1606,25 +1573,20 @@ async function runGenerate(kind, opts) {
1606
1573
  process.exitCode = 1;
1607
1574
  return;
1608
1575
  }
1609
- const slug = slugify(prompt);
1610
- const written = [];
1611
- try {
1612
- for (const f of view.files ?? []) {
1613
- const rel = fileDest(kind, slug, f);
1614
- const url = `${apiUrl}/api/generations/${id}/asset/${f.role}`;
1615
- const bytes = await downloadToFile(url, path12.join(process.cwd(), rel), {
1616
- Authorization: `Bearer ${token}`
1617
- });
1618
- written.push(rel);
1619
- log.success(`${rel} ${c.dim(`(${(bytes / 1024).toFixed(0)} KB)`)}`);
1620
- }
1621
- } catch (err) {
1622
- log.error(`Download failed: ${String(err)}`);
1576
+ const files = view.files ?? [];
1577
+ if (files.length === 0) {
1578
+ log.error("Generation completed but produced no files.");
1623
1579
  process.exitCode = 1;
1624
1580
  return;
1625
1581
  }
1626
1582
  log.plain("");
1627
- printHint(kind, written, log);
1583
+ log.success("Done \u2014 live on R2 (nothing downloaded or committed):");
1584
+ for (const f of files) {
1585
+ const label = files.length > 1 ? `${c.dim(`${f.role.replace(/^texture-/, "")}: `)}` : "";
1586
+ log.plain(` ${label}${f.url}`);
1587
+ }
1588
+ log.plain("");
1589
+ printHint(kind, files, log);
1628
1590
  }
1629
1591
  var WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
1630
1592
  var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed"]);
@@ -1703,23 +1665,21 @@ async function poll(apiUrl, token, id, onProgress) {
1703
1665
  }
1704
1666
  return null;
1705
1667
  }
1706
- function printHint(kind, written, log) {
1707
- const load = runtimePath(written[0] ?? "");
1668
+ function printHint(kind, files, log) {
1669
+ const url = files[0]?.url ?? "";
1708
1670
  const hint = {
1709
- model: `Load with GLTFLoader and add to the scene \u2014 see the genex-ai-model skill. Use the relative path "${load}".`,
1710
- skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment \u2014 see genex-ai-skybox. Path "${load}".`,
1711
- sfx: `Load with AudioLoader into a THREE.PositionalAudio (camera needs an AudioListener) \u2014 see genex-ai-sfx. Path "${load}".`,
1712
- texture: `Load with TextureLoader, set RepeatWrapping, build a MeshStandardMaterial \u2014 see genex-ai-texture. Path "${load}".`
1671
+ model: `Load with GLTFLoader from the URL and add it to the scene \u2014 see the genex-ai-model skill. url = "${url}"`,
1672
+ skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment \u2014 see genex-ai-skybox. url = "${url}"`,
1673
+ sfx: `Load with AudioLoader into a THREE.PositionalAudio (camera needs an AudioListener) \u2014 see genex-ai-sfx. url = "${url}"`,
1674
+ texture: `Load each map with TextureLoader (RepeatWrapping) into a MeshStandardMaterial \u2014 see genex-ai-texture. Use the URLs above by role.`
1713
1675
  };
1714
- log.success(
1715
- "Done. Asset saved into public/assets (so Vite ships it; load it as ./assets/\u2026 \u2014 `genex publish` commits and ships it with the game)."
1716
- );
1717
1676
  log.dim(` ${hint[kind]}`);
1677
+ log.dim(" Reference the URL directly in your code \u2014 don't download it into the repo.");
1718
1678
  }
1719
1679
 
1720
1680
  // src/commands/controller.ts
1721
- import fs11 from "fs/promises";
1722
- import path13 from "path";
1681
+ import fs10 from "fs/promises";
1682
+ import path11 from "path";
1723
1683
  var CONTROLLER_KINDS = ["character", "car", "drone"];
1724
1684
  var SHARED = [
1725
1685
  "shared/math.ts",
@@ -1798,8 +1758,8 @@ var CONTROLLER_FILE_SETS = {
1798
1758
  ]
1799
1759
  }
1800
1760
  };
1801
- var CODE_DEST = path13.join("src", "controllers");
1802
- var ASSETS_DEST = path13.join("public", "assets");
1761
+ var CODE_DEST = path11.join("src", "controllers");
1762
+ var ASSETS_DEST = path11.join("public", "assets");
1803
1763
  async function runController(opts) {
1804
1764
  const log = createLogger({ quiet: opts.quiet });
1805
1765
  const kind = opts.kind?.trim();
@@ -1812,31 +1772,31 @@ async function runController(opts) {
1812
1772
  process.exitCode = 1;
1813
1773
  return;
1814
1774
  }
1815
- const srcDir = path13.join(getTemplatesDir(), "controllers");
1775
+ const srcDir = path11.join(getTemplatesDir(), "controllers");
1816
1776
  const root = opts.cwd ?? process.cwd();
1817
1777
  const set = CONTROLLER_FILE_SETS[kind];
1818
1778
  log.plain(c.bold(`genex controller ${kind}`));
1819
1779
  log.plain("");
1820
- log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path13.sep)}`);
1780
+ log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path11.sep)}`);
1821
1781
  const plan = [
1822
- ...set.code.map((rel) => ({ from: rel, rel: path13.join(CODE_DEST, rel) })),
1782
+ ...set.code.map((rel) => ({ from: rel, rel: path11.join(CODE_DEST, rel) })),
1823
1783
  ...set.assets.map((rel) => ({
1824
1784
  from: rel,
1825
- rel: path13.join(ASSETS_DEST, path13.basename(rel))
1785
+ rel: path11.join(ASSETS_DEST, path11.basename(rel))
1826
1786
  }))
1827
1787
  ];
1828
1788
  let copied = 0;
1829
1789
  let skipped = 0;
1830
1790
  try {
1831
1791
  for (const file of plan) {
1832
- const dest = path13.join(root, file.rel);
1792
+ const dest = path11.join(root, file.rel);
1833
1793
  if (!opts.force && await exists2(dest)) {
1834
1794
  skipped++;
1835
1795
  log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
1836
1796
  continue;
1837
1797
  }
1838
- await fs11.mkdir(path13.dirname(dest), { recursive: true });
1839
- await fs11.copyFile(path13.join(srcDir, file.from), dest);
1798
+ await fs10.mkdir(path11.dirname(dest), { recursive: true });
1799
+ await fs10.copyFile(path11.join(srcDir, file.from), dest);
1840
1800
  copied++;
1841
1801
  log.dim(` ${file.rel}`);
1842
1802
  }
@@ -1868,8 +1828,8 @@ async function runController(opts) {
1868
1828
  }
1869
1829
  async function installOwnerAvatar(args) {
1870
1830
  const { root, srcDir, apiUrl, token, log } = args;
1871
- const dest = path13.join(root, ASSETS_DEST, "avatar.vrm");
1872
- await fs11.mkdir(path13.dirname(dest), { recursive: true });
1831
+ const dest = path11.join(root, ASSETS_DEST, "avatar.vrm");
1832
+ await fs10.mkdir(path11.dirname(dest), { recursive: true });
1873
1833
  if (token) {
1874
1834
  try {
1875
1835
  const res = await fetch(`${apiUrl}/api/avatars/me`, {
@@ -1881,7 +1841,7 @@ async function installOwnerAvatar(args) {
1881
1841
  const vrmRes = await fetch(me.vrmUrl);
1882
1842
  if (vrmRes.ok) {
1883
1843
  const buf = Buffer.from(await vrmRes.arrayBuffer());
1884
- await fs11.writeFile(dest, buf);
1844
+ await fs10.writeFile(dest, buf);
1885
1845
  log.dim(` public/assets/avatar.vrm (your avatar \u2014 ${(buf.length / 1e6).toFixed(1)} MB)`);
1886
1846
  return;
1887
1847
  }
@@ -1892,27 +1852,93 @@ async function installOwnerAvatar(args) {
1892
1852
  log.dim(" avatar fetch failed (offline?); using the bundled default.");
1893
1853
  }
1894
1854
  }
1895
- await fs11.copyFile(path13.join(srcDir, "assets", "default-avatar.vrm"), dest);
1855
+ await fs10.copyFile(path11.join(srcDir, "assets", "default-avatar.vrm"), dest);
1896
1856
  log.dim(
1897
1857
  token ? " public/assets/avatar.vrm (bundled default)" : " public/assets/avatar.vrm (bundled default \u2014 sign in and re-run for your own)"
1898
1858
  );
1899
1859
  }
1900
1860
  async function exists2(p) {
1901
1861
  try {
1902
- await fs11.access(p);
1862
+ await fs10.access(p);
1903
1863
  return true;
1904
1864
  } catch {
1905
1865
  return false;
1906
1866
  }
1907
1867
  }
1908
1868
 
1869
+ // src/commands/explore.ts
1870
+ async function runExplore(opts) {
1871
+ const log = createLogger({ quiet: opts.quiet });
1872
+ const query = opts.query?.trim();
1873
+ if (!query) {
1874
+ log.error('Missing query. Usage: genex explore "<what you need>"');
1875
+ process.exitCode = 1;
1876
+ return;
1877
+ }
1878
+ const apiUrl = getApiUrl(opts.apiUrl);
1879
+ let res;
1880
+ try {
1881
+ res = await fetch(`${apiUrl}/api/gallery/search-corpus?curated=1`);
1882
+ } catch {
1883
+ log.error("Couldn't reach Genex \u2014 please try again.");
1884
+ process.exitCode = 1;
1885
+ return;
1886
+ }
1887
+ if (!res.ok) {
1888
+ log.error(`explore: API returned ${res.status} \u2014 please try again.`);
1889
+ process.exitCode = 1;
1890
+ return;
1891
+ }
1892
+ const { items } = await res.json();
1893
+ const ranked = rank(items, query).slice(0, 5);
1894
+ if (opts.json) {
1895
+ console.log(JSON.stringify(ranked, null, 2));
1896
+ return;
1897
+ }
1898
+ if (ranked.length === 0) {
1899
+ log.plain(
1900
+ `No curated projects matched "${query}". Try a broader term (terrain, grass, vehicle, water, building, dungeon, fish, shader).`
1901
+ );
1902
+ return;
1903
+ }
1904
+ for (const [i, p] of ranked.entries()) {
1905
+ const meta = [
1906
+ p.categories.join(", "),
1907
+ p.license ?? "",
1908
+ p.sourceAuthor ? `originally by ${p.sourceAuthor}` : ""
1909
+ ].filter(Boolean).join(" \xB7 ");
1910
+ log.plain(`${c.bold(`${i + 1}. ${p.title}`)} \u2014 ${meta}`);
1911
+ if (p.description) log.plain(` ${p.description.slice(0, 160)}`);
1912
+ if (p.playUrl) log.plain(` play: ${p.playUrl}`);
1913
+ if (p.cloneUrl) log.plain(` clone: ${p.cloneUrl} (editable source on main)`);
1914
+ if (p.sourceRepoUrl) log.plain(` upstream: ${p.sourceRepoUrl}`);
1915
+ }
1916
+ log.plain("");
1917
+ log.plain(
1918
+ `To use one: clone it as a starting template, or borrow parts into your current game \u2014 the genex-explore skill has the exact steps. Keep the credits.`
1919
+ );
1920
+ }
1921
+ function rank(items, query) {
1922
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
1923
+ return items.map((p) => {
1924
+ const hay = `${p.title} ${p.description} ${p.categories.join(" ")}`.toLowerCase();
1925
+ let score = 0;
1926
+ for (const t of terms) {
1927
+ if (p.title.toLowerCase().includes(t)) score += 3;
1928
+ if (p.categories.some((cat) => cat.includes(t))) score += 2;
1929
+ if (hay.includes(t)) score += 1;
1930
+ }
1931
+ return { p, score };
1932
+ }).filter((x) => x.score > 0).sort((a, b) => b.score - a.score || b.p.playsCount - a.p.playsCount).map((x) => x.p);
1933
+ }
1934
+
1909
1935
  // src/index.ts
1910
1936
  var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "texture"]);
1911
1937
  function getVersion() {
1912
1938
  try {
1913
- const here = path14.dirname(fileURLToPath2(import.meta.url));
1939
+ const here = path12.dirname(fileURLToPath2(import.meta.url));
1914
1940
  const pkg = JSON.parse(
1915
- readFileSync(path14.resolve(here, "..", "package.json"), "utf8")
1941
+ readFileSync(path12.resolve(here, "..", "package.json"), "utf8")
1916
1942
  );
1917
1943
  return pkg.version ?? "0.0.0";
1918
1944
  } catch {
@@ -1934,6 +1960,8 @@ ${c.bold("Usage")}
1934
1960
  genex texture "<prompt>" [options] Generate a PBR texture into public/assets/textures.
1935
1961
  genex controller <type> [--force] Install a physics controller (character|car|drone)
1936
1962
  into src/controllers (+ assets into public/assets).
1963
+ genex explore "<query>" [options] Search the curated community gallery \u2014 proven
1964
+ Three.js systems you can clone or borrow parts from.
1937
1965
 
1938
1966
  ${c.bold("Options for the generators (`model` `skybox` `sfx` `texture`)")}
1939
1967
  --terrain (texture) seamless tiling surface for terrain/ground.
@@ -1972,9 +2000,17 @@ ${c.bold("Options for `preview` / `publish`")}
1972
2000
  --categories <list> (publish) 1-3 gallery categories, comma-separated.
1973
2001
  Valid: games, assets, physics, terrain, lighting, vfx.
1974
2002
  --regenerate-cover (publish) Re-mint the disc cover (concept changed).
2003
+ --source-repo-url <url> (publish) Upstream repo this game is a port of \u2014
2004
+ shown as "Ported from \u2026" credit on the game page.
2005
+ --source-author <name> (publish) Upstream author to credit.
2006
+ --license <label> (publish) Upstream license label (e.g. MIT).
1975
2007
  --api-url <url> (publish) Override the API base URL.
1976
2008
  --env <path> Token env file (default: ~/.genex/env).
1977
2009
 
2010
+ ${c.bold("Options for `explore`")}
2011
+ --json Print the top matches as JSON (for tools/agents).
2012
+ --api-url <url> Override the API base URL.
2013
+
1978
2014
  ${c.bold("Global")}
1979
2015
  --quiet Reduce output.
1980
2016
  -h, --help Show this help.
@@ -1999,6 +2035,7 @@ ${c.bold("Examples")}
1999
2035
  genex sfx "punchy laser zap" --duration 2
2000
2036
  genex texture "mossy cracked cobblestone" --terrain
2001
2037
  genex controller character
2038
+ genex explore "grass"
2002
2039
  `;
2003
2040
  function parseArgs(argv) {
2004
2041
  const parsed = {
@@ -2019,7 +2056,10 @@ function parseArgs(argv) {
2019
2056
  "--description",
2020
2057
  "--categories",
2021
2058
  "--timeout",
2022
- "--duration"
2059
+ "--duration",
2060
+ "--source-repo-url",
2061
+ "--source-author",
2062
+ "--license"
2023
2063
  ]);
2024
2064
  let i = 0;
2025
2065
  while (i < argv.length) {
@@ -2058,6 +2098,9 @@ function parseArgs(argv) {
2058
2098
  case "--quiet":
2059
2099
  parsed.options.quiet = true;
2060
2100
  break;
2101
+ case "--json":
2102
+ parsed.options.json = true;
2103
+ break;
2061
2104
  default: {
2062
2105
  if (needsValue.has(arg)) {
2063
2106
  const value = argv[i++];
@@ -2073,6 +2116,8 @@ function parseArgs(argv) {
2073
2116
  parsed.command = arg;
2074
2117
  } else if (parsed.options.name === void 0) {
2075
2118
  parsed.options.name = arg;
2119
+ } else if (parsed.command === "explore") {
2120
+ parsed.options.name = `${parsed.options.name} ${arg}`;
2076
2121
  } else {
2077
2122
  parsed.error = `Unexpected argument: ${arg}`;
2078
2123
  return parsed;
@@ -2115,6 +2160,15 @@ function applyValueFlag(options, flag, value) {
2115
2160
  case "--categories":
2116
2161
  options.categories = value.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
2117
2162
  break;
2163
+ case "--source-repo-url":
2164
+ options.sourceRepoUrl = value;
2165
+ break;
2166
+ case "--source-author":
2167
+ options.sourceAuthor = value;
2168
+ break;
2169
+ case "--license":
2170
+ options.license = value;
2171
+ break;
2118
2172
  case "--timeout": {
2119
2173
  const n = Number(value);
2120
2174
  if (!Number.isFinite(n) || n <= 0) {
@@ -2180,6 +2234,9 @@ async function main() {
2180
2234
  case "publish":
2181
2235
  await runPublish(parsed.options);
2182
2236
  break;
2237
+ case "explore":
2238
+ await runExplore({ ...parsed.options, query: parsed.options.name });
2239
+ break;
2183
2240
  default:
2184
2241
  log.error(`Unknown command: ${parsed.command}`);
2185
2242
  log.plain(`Run ${c.cyan("genex --help")} for usage.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "Set up your ~/.claude workspace, authorize, create a game project, generate AI assets, and publish (genex CLI).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,47 +21,58 @@ npx genex model "<prompt>"
21
21
  ```
22
22
 
23
23
  Write a specific prompt — "weathered wooden barrel with rusted iron bands" beats
24
- "barrel". The command blocks until the mesh is ready (up to ~a minute), then saves:
24
+ "barrel". The command blocks until the mesh is ready (up to ~a minute), then prints
25
+ its public URL:
25
26
 
26
27
  ```
27
- public/assets/models/<slug>.glb
28
+ https://assets.genex.technology/generations/<id>/model-glb
28
29
  ```
29
30
 
30
- `<slug>` is derived from the prompt (e.g. `weathered-wooden-barrel.glb`). The file
31
- lives under `public/` so Vite ships it in the build (load it as `./assets/models/...`
32
- the `public/` prefix is stripped when served). It is **committed by `npx genex publish`**,
33
- so it ships inside your published game.
34
-
35
- ## Hero models must face forward
36
-
37
- For the model the player controls (a ship, car, character — the "hero"), the mesh should
38
- **face forward**: its front pointing down the direction of travel (conventionally `+Z`,
39
- i.e. "looking forward"). A hero whose nose points sideways reads as broken at a glance.
40
-
41
- - **In the prompt:** when it's the player's craft, ask for it facing forward — e.g.
42
- `"...game-ready, facing forward, front toward +Z"`.
43
- - **On load (fallback):** if the imported GLB still isn't forward, rotate it once so its
44
- nose aligns with travel. Wrap the model in a parent `Object3D` and apply the correction
45
- to the child, so your movement code can rotate the parent cleanly:
31
+ The GLB lives in Genex storage (R2) and loads straight from that URL. You **don't
32
+ download it** and **nothing is committed to your repo** copy the URL the command
33
+ prints into your loader. The URL is permanent, so it works the same in local dev, your
34
+ published game, and remixes.
35
+
36
+ ## Models that point somewhere must face forward
37
+
38
+ Any model with a "front" the game aims — the hero the player controls, an **NPC that
39
+ walks toward or looks at players**, a turret, a vehicle should face **forward**: its
40
+ front toward `+Z` (glTF's "looking forward"), so yaw/`lookAt` code points it correctly.
41
+ A hero whose nose points sideways — or an enemy that chases you while staring off to the
42
+ side reads as broken at a glance.
43
+
44
+ - **In the prompt:** ask for it e.g. `"...game-ready, facing forward, front toward +Z"`.
45
+ Treat this as a hint, not a guarantee: generated meshes regularly come back rotated
46
+ anyway, so the check below is never optional.
47
+ - **On load (always check — hero AND NPC):** if the GLB isn't `+Z`-forward, rotate it
48
+ once. Wrap the model in a parent `Object3D` and put the correction on the child, so
49
+ game code rotates the parent cleanly:
46
50
  ```ts
47
- const hero = new THREE.Object3D();
51
+ const rig = new THREE.Object3D();
48
52
  gltf.scene.rotation.y = Math.PI / 2; // one-time facing correction — tune per model
49
- hero.add(gltf.scene);
50
- scene.add(hero); // move/rotate `hero`, not gltf.scene
53
+ rig.add(gltf.scene);
54
+ scene.add(rig); // move/rotate/lookAt `rig`, not gltf.scene
51
55
  ```
52
- - Orientation **is** visible in a still confirm the hero faces forward in your
53
- self-check screenshot before moving on.
56
+ Clones inherit the fix only if you clone the corrected child (or the whole rig) —
57
+ never the raw `gltf.scene`.
58
+ - **Verify it, don't assume it.** Orientation **is** visible in a still — in your
59
+ self-check screenshot confirm the hero faces its travel direction AND that NPCs driven
60
+ by chase/aim code face their target (an enemy rotated 90° from its victim is this
61
+ pipeline's most common visible bug). If you can't capture real gameplay (a draft's
62
+ sign-in gate is up), say so plainly instead of skipping the check silently.
54
63
 
55
64
  ## Load it into the scene
56
65
 
57
- Use Three.js `GLTFLoader` (the game serves at the domain root
58
- `https://<slug>.genex.technology/`, so relative or absolute paths both work):
66
+ Use Three.js `GLTFLoader` with the URL the command printed (R2 sends the right CORS
67
+ headers, so cross-origin loading just works):
59
68
 
60
69
  ```ts
61
70
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
62
71
 
63
72
  const loader = new GLTFLoader();
64
- const gltf = await loader.loadAsync("./assets/models/weathered-wooden-barrel.glb");
73
+ // the URL `npx genex model` printed:
74
+ const MODEL_URL = "https://assets.genex.technology/generations/<id>/model-glb";
75
+ const gltf = await loader.loadAsync(MODEL_URL);
65
76
  const model = gltf.scene;
66
77
  model.scale.setScalar(1); // tune to taste
67
78
  model.position.set(0, 0, 0);
@@ -78,17 +89,14 @@ scene is a ghost: players and objects pass straight through it.
78
89
 
79
90
  ## Publish checklist (so players see the model)
80
91
 
81
- - Reference assets with **relative** paths (`./assets/...`); absolute
82
- (`/assets/...`) also resolves since the game serves at the domain root.
83
- - Set `base: "./"` in `vite.config.ts` before `npm run build` (see the scaffold
84
- prompt's publish step).
85
- - Keep generated assets under `public/assets/` — Vite only ships files under `public/`
86
- (or files you `import`); `npx genex publish` ships that folder with the game.
92
+ - Load it from the **URL** the command printed — it's absolute and permanent, so it
93
+ resolves the same in local dev, the published game, and remixes. Nothing to commit.
94
+ - Don't copy the GLB into `public/assets/` generated assets live in R2, not the repo.
87
95
 
88
96
  ## Options
89
97
 
90
- - `--no-wait` — enqueue and return immediately (the file won't be downloaded;
91
- re-run without `--no-wait` to fetch it).
98
+ - `--no-wait` — enqueue and return immediately (won't print the URL; re-run without
99
+ `--no-wait` to get it).
92
100
  - `--api-url <url>` — override the API base (local dev).
93
101
 
94
102
  ## Troubleshooting
@@ -15,13 +15,15 @@ npx genex sfx "punchy laser zap, short and dry" --duration 2
15
15
  ```
16
16
 
17
17
  `--duration <sec>` (0.5–22) sets a target length; omit it to let the model pick.
18
- Blocks until ready, then saves:
18
+ Blocks until ready, then prints its public URL:
19
19
 
20
20
  ```
21
- public/assets/sfx/<slug>.mp3
21
+ https://assets.genex.technology/generations/<id>/audio-sfx
22
22
  ```
23
23
 
24
- Committed by `npx genex publish`, so it ships with your game.
24
+ The mp3 lives in Genex storage (R2) and loads straight from that URL — you don't download
25
+ it and nothing is committed to your repo. The URL is permanent (local dev, published game,
26
+ and remixes alike).
25
27
 
26
28
  ## Play it in Three.js
27
29
 
@@ -36,7 +38,9 @@ import * as THREE from "three";
36
38
  const listener = new THREE.AudioListener();
37
39
  camera.add(listener);
38
40
 
39
- const buffer = await new THREE.AudioLoader().loadAsync("./assets/sfx/laser-zap.mp3");
41
+ // the URL `npx genex sfx` printed (R2 sends CORS headers, so cross-origin works):
42
+ const SFX_URL = "https://assets.genex.technology/generations/<id>/audio-sfx";
43
+ const buffer = await new THREE.AudioLoader().loadAsync(SFX_URL);
40
44
 
41
45
  const sound = new THREE.PositionalAudio(listener);
42
46
  sound.setBuffer(buffer);
@@ -55,8 +59,8 @@ Reuse one loaded `buffer` across many plays; create a fresh `Audio`/`PositionalA
55
59
 
56
60
  ## Publish checklist
57
61
 
58
- - Relative path `./assets/sfx/...`; `base: "./"` in `vite.config.ts`; files live under
59
- `public/assets/` so Vite ships them; `npx genex publish` ships them to R2.
62
+ - Load it from the **URL** the command printed absolute and permanent, so it resolves
63
+ the same in local dev, the published game, and remixes. Nothing to commit.
60
64
 
61
65
  ## Options
62
66
 
@@ -23,13 +23,15 @@ also lights it (image-based lighting).
23
23
  npx genex skybox "<prompt>"
24
24
  ```
25
25
 
26
- Blocks until ready, then saves:
26
+ Blocks until ready, then prints its public URL:
27
27
 
28
28
  ```
29
- public/assets/skybox/<slug>.jpg
29
+ https://assets.genex.technology/generations/<id>/skybox-equirect
30
30
  ```
31
31
 
32
- The JPG is **committed by `npx genex publish`**, so it ships with your game.
32
+ The image lives in Genex storage (R2) and loads straight from that URL — you don't
33
+ download it and nothing is committed to your repo. The URL is permanent (works the same
34
+ in local dev, the published game, and remixes).
33
35
 
34
36
  ## Load it as background + environment
35
37
 
@@ -39,9 +41,9 @@ background and the lighting:
39
41
  ```ts
40
42
  import * as THREE from "three";
41
43
 
42
- const texture = await new THREE.TextureLoader().loadAsync(
43
- "./assets/skybox/golden-hour-over-misty-mountains.jpg",
44
- );
44
+ // the URL `npx genex skybox` printed (R2 sends CORS headers, so cross-origin works):
45
+ const SKYBOX_URL = "https://assets.genex.technology/generations/<id>/skybox-equirect";
46
+ const texture = await new THREE.TextureLoader().loadAsync(SKYBOX_URL);
45
47
  texture.mapping = THREE.EquirectangularReflectionMapping;
46
48
  texture.colorSpace = THREE.SRGBColorSpace;
47
49
 
@@ -62,10 +64,9 @@ scene.background = texture; // keep the raw texture for the visible sky
62
64
 
63
65
  ## Publish checklist
64
66
 
65
- - Use the **relative** path `./assets/skybox/...` (absolute `/assets/...` also works the game serves at the domain root).
66
- - `base: "./"` in `vite.config.ts` before `npm run build`.
67
- - Generated files live under `public/assets/` so Vite ships them; `npx genex publish`
68
- ships that folder with the game.
67
+ - Load it from the **URL** the command printedabsolute and permanent, so it resolves
68
+ the same in local dev, the published game, and remixes. Nothing to commit.
69
+ - Don't copy the image into `public/assets/` generated assets live in R2, not the repo.
69
70
 
70
71
  ## Options
71
72
 
@@ -24,13 +24,15 @@ npx genex texture "<prompt>"
24
24
  npx genex texture "lush green grass" --terrain # seamless tiling for ground/terrain
25
25
  ```
26
26
 
27
- Blocks until ready, then saves:
27
+ Blocks until ready, then prints its public URL:
28
28
 
29
29
  ```
30
- public/assets/textures/<slug>/basecolor.<ext>
30
+ https://assets.genex.technology/generations/<id>/texture-basecolor
31
31
  ```
32
32
 
33
- Committed by `npx genex publish`, so it ships with your game.
33
+ The image lives in Genex storage (R2) and loads straight from that URL — you don't
34
+ download it and nothing is committed to your repo. The URL is permanent (local dev,
35
+ published game, and remixes alike).
34
36
 
35
37
  > **Scope:** v1 generates the **base-color (albedo)** map only. Normal / roughness
36
38
  > / AO are a planned follow-up — for now set sensible `roughness`/`metalness`
@@ -41,11 +43,9 @@ Committed by `npx genex publish`, so it ships with your game.
41
43
  ```ts
42
44
  import * as THREE from "three";
43
45
 
44
- // The extension varies by provider (production emits .png) use the EXACT
45
- // path the CLI printed when generation finished, don't assume .jpg.
46
- const map = await new THREE.TextureLoader().loadAsync(
47
- "./assets/textures/lush-green-grass/basecolor.png",
48
- );
46
+ // the URL `npx genex texture` printed (R2 sends CORS headers, so cross-origin works):
47
+ const TEXTURE_URL = "https://assets.genex.technology/generations/<id>/texture-basecolor";
48
+ const map = await new THREE.TextureLoader().loadAsync(TEXTURE_URL);
49
49
  map.colorSpace = THREE.SRGBColorSpace;
50
50
  map.wrapS = map.wrapT = THREE.RepeatWrapping;
51
51
  map.repeat.set(8, 8); // tile count — raise for large surfaces
@@ -69,8 +69,8 @@ scene.add(ground);
69
69
 
70
70
  ## Publish checklist
71
71
 
72
- - Relative path `./assets/textures/...`; `base: "./"` in `vite.config.ts`; files live under
73
- `public/assets/` so Vite ships them; `npx genex publish` ships them to R2.
72
+ - Load it from the **URL** the command printed absolute and permanent, so it resolves
73
+ the same in local dev, the published game, and remixes. Nothing to commit.
74
74
 
75
75
  ## Options
76
76
 
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: genex-explore
3
+ description: Search the curated Genex community gallery with `npx genex explore` before hand-writing hard visual or physics systems. Use when the game needs terrain, grass, water, vehicles, buildings, dungeons, flocking/boids, or advanced shader work — proven open-source Three.js implementations you can clone as a starting point or borrow parts from, credits included.
4
+ ---
5
+
6
+ # Genex Explore — proven building blocks from the community gallery
7
+
8
+ Some systems are notoriously hard to get right from scratch: believable grass,
9
+ vehicle physics, procedural buildings and dungeons, fish/boid flocking, water,
10
+ advanced shader effects. The curated community gallery holds **faithful ports of
11
+ proven open-source Three.js projects** — playable, cloneable, and licensed for
12
+ reuse. Search it before hand-writing one of those systems.
13
+
14
+ ## When to use
15
+
16
+ Before building a hard visual/physics system by hand, run:
17
+
18
+ ```bash
19
+ npx genex explore "<what you need>"
20
+ ```
21
+
22
+ Examples: `npx genex explore "grass"`, `npx genex explore "vehicle physics"`,
23
+ `npx genex explore "procedural building"`. No sign-in needed — this works even
24
+ before `genex init`.
25
+
26
+ Each result prints everything you need to act on it:
27
+
28
+ - `play:` — open it in the browser to judge whether it fits.
29
+ - `clone:` — the public repo with **editable source on `main`**.
30
+ - `upstream:` — the original source repo, with its author and license.
31
+
32
+ Add `--json` for machine-readable output.
33
+
34
+ ## How to integrate a result
35
+
36
+ **As a NEW game (start from the whole project):**
37
+
38
+ 1. `git clone <clone URL from the output> <name>` — pick a short one-word name.
39
+ 2. `cd <name>`, then `npx @genex-ai/cli-demo@latest init <name>` — choose
40
+ "Ignore files and continue" if it warns the folder isn't empty; never use
41
+ `--force`. This creates your own project; the original is untouched.
42
+ 3. `npm install`, keep `base: './'` in `vite.config`, then build your changes
43
+ and ship with `npx genex preview`.
44
+
45
+ **To BORROW parts into the current project:**
46
+
47
+ 1. Clone the result somewhere temporary, separate from your project
48
+ (e.g. `/tmp/genex-explore-src`).
49
+ 2. Study how it implements the part you need, then bring **just that** over —
50
+ real code and assets, adapted to your file paths. Don't wholesale-overwrite
51
+ your game.
52
+ 3. Reused assets must live under `public/assets/` to ship. New assets are
53
+ generated as usual with `$genex-ai-model`, `$genex-ai-skybox`,
54
+ `$genex-ai-sfx`, or `$genex-ai-texture`.
55
+
56
+ ## Credits rule (non-negotiable)
57
+
58
+ Every curated result carries an upstream link, author, and license — that
59
+ credit is part of the deal that makes these projects reusable. Whatever you
60
+ build from one:
61
+
62
+ - keep the attribution block in the README (upstream source repo URL, author,
63
+ license) exactly as the port carries it;
64
+ - keep any in-game credits screens or notices in place;
65
+ - carry the credit into anything you publish or remix from it.
66
+
67
+ If nothing matches your query, try a broader term (terrain, grass, vehicle,
68
+ water, building, dungeon, fish, shader) — or build it with the regular
69
+ `genex-threejs-*` skills instead.
@@ -28,9 +28,9 @@ validation.
28
28
 
29
29
  ## Generating real assets
30
30
 
31
- Beyond procedural code, Genex can generate **real, AI-made assets** from a prompt
32
- and drop them into `public/assets/` (shipped with your published game; load them
33
- as `./assets/...` the `public/` prefix is stripped when served):
31
+ Beyond procedural code, Genex can generate **real, AI-made assets** from a prompt.
32
+ Each command prints a permanent `assets.genex.technology` URL you load directly
33
+ the asset lives in Genex storage (R2), not your repo, so there's nothing to commit:
34
34
 
35
35
  ```bash
36
36
  npx genex model "weathered wooden barrel" # a 3D mesh (GLB)
@@ -33,6 +33,18 @@ Read [references/camera-rigs.md](references/camera-rigs.md)
33
33
  for exact chase/side/orbit rigs, projection values, transition
34
34
  rules, floating-origin shot, pointer controls, and implementation limits.
35
35
 
36
+ ## Aiming and pointer lock
37
+
38
+ Camera-aimed action — a third-person shooter reticle, first-person look —
39
+ wants **pointer lock**, not drag-orbit: request it on canvas click
40
+ (`canvas.requestPointerLock()`), drive yaw/pitch from `mousemove` deltas while
41
+ locked, and treat lock loss (Esc) as aim-paused — show a small "click to aim"
42
+ hint whenever unlocked. Keep drag-orbit for non-combat cameras (exploration,
43
+ building, spectating). Pointer lock works everywhere a Genex game runs:
44
+ standalone the game is the top-level page, and the platform's game frame
45
+ already grants the pointer-lock permission — no setup needed. Re-sync
46
+ yaw/pitch from the camera whenever lock is acquired (rule below).
47
+
36
48
  ## Non-negotiable rules
37
49
 
38
50
  - Use subject dimensions to derive offsets; do not tune one fixed distance for
@@ -260,6 +260,24 @@ instead. Don't code around any of this: no `?`/`#` URL params of yours will
260
260
  be affected, and `isEmbedded()` / the return-trip handling are internal SDK
261
261
  concerns.
262
262
 
263
+ ### Validating a draft (read before self-testing)
264
+
265
+ An unpublished draft shows the sign-in gate to any browser that isn't signed
266
+ in as the owner — **including your own test browser** (Playwright, headless
267
+ Chrome). The game still boots behind the overlay: console logs, DOM snapshots,
268
+ and key events all work — but every screenshot shows the gate, not the game,
269
+ and a gate capture is NOT visual evidence.
270
+
271
+ - Validate what the gate can't hide: a clean console, the canvas booting, the
272
+ HUD present in a DOM snapshot, controls registering.
273
+ - Do NOT work around the gate: don't dig through the SDK's internals for
274
+ undocumented URL fragments, and don't drive the user's own signed-in
275
+ browser.
276
+ - For the visual pass on a draft, hand it off plainly: "open your draft page
277
+ and tell me what you see" — the owner's view is the real check. Once the
278
+ game is **published**, any fresh browser gets in as a guest, so your own
279
+ test browser works again for full visual validation.
280
+
263
281
  ## Checklist
264
282
 
265
283
  - [ ] `initGameSentry({ slug: GENEX.slug })` is the very first call in `main.ts`.
@@ -33,6 +33,10 @@ no matter how good it looks.
33
33
  itself, or apply them only to hand-rolled movement.)
34
34
  - If an action can't fire (cooldown, no ammo), say so instantly — a click, a
35
35
  dimmed icon — silence reads as broken input.
36
+ - Camera-aimed shooting wants **pointer lock** (click to aim, Esc releases —
37
+ `$genex-threejs-camera-direction` has the rig rules); firing at a reticle
38
+ with an unlocked drag-to-turn camera feels imprecise no matter how tight
39
+ the numbers are.
36
40
 
37
41
  ## Movement: snappy beats realistic
38
42
 
@@ -32,7 +32,15 @@ this is the whole acceptance gate, and it is also the minimum for every game:
32
32
  2. Press each documented control once (keys, pointer); assert a **visible
33
33
  response** to every one — the player moves, the camera turns, the button
34
34
  fires.
35
- 3. Capture one screenshot of live gameplay.
35
+ 3. Capture one screenshot of live gameplay — of the **game**, not a sign-in
36
+ gate or loading screen. A capture of the SDK's "Sign in to play" overlay is
37
+ NOT gameplay evidence; if a draft's gate blocks the view, say so plainly
38
+ (see the embed-auth skill's "Validating a draft" note) instead of passing
39
+ the capture off as validation.
40
+ 4. In that screenshot, check oriented models: the hero faces its travel
41
+ direction, and NPCs driven by chase/aim code face their target. A model
42
+ rotated 90° reads as broken — `$genex-ai-model` has the one-time facing
43
+ fix.
36
44
 
37
45
  Everything deeper (baselines, seed sweeps, mosaics, budgets) belongs to
38
46
  visual-system work — the sequence above.