@genex-ai/cli-demo 0.21.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
@@ -1468,25 +1468,6 @@ async function runPreview(opts) {
1468
1468
  log.success("Preview deployed \u2014 unlisted draft (run `genex publish` to list it).");
1469
1469
  }
1470
1470
 
1471
- // src/commands/generate.ts
1472
- import path12 from "path";
1473
-
1474
- // src/lib/assets.ts
1475
- import fs10 from "fs/promises";
1476
- import path11 from "path";
1477
- function slugify(input) {
1478
- const s = input.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50).replace(/-+$/g, "");
1479
- return s || "asset";
1480
- }
1481
- async function downloadToFile(url, dest, headers) {
1482
- const res = await fetch(url, { headers });
1483
- if (!res.ok) throw new Error(`download failed (HTTP ${res.status}) for ${url}`);
1484
- const buf = Buffer.from(await res.arrayBuffer());
1485
- await fs10.mkdir(path11.dirname(dest), { recursive: true });
1486
- await fs10.writeFile(dest, buf);
1487
- return buf.byteLength;
1488
- }
1489
-
1490
1471
  // src/lib/sse.ts
1491
1472
  async function* readSSE(body) {
1492
1473
  const decoder = new TextDecoder();
@@ -1527,24 +1508,7 @@ async function* readSSE(body) {
1527
1508
  }
1528
1509
 
1529
1510
  // src/commands/generate.ts
1530
- var PUBLIC_PREFIX = "public/";
1531
- var KIND_DIR = {
1532
- model: "public/assets/models",
1533
- skybox: "public/assets/skybox",
1534
- sfx: "public/assets/sfx",
1535
- texture: "public/assets/textures"
1536
- };
1537
- function runtimePath(rel) {
1538
- return `./${rel.replace(PUBLIC_PREFIX, "")}`;
1539
- }
1540
1511
  var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
1541
- function fileDest(kind, slug, f) {
1542
- if (kind === "texture") {
1543
- const name = f.role.replace(/^texture-/, "");
1544
- return `public/assets/textures/${slug}/${name}.${f.ext}`;
1545
- }
1546
- return `${KIND_DIR[kind]}/${slug}.${f.ext}`;
1547
- }
1548
1512
  async function runGenerate(kind, opts) {
1549
1513
  const log = createLogger({ quiet: opts.quiet });
1550
1514
  const prompt = opts.prompt?.trim();
@@ -1591,7 +1555,7 @@ async function runGenerate(kind, opts) {
1591
1555
  return;
1592
1556
  }
1593
1557
  if (opts.noWait) {
1594
- 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.`);
1595
1559
  return;
1596
1560
  }
1597
1561
  log.step("Generating\u2026 (this can take up to a minute)");
@@ -1609,25 +1573,20 @@ async function runGenerate(kind, opts) {
1609
1573
  process.exitCode = 1;
1610
1574
  return;
1611
1575
  }
1612
- const slug = slugify(prompt);
1613
- const written = [];
1614
- try {
1615
- for (const f of view.files ?? []) {
1616
- const rel = fileDest(kind, slug, f);
1617
- const url = `${apiUrl}/api/generations/${id}/asset/${f.role}`;
1618
- const bytes = await downloadToFile(url, path12.join(process.cwd(), rel), {
1619
- Authorization: `Bearer ${token}`
1620
- });
1621
- written.push(rel);
1622
- log.success(`${rel} ${c.dim(`(${(bytes / 1024).toFixed(0)} KB)`)}`);
1623
- }
1624
- } catch (err) {
1625
- 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.");
1626
1579
  process.exitCode = 1;
1627
1580
  return;
1628
1581
  }
1629
1582
  log.plain("");
1630
- 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);
1631
1590
  }
1632
1591
  var WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
1633
1592
  var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed"]);
@@ -1706,23 +1665,21 @@ async function poll(apiUrl, token, id, onProgress) {
1706
1665
  }
1707
1666
  return null;
1708
1667
  }
1709
- function printHint(kind, written, log) {
1710
- const load = runtimePath(written[0] ?? "");
1668
+ function printHint(kind, files, log) {
1669
+ const url = files[0]?.url ?? "";
1711
1670
  const hint = {
1712
- model: `Load with GLTFLoader and add to the scene \u2014 see the genex-ai-model skill. Use the relative path "${load}".`,
1713
- skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment \u2014 see genex-ai-skybox. Path "${load}".`,
1714
- sfx: `Load with AudioLoader into a THREE.PositionalAudio (camera needs an AudioListener) \u2014 see genex-ai-sfx. Path "${load}".`,
1715
- 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.`
1716
1675
  };
1717
- log.success(
1718
- "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)."
1719
- );
1720
1676
  log.dim(` ${hint[kind]}`);
1677
+ log.dim(" Reference the URL directly in your code \u2014 don't download it into the repo.");
1721
1678
  }
1722
1679
 
1723
1680
  // src/commands/controller.ts
1724
- import fs11 from "fs/promises";
1725
- import path13 from "path";
1681
+ import fs10 from "fs/promises";
1682
+ import path11 from "path";
1726
1683
  var CONTROLLER_KINDS = ["character", "car", "drone"];
1727
1684
  var SHARED = [
1728
1685
  "shared/math.ts",
@@ -1801,8 +1758,8 @@ var CONTROLLER_FILE_SETS = {
1801
1758
  ]
1802
1759
  }
1803
1760
  };
1804
- var CODE_DEST = path13.join("src", "controllers");
1805
- var ASSETS_DEST = path13.join("public", "assets");
1761
+ var CODE_DEST = path11.join("src", "controllers");
1762
+ var ASSETS_DEST = path11.join("public", "assets");
1806
1763
  async function runController(opts) {
1807
1764
  const log = createLogger({ quiet: opts.quiet });
1808
1765
  const kind = opts.kind?.trim();
@@ -1815,31 +1772,31 @@ async function runController(opts) {
1815
1772
  process.exitCode = 1;
1816
1773
  return;
1817
1774
  }
1818
- const srcDir = path13.join(getTemplatesDir(), "controllers");
1775
+ const srcDir = path11.join(getTemplatesDir(), "controllers");
1819
1776
  const root = opts.cwd ?? process.cwd();
1820
1777
  const set = CONTROLLER_FILE_SETS[kind];
1821
1778
  log.plain(c.bold(`genex controller ${kind}`));
1822
1779
  log.plain("");
1823
- 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)}`);
1824
1781
  const plan = [
1825
- ...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) })),
1826
1783
  ...set.assets.map((rel) => ({
1827
1784
  from: rel,
1828
- rel: path13.join(ASSETS_DEST, path13.basename(rel))
1785
+ rel: path11.join(ASSETS_DEST, path11.basename(rel))
1829
1786
  }))
1830
1787
  ];
1831
1788
  let copied = 0;
1832
1789
  let skipped = 0;
1833
1790
  try {
1834
1791
  for (const file of plan) {
1835
- const dest = path13.join(root, file.rel);
1792
+ const dest = path11.join(root, file.rel);
1836
1793
  if (!opts.force && await exists2(dest)) {
1837
1794
  skipped++;
1838
1795
  log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
1839
1796
  continue;
1840
1797
  }
1841
- await fs11.mkdir(path13.dirname(dest), { recursive: true });
1842
- 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);
1843
1800
  copied++;
1844
1801
  log.dim(` ${file.rel}`);
1845
1802
  }
@@ -1871,8 +1828,8 @@ async function runController(opts) {
1871
1828
  }
1872
1829
  async function installOwnerAvatar(args) {
1873
1830
  const { root, srcDir, apiUrl, token, log } = args;
1874
- const dest = path13.join(root, ASSETS_DEST, "avatar.vrm");
1875
- 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 });
1876
1833
  if (token) {
1877
1834
  try {
1878
1835
  const res = await fetch(`${apiUrl}/api/avatars/me`, {
@@ -1884,7 +1841,7 @@ async function installOwnerAvatar(args) {
1884
1841
  const vrmRes = await fetch(me.vrmUrl);
1885
1842
  if (vrmRes.ok) {
1886
1843
  const buf = Buffer.from(await vrmRes.arrayBuffer());
1887
- await fs11.writeFile(dest, buf);
1844
+ await fs10.writeFile(dest, buf);
1888
1845
  log.dim(` public/assets/avatar.vrm (your avatar \u2014 ${(buf.length / 1e6).toFixed(1)} MB)`);
1889
1846
  return;
1890
1847
  }
@@ -1895,14 +1852,14 @@ async function installOwnerAvatar(args) {
1895
1852
  log.dim(" avatar fetch failed (offline?); using the bundled default.");
1896
1853
  }
1897
1854
  }
1898
- await fs11.copyFile(path13.join(srcDir, "assets", "default-avatar.vrm"), dest);
1855
+ await fs10.copyFile(path11.join(srcDir, "assets", "default-avatar.vrm"), dest);
1899
1856
  log.dim(
1900
1857
  token ? " public/assets/avatar.vrm (bundled default)" : " public/assets/avatar.vrm (bundled default \u2014 sign in and re-run for your own)"
1901
1858
  );
1902
1859
  }
1903
1860
  async function exists2(p) {
1904
1861
  try {
1905
- await fs11.access(p);
1862
+ await fs10.access(p);
1906
1863
  return true;
1907
1864
  } catch {
1908
1865
  return false;
@@ -1979,9 +1936,9 @@ function rank(items, query) {
1979
1936
  var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "texture"]);
1980
1937
  function getVersion() {
1981
1938
  try {
1982
- const here = path14.dirname(fileURLToPath2(import.meta.url));
1939
+ const here = path12.dirname(fileURLToPath2(import.meta.url));
1983
1940
  const pkg = JSON.parse(
1984
- readFileSync(path14.resolve(here, "..", "package.json"), "utf8")
1941
+ readFileSync(path12.resolve(here, "..", "package.json"), "utf8")
1985
1942
  );
1986
1943
  return pkg.version ?? "0.0.0";
1987
1944
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.21.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,16 +21,17 @@ 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.
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.
34
35
 
35
36
  ## Models that point somewhere must face forward
36
37
 
@@ -62,14 +63,16 @@ side — reads as broken at a glance.
62
63
 
63
64
  ## Load it into the scene
64
65
 
65
- Use Three.js `GLTFLoader` (the game serves at the domain root
66
- `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):
67
68
 
68
69
  ```ts
69
70
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
70
71
 
71
72
  const loader = new GLTFLoader();
72
- 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);
73
76
  const model = gltf.scene;
74
77
  model.scale.setScalar(1); // tune to taste
75
78
  model.position.set(0, 0, 0);
@@ -86,17 +89,14 @@ scene is a ghost: players and objects pass straight through it.
86
89
 
87
90
  ## Publish checklist (so players see the model)
88
91
 
89
- - Reference assets with **relative** paths (`./assets/...`); absolute
90
- (`/assets/...`) also resolves since the game serves at the domain root.
91
- - Set `base: "./"` in `vite.config.ts` before `npm run build` (see the scaffold
92
- prompt's publish step).
93
- - Keep generated assets under `public/assets/` — Vite only ships files under `public/`
94
- (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.
95
95
 
96
96
  ## Options
97
97
 
98
- - `--no-wait` — enqueue and return immediately (the file won't be downloaded;
99
- 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).
100
100
  - `--api-url <url>` — override the API base (local dev).
101
101
 
102
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
 
@@ -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)