@genex-ai/cli-demo 0.1.2 → 0.3.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
@@ -4,10 +4,21 @@ Set up your `~/.claude` workspace, authorize, create a game project, and publish
4
4
  This is the `cli` app of the [genex monorepo](../../README.md).
5
5
 
6
6
  ```bash
7
- genex init <name> # authorize + create the draft project
8
- genex publish # push the built game + list it in the gallery
7
+ genex init <name> # authorize + create the draft project
8
+ genex preview # build + push to the hosted draft URL (unlisted)
9
+ genex publish # build + push, then list the game in the gallery
10
+ genex model "<prompt>" # generate a 3D model → assets/models/
11
+ genex skybox "<prompt>" # generate a 360° sky → assets/skybox/
12
+ genex sfx "<prompt>" # generate a sound fx → assets/sfx/
13
+ genex texture "<prompt>" # generate a texture → assets/textures/
9
14
  ```
10
15
 
16
+ > **Invoking it.** First-time setup runs via `npx @genex-ai/cli-demo@latest init`.
17
+ > The scaffold then adds `@genex-ai/cli-demo` as a project dev dependency, so the
18
+ > rest run as **`npx genex <cmd>`** from the project dir (resolving to this CLI,
19
+ > never a different global `genex`). `genex <cmd>` works too if you install it
20
+ > globally. The bare command forms below name the command; prefix with `npx` to run.
21
+
11
22
  `genex init` does four things:
12
23
 
13
24
  1. **Scaffolds your workspace** — copies the bundled templates (skills, agents,
@@ -23,14 +34,49 @@ genex publish # push the built game + list it in the gallery
23
34
  `sshUrl`, urls) in `./.genex/project.json`. The game shows up in your
24
35
  dashboard's **My games** immediately.
25
36
 
26
- `genex publish` reads that metadata, pushes the built game to GitHub over the
27
- deploy key (best-effort `index.html` must be at the repo root), then flips the
28
- project to **published** so it appears in the public gallery. `git push` (updates
29
- the live Pages site) and the gallery flag are independent; `--no-push` skips the
30
- push. Defaults: API `https://demo-api.glotech.world`, auth site
37
+ `genex preview` and `genex publish` share a build-aware deploy core: each runs
38
+ `npm run build` (when the project has a build script), then pushes the built
39
+ `dist/` **as the repo root** over the deploy key no copying files or swapping
40
+ `index.html` by hand. A `.nojekyll` marker is written so GitHub Pages serves the
41
+ bundle verbatim, and the deploy never stages `genex_key` (the private key can't
42
+ leak). Pass `--no-build` to deploy whatever is already built.
43
+
44
+ - **`genex preview`** pushes the current build to the project's hosted draft URL
45
+ (`https://<user>.github.io/<slug>/`) without listing it publicly — re-run it
46
+ after each change to update the shareable draft.
47
+ - **`genex publish`** does the same deploy, then flips the project to
48
+ **published** so it appears in the public gallery. The deploy and the gallery
49
+ flag are independent; `--no-push` flips the flag only.
50
+
51
+ Defaults: API `https://demo-api.glotech.world`, auth site
31
52
  `https://demo-web.glotech.world` — override with `--api-url` / `--auth-url` (or
32
53
  `GENEX_API_URL` / `GENEX_AUTH_URL`) for local dev.
33
54
 
55
+ ## Generating assets
56
+
57
+ `genex model | skybox | sfx | texture "<prompt>"` turn a prompt into a real,
58
+ game-ready asset and download it into the project's `./assets/<kind>/` folder. The
59
+ command blocks until the asset is ready (poll loop), then writes the file(s):
60
+
61
+ | Command | Provider | Writes |
62
+ | --- | --- | --- |
63
+ | `genex model "<prompt>"` | Tripo | `assets/models/<slug>.glb` |
64
+ | `genex skybox "<prompt>"` | Blockade Labs | `assets/skybox/<slug>.jpg` |
65
+ | `genex sfx "<prompt>" [--duration <s>]` | ElevenLabs | `assets/sfx/<slug>.mp3` |
66
+ | `genex texture "<prompt>" [--terrain]` | Gemini | `assets/textures/<slug>/basecolor.<ext>` |
67
+
68
+ Because the file lands in the project and `genex publish` commits the whole working
69
+ directory, generated assets **ship inside the published GitHub Pages build** — load
70
+ them by **relative** path (`./assets/...`) and players get them from the same origin
71
+ (no CORS, no runtime dependency on this API). Each kind has a scaffolded
72
+ `genex-ai-<kind>` skill with the exact Three.js loader code.
73
+
74
+ Auth reuses the existing `GENEX_TOKEN` (run `genex init` first). Server-side, each
75
+ provider is keyed by an env var (`TRIPO_API_KEY`, `BLOCKADE_LABS_API_KEY`,
76
+ `ELEVENLABS_API_KEY`, `GEMINI_API_KEY`); when a key is unset that kind falls back to
77
+ a built-in **mock** provider that returns sample assets, so the flow runs keyless.
78
+ `--no-wait` enqueues without downloading.
79
+
34
80
  ## Install / run
35
81
 
36
82
  From the monorepo root (the CLI runs directly via Node — no build step):
@@ -48,11 +94,12 @@ pnpm --filter @genex-ai/cli-demo start init
48
94
  Once published, end users run it with `npx`:
49
95
 
50
96
  ```bash
51
- npx @genex-ai/cli-demo init
97
+ npx @genex-ai/cli-demo@latest init
52
98
  ```
53
99
 
54
- > Requires **Node 24** (the CLI is executed as TypeScript via Node's native
55
- > type stripping, matching the rest of the monorepo).
100
+ > The published package ships a bundled `dist/` (built with tsup) and runs on
101
+ > **Node 20**. Running from *source* in the monorepo needs **Node ≥ 24** (the
102
+ > source is executed as TypeScript via Node's native type stripping).
56
103
 
57
104
  ## Usage
58
105
 
@@ -61,8 +108,8 @@ genex init [options]
61
108
 
62
109
  Options
63
110
  --dir <path> Destination workspace (default: ~/.claude)
64
- --env <path> Path to the .env file to write (default: ./.env)
65
- --auth-url <url> Override the auth site (default: https://genex.dev)
111
+ --env <path> Token env file (default: ~/.genex/env)
112
+ --auth-url <url> Override the auth site (default: https://demo-web.glotech.world)
66
113
  --no-auth Only scaffold templates; skip authorization
67
114
  --force Overwrite existing files (default: never overwrite)
68
115
  --timeout <seconds> How long to wait for the auth redirect (default: 300)
@@ -93,8 +140,8 @@ genex init --auth-url http://localhost:3000
93
140
 
94
141
  The auth site URL is a single configurable value:
95
142
 
96
- - **Default:** `https://genex.dev` (the `DEFAULT_AUTH_URL` constant in
97
- [`src/config.ts`](src/config.ts)).
143
+ - **Default:** `https://demo-web.glotech.world` (the `DEFAULT_AUTH_URL` constant
144
+ in [`src/config.ts`](src/config.ts)).
98
145
  - **Override at runtime:** set `GENEX_AUTH_URL`, or pass `--auth-url`.
99
146
 
100
147
  ```bash
@@ -139,8 +186,8 @@ The CLI uses a loopback-redirect flow (the same pattern as `gh auth login` and
139
186
  > the exact `state` it received. A missing or mismatched `state` is rejected
140
187
  > (CSRF protection).
141
188
 
142
- 3. The CLI verifies `state`, writes `GENEX_TOKEN=<TOKEN>` to `.env`, and shows a
143
- success page in the browser.
189
+ 3. The CLI verifies `state`, writes `GENEX_TOKEN=<TOKEN>` to `~/.genex/env`, and
190
+ shows a success page in the browser.
144
191
 
145
192
  If the browser can't be opened, the URL is printed and — when run in an
146
193
  interactive terminal — a pasted token is also accepted.
@@ -165,6 +212,28 @@ pnpm test # node --test test/*.test.ts
165
212
  pnpm start init --no-auth --dir ./tmp-claude
166
213
  ```
167
214
 
215
+ ### Testing a command like an end user
216
+
217
+ `pnpm start <cmd>` runs a single command from source — fine for quick checks. To
218
+ exercise the CLI as a *global* command (the way users invoke `genex`), symlink a
219
+ `genexd` shim onto your PATH pointing at the source entrypoint. No build step —
220
+ it runs the TypeScript directly and picks up edits instantly:
221
+
222
+ ```bash
223
+ # one-time, from this package directory: put `genexd` on PATH
224
+ ln -sf "$PWD/src/index.ts" "$(dirname "$(which node)")/genexd"
225
+
226
+ genexd --help # works from any directory
227
+ genexd init --no-auth --dir ./tmp-claude
228
+
229
+ rm "$(dirname "$(which node)")/genexd" # remove it when done
230
+ ```
231
+
232
+ Use `genexd` (not `genex`) so the local shim never shadows a globally installed
233
+ published build — this is the same shim the local scaffold flow uses (see
234
+ `scaffold_prompt.local.md`). `npm link` is unnecessary here: it would require a
235
+ `dist/` build, whereas `genexd` / `pnpm start` run the source as-is.
236
+
168
237
  Relative imports use explicit `.ts` extensions and only erasable TypeScript
169
238
  syntax is allowed — both are monorepo-wide rules (see the root README).
170
239
 
@@ -185,9 +254,9 @@ templates/ copied into ~/.claude by `genex init`
185
254
 
186
255
  ## Publishing
187
256
 
188
- The package ships its TypeScript source (no bundle); consumers run it on
189
- Node ≥ 24. The published tarball includes only `src/`, `templates/`, `README.md`,
190
- and `LICENSE` (see the `files` field in `package.json`).
257
+ The package ships a bundled `dist/` (built with tsup via `prepack`); consumers
258
+ run it on Node ≥ 20. The published tarball includes `dist/`, `templates/`,
259
+ `README.md`, and `LICENSE` (see the `files` field in `package.json`).
191
260
 
192
261
  ```bash
193
262
  pnpm --filter @genex-ai/cli-demo publish --access public
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { readFileSync } from "fs";
5
- import path8 from "path";
5
+ import path10 from "path";
6
6
  import { fileURLToPath as fileURLToPath2 } from "url";
7
7
 
8
8
  // src/commands/init.ts
@@ -396,7 +396,7 @@ async function createDraftProject(opts) {
396
396
  return null;
397
397
  }
398
398
  log.success(`Created project ${c.cyan(project.slug)}.`);
399
- if (project.playUrl) log.dim(` play (after publish): ${project.playUrl}`);
399
+ if (project.playUrl) log.dim(` play (after preview/publish): ${project.playUrl}`);
400
400
  log.dim(` dashboard: ${dashboardUrl}/dashboard`);
401
401
  return {
402
402
  id: project.id,
@@ -701,9 +701,10 @@ async function runInit(opts) {
701
701
  log.success("All set. \u{1F680}");
702
702
  }
703
703
 
704
- // src/commands/publish.ts
704
+ // src/lib/deploy.ts
705
705
  import { spawn as spawn4 } from "child_process";
706
706
  import fs5 from "fs/promises";
707
+ import os2 from "os";
707
708
  import path7 from "path";
708
709
  function run(cmd, args, env) {
709
710
  return new Promise((resolve) => {
@@ -722,6 +723,113 @@ function run(cmd, args, env) {
722
723
  child.on("close", (code2) => resolve({ code: code2 ?? -1, out, err }));
723
724
  });
724
725
  }
726
+ async function deployGame(sshUrl, opts, log) {
727
+ const cwd = process.cwd();
728
+ if (!opts.noBuild && await hasBuildScript(cwd)) {
729
+ log.step("Building the production bundle\u2026");
730
+ const built = await run("npm", ["run", "build"]);
731
+ if (built.code !== 0) {
732
+ log.error("Build failed \u2014 your game was NOT deployed.");
733
+ const tail = (built.err || built.out).trim().split("\n").slice(-12).join("\n");
734
+ if (tail) log.dim(tail);
735
+ return false;
736
+ }
737
+ log.success("Built.");
738
+ }
739
+ const distDir = path7.join(cwd, "dist");
740
+ const siteDir = await isDir(distDir) ? distDir : cwd;
741
+ if (siteDir === cwd) {
742
+ await writeGitignore(cwd, log);
743
+ }
744
+ const rel = path7.relative(cwd, siteDir) || ".";
745
+ try {
746
+ await fs5.access(path7.join(siteDir, "index.html"));
747
+ } catch {
748
+ log.warn(`No index.html in ${rel} \u2014 GitHub Pages needs one to serve the game.`);
749
+ }
750
+ await warnIfAbsolutePaths(siteDir, log);
751
+ await fs5.writeFile(path7.join(siteDir, ".nojekyll"), "");
752
+ const keyPath = path7.resolve(cwd, KEY_NAME);
753
+ if (!await fileExists(keyPath)) {
754
+ log.warn(`No deploy key (${KEY_NAME}) here \u2014 run \`genex init\` in this folder first.`);
755
+ return false;
756
+ }
757
+ const gitDir = await fs5.mkdtemp(path7.join(os2.tmpdir(), "genex-deploy-"));
758
+ const gitEnv = { GIT_DIR: gitDir, GIT_WORK_TREE: siteDir };
759
+ try {
760
+ if ((await run("git", ["init", "-q"], gitEnv)).code !== 0) {
761
+ log.warn("git init failed \u2014 the game was not pushed.");
762
+ return false;
763
+ }
764
+ await run("git", ["add", "-A"], gitEnv);
765
+ const tracked = await run("git", ["ls-files"], gitEnv);
766
+ if (/(^|\/)genex_key(\.pub)?$/m.test(tracked.out)) {
767
+ log.error(`Refusing to deploy: ${KEY_NAME} is staged. Add it to .gitignore and retry.`);
768
+ return false;
769
+ }
770
+ const commit = await run(
771
+ "git",
772
+ ["-c", "user.email=agent@genex.local", "-c", "user.name=genex", "commit", "-q", "-m", "build"],
773
+ gitEnv
774
+ );
775
+ if (commit.code !== 0 && !/nothing to commit/i.test(commit.out + commit.err)) {
776
+ log.warn("Nothing to commit \u2014 the build produced no files.");
777
+ return false;
778
+ }
779
+ log.step("Pushing your game over SSH\u2026");
780
+ const push = await run("git", ["push", "-q", sshUrl, "+HEAD:main"], {
781
+ ...gitEnv,
782
+ GIT_SSH_COMMAND: `ssh -i ${keyPath} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new`
783
+ });
784
+ if (push.code === 0) {
785
+ log.success("Pushed.");
786
+ return true;
787
+ }
788
+ log.warn("git push failed \u2014 your live game was NOT updated.");
789
+ const tail = push.err.trim().split("\n").slice(-2).join(" ");
790
+ if (tail) log.dim(` ${tail}`);
791
+ return false;
792
+ } finally {
793
+ await fs5.rm(gitDir, { recursive: true, force: true }).catch(() => {
794
+ });
795
+ }
796
+ }
797
+ async function hasBuildScript(cwd) {
798
+ try {
799
+ const pkg = JSON.parse(await fs5.readFile(path7.join(cwd, "package.json"), "utf8"));
800
+ return Boolean(pkg.scripts?.build);
801
+ } catch {
802
+ return false;
803
+ }
804
+ }
805
+ async function isDir(p) {
806
+ try {
807
+ return (await fs5.stat(p)).isDirectory();
808
+ } catch {
809
+ return false;
810
+ }
811
+ }
812
+ async function fileExists(p) {
813
+ try {
814
+ await fs5.access(p);
815
+ return true;
816
+ } catch {
817
+ return false;
818
+ }
819
+ }
820
+ async function warnIfAbsolutePaths(siteDir, log) {
821
+ let html;
822
+ try {
823
+ html = await fs5.readFile(path7.join(siteDir, "index.html"), "utf8");
824
+ } catch {
825
+ return;
826
+ }
827
+ if (/(?:src|href)="\/(?!\/)/.test(html)) {
828
+ log.warn("Built index.html uses absolute paths \u2014 set `base: './'` in vite.config so assets load under the Pages subpath.");
829
+ }
830
+ }
831
+
832
+ // src/commands/publish.ts
725
833
  async function runPublish(opts) {
726
834
  const log = createLogger({ quiet: opts.quiet });
727
835
  log.plain(c.bold("genex publish"));
@@ -741,11 +849,11 @@ async function runPublish(opts) {
741
849
  const apiUrl = getApiUrl(opts.apiUrl ?? meta.apiUrl);
742
850
  let pushed = null;
743
851
  if (opts.noPush) {
744
- log.info("Skipping git push (--no-push).");
852
+ log.info("Skipping build + git push (--no-push).");
745
853
  } else {
746
- pushed = await pushGame(meta.sshUrl, log);
854
+ pushed = await deployGame(meta.sshUrl, { noBuild: opts.noBuild }, log);
747
855
  }
748
- log.step("Publishing to the gallery\u2026");
856
+ log.step("Listing it in the gallery\u2026");
749
857
  let res;
750
858
  try {
751
859
  const body = {};
@@ -771,7 +879,7 @@ async function runPublish(opts) {
771
879
  log.warn(
772
880
  "Listed in the gallery, but the game wasn't pushed \u2014 the play URL won't work until the push succeeds."
773
881
  );
774
- log.dim(" Fix the push error above and re-run `genex publish`.");
882
+ log.dim(" Fix the error above and re-run `genex publish`.");
775
883
  } else {
776
884
  log.success("Published. \u{1F389}");
777
885
  }
@@ -780,45 +888,190 @@ async function runPublish(opts) {
780
888
  if (pushed === true) log.dim(" (GitHub Pages rebuilds ~30\u201390s after a push.)");
781
889
  }
782
890
  }
783
- async function pushGame(sshUrl, log) {
784
- try {
785
- await fs5.access(path7.join(process.cwd(), "index.html"));
786
- } catch {
787
- log.warn("No index.html at the project root \u2014 GitHub Pages needs one to serve the game.");
891
+
892
+ // src/commands/preview.ts
893
+ async function runPreview(opts) {
894
+ const log = createLogger({ quiet: opts.quiet });
895
+ log.plain(c.bold("genex preview"));
896
+ log.plain("");
897
+ const meta = await readProject();
898
+ if (!meta) {
899
+ log.error("No genex project here. Run `genex init` in this directory first.");
900
+ process.exitCode = 1;
901
+ return;
788
902
  }
789
- const isRepo = (await run("git", ["rev-parse", "--git-dir"])).code === 0;
790
- if (!isRepo) {
791
- log.step("Initializing git\u2026");
792
- if ((await run("git", ["init"])).code !== 0) {
793
- log.warn("git init failed \u2014 the game was not pushed (use `genex publish --no-push` to silence).");
794
- return false;
903
+ const pushed = await deployGame(meta.sshUrl, { noBuild: opts.noBuild }, log);
904
+ log.plain("");
905
+ if (!pushed) {
906
+ log.warn("Preview not updated \u2014 see the error above.");
907
+ process.exitCode = 1;
908
+ return;
909
+ }
910
+ log.success("Preview deployed. \u{1F310}");
911
+ if (meta.playUrl) {
912
+ log.dim(` ${meta.playUrl}`);
913
+ log.dim(" (GitHub Pages rebuilds ~30\u201390s after a push; it stays an unlisted draft until you publish.)");
914
+ }
915
+ }
916
+
917
+ // src/commands/generate.ts
918
+ import path9 from "path";
919
+
920
+ // src/lib/assets.ts
921
+ import fs6 from "fs/promises";
922
+ import path8 from "path";
923
+ function slugify(input) {
924
+ const s = input.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50).replace(/-+$/g, "");
925
+ return s || "asset";
926
+ }
927
+ async function downloadToFile(url, dest, headers) {
928
+ const res = await fetch(url, { headers });
929
+ if (!res.ok) throw new Error(`download failed (HTTP ${res.status}) for ${url}`);
930
+ const buf = Buffer.from(await res.arrayBuffer());
931
+ await fs6.mkdir(path8.dirname(dest), { recursive: true });
932
+ await fs6.writeFile(dest, buf);
933
+ return buf.byteLength;
934
+ }
935
+
936
+ // src/commands/generate.ts
937
+ var KIND_DIR = {
938
+ model: "assets/models",
939
+ skybox: "assets/skybox",
940
+ sfx: "assets/sfx",
941
+ texture: "assets/textures"
942
+ };
943
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
944
+ function fileDest(kind, slug, f) {
945
+ if (kind === "texture") {
946
+ const name = f.role.replace(/^texture-/, "");
947
+ return `assets/textures/${slug}/${name}.${f.ext}`;
948
+ }
949
+ return `${KIND_DIR[kind]}/${slug}.${f.ext}`;
950
+ }
951
+ async function runGenerate(kind, opts) {
952
+ const log = createLogger({ quiet: opts.quiet });
953
+ const prompt = opts.prompt?.trim();
954
+ if (!prompt) {
955
+ log.error(`Missing prompt. Usage: ${c.cyan(`genex ${kind} "<prompt>"`)}`);
956
+ process.exitCode = 1;
957
+ return;
958
+ }
959
+ const token = opts.token ?? await readUserToken(opts.envPath);
960
+ if (!token) {
961
+ log.error("Not authorized. Run `genex init` first to sign in.");
962
+ process.exitCode = 1;
963
+ return;
964
+ }
965
+ const meta = await readProject();
966
+ const apiUrl = getApiUrl(opts.apiUrl ?? meta?.apiUrl);
967
+ const options = {};
968
+ if (kind === "texture" && opts.terrain) options.terrain = true;
969
+ if (kind === "sfx" && opts.duration) options.durationSeconds = opts.duration;
970
+ log.plain(c.bold(`genex ${kind}`));
971
+ log.dim(` ${prompt}`);
972
+ log.plain("");
973
+ let id;
974
+ try {
975
+ const res = await fetch(`${apiUrl}/api/generations`, {
976
+ method: "POST",
977
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
978
+ body: JSON.stringify({ kind, prompt, options })
979
+ });
980
+ if (res.status === 401) {
981
+ log.error("Unauthorized \u2014 your token may have expired. Re-run `genex init`.");
982
+ process.exitCode = 1;
983
+ return;
795
984
  }
985
+ if (!res.ok) {
986
+ log.error(`Generation request failed (HTTP ${res.status}).`);
987
+ process.exitCode = 1;
988
+ return;
989
+ }
990
+ ({ id } = await res.json());
991
+ } catch (err) {
992
+ log.error(`Couldn't reach the API at ${apiUrl}: ${String(err)}`);
993
+ process.exitCode = 1;
994
+ return;
796
995
  }
797
- await run("git", ["add", "-A"]);
798
- const commit = await run("git", ["commit", "-m", "build"]);
799
- if (commit.code !== 0 && !/nothing to commit/i.test(commit.out + commit.err)) {
800
- log.dim(" (no new commit to push)");
996
+ if (opts.noWait) {
997
+ log.success(`Queued (${id}). Re-run without --no-wait to fetch the finished asset.`);
998
+ return;
801
999
  }
802
- log.step("Pushing your game over SSH\u2026");
803
- const push = await run("git", ["push", sshUrl, "+HEAD:main"], {
804
- GIT_SSH_COMMAND: `ssh -i ./${KEY_NAME} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new`
805
- });
806
- if (push.code === 0) {
807
- log.success("Pushed to main.");
808
- return true;
1000
+ log.step("Generating\u2026 (this can take up to a minute)");
1001
+ const view = await poll(apiUrl, token, id, (p) => log.dim(` ${p}%`));
1002
+ if (!view) {
1003
+ log.error("Timed out waiting for the generation.");
1004
+ process.exitCode = 1;
1005
+ return;
1006
+ }
1007
+ if (view.status !== "completed") {
1008
+ log.error(`Generation ${view.status}${view.error ? `: ${view.error}` : ""}.`);
1009
+ process.exitCode = 1;
1010
+ return;
809
1011
  }
810
- log.warn("git push failed \u2014 your live game was NOT updated.");
811
- const tail = push.err.trim().split("\n").slice(-2).join(" ");
812
- if (tail) log.dim(` ${tail}`);
813
- return false;
1012
+ const slug = slugify(prompt);
1013
+ const written = [];
1014
+ try {
1015
+ for (const f of view.files ?? []) {
1016
+ const rel = fileDest(kind, slug, f);
1017
+ const url = `${apiUrl}/api/generations/${id}/asset/${f.role}`;
1018
+ const bytes = await downloadToFile(url, path9.join(process.cwd(), rel), {
1019
+ Authorization: `Bearer ${token}`
1020
+ });
1021
+ written.push(rel);
1022
+ log.success(`${rel} ${c.dim(`(${(bytes / 1024).toFixed(0)} KB)`)}`);
1023
+ }
1024
+ } catch (err) {
1025
+ log.error(`Download failed: ${String(err)}`);
1026
+ process.exitCode = 1;
1027
+ return;
1028
+ }
1029
+ log.plain("");
1030
+ printHint(kind, written, log);
1031
+ }
1032
+ async function poll(apiUrl, token, id, onProgress) {
1033
+ const deadline = Date.now() + 10 * 60 * 1e3;
1034
+ let last = -1;
1035
+ while (Date.now() < deadline) {
1036
+ try {
1037
+ const res = await fetch(`${apiUrl}/api/generations/${id}`, {
1038
+ headers: { Authorization: `Bearer ${token}` }
1039
+ });
1040
+ if (res.ok) {
1041
+ const { generation } = await res.json();
1042
+ if (generation.progress !== last) {
1043
+ last = generation.progress;
1044
+ onProgress(generation.progress);
1045
+ }
1046
+ if (generation.status === "completed" || generation.status === "failed") {
1047
+ return generation;
1048
+ }
1049
+ }
1050
+ } catch {
1051
+ }
1052
+ await sleep(3e3);
1053
+ }
1054
+ return null;
1055
+ }
1056
+ function printHint(kind, written, log) {
1057
+ const primary = written[0] ?? "";
1058
+ const hint = {
1059
+ model: `Load with GLTFLoader and add to the scene \u2014 see the genex-ai-model skill. Use the relative path "./${primary}".`,
1060
+ skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment \u2014 see genex-ai-skybox. Path "./${primary}".`,
1061
+ sfx: `Load with AudioLoader into a THREE.PositionalAudio (camera needs an AudioListener) \u2014 see genex-ai-sfx. Path "./${primary}".`,
1062
+ texture: `Load with TextureLoader, set RepeatWrapping, build a MeshStandardMaterial \u2014 see genex-ai-texture. Path "./${primary}".`
1063
+ };
1064
+ log.success("Done. Asset saved into ./assets (commit it \u2014 `genex publish` ships it with the game).");
1065
+ log.dim(` ${hint[kind]}`);
814
1066
  }
815
1067
 
816
1068
  // src/index.ts
1069
+ var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "texture"]);
817
1070
  function getVersion() {
818
1071
  try {
819
- const here = path8.dirname(fileURLToPath2(import.meta.url));
1072
+ const here = path10.dirname(fileURLToPath2(import.meta.url));
820
1073
  const pkg = JSON.parse(
821
- readFileSync(path8.resolve(here, "..", "package.json"), "utf8")
1074
+ readFileSync(path10.resolve(here, "..", "package.json"), "utf8")
822
1075
  );
823
1076
  return pkg.version ?? "0.0.0";
824
1077
  } catch {
@@ -829,7 +1082,19 @@ var HELP = `${c.bold("genex")} \u2014 set up your ~/.claude workspace, authorize
829
1082
 
830
1083
  ${c.bold("Usage")}
831
1084
  genex init [<name>] [options] Scaffold + authorize + create the draft project.
832
- genex publish [options] Push the built game and list it in the gallery.
1085
+ genex preview [options] Build + push to the hosted draft URL (unlisted).
1086
+ genex publish [options] Build + push, then list the game in the gallery.
1087
+ genex model "<prompt>" [options] Generate a 3D model (GLB) into ./assets/models.
1088
+ genex skybox "<prompt>" [options] Generate a skybox (equirect) into ./assets/skybox.
1089
+ genex sfx "<prompt>" [options] Generate a sound effect (mp3) into ./assets/sfx.
1090
+ genex texture "<prompt>" [options] Generate a PBR texture into ./assets/textures.
1091
+
1092
+ ${c.bold("Options for the generators (`model` `skybox` `sfx` `texture`)")}
1093
+ --terrain (texture) seamless tiling surface for terrain/ground.
1094
+ --duration <sec> (sfx) target clip length in seconds.
1095
+ --no-wait Enqueue only; don't block waiting for the asset.
1096
+ --api-url <url> Override the API base URL.
1097
+ --env <path> Token env file (default: ~/.genex/env).
833
1098
 
834
1099
  ${c.bold("Options for `init`")}
835
1100
  <name> Project name (positional; default: current directory name).
@@ -843,11 +1108,12 @@ ${c.bold("Options for `init`")}
843
1108
  --force Overwrite existing files (default: never overwrite).
844
1109
  --timeout <seconds> How long to wait for the auth redirect (default: 300).
845
1110
 
846
- ${c.bold("Options for `publish`")}
847
- --no-push Skip the git push; only flip the gallery flag.
848
- --title <title> Gallery title.
849
- --description <text> Gallery description.
850
- --api-url <url> Override the API base URL.
1111
+ ${c.bold("Options for `preview` / `publish`")}
1112
+ --no-build Skip the build step; deploy whatever is already built.
1113
+ --no-push (publish) Skip build + push; only flip the gallery flag.
1114
+ --title <title> (publish) Gallery title.
1115
+ --description <text> (publish) Gallery description.
1116
+ --api-url <url> (publish) Override the API base URL.
851
1117
  --env <path> Token env file (default: ~/.genex/env).
852
1118
 
853
1119
  ${c.bold("Global")}
@@ -864,8 +1130,13 @@ ${c.bold("Environment")}
864
1130
  ${c.bold("Examples")}
865
1131
  genex init my-game
866
1132
  genex init my-game --api-url http://localhost:3000 --auth-url http://localhost:5173
1133
+ genex preview
867
1134
  genex publish
868
1135
  genex publish --no-push --title "My Game"
1136
+ genex model "weathered wooden barrel with iron bands"
1137
+ genex skybox "golden hour over a misty mountain range"
1138
+ genex sfx "punchy laser zap" --duration 2
1139
+ genex texture "mossy cracked cobblestone" --terrain
869
1140
  `;
870
1141
  function parseArgs(argv) {
871
1142
  const parsed = {
@@ -883,7 +1154,8 @@ function parseArgs(argv) {
883
1154
  "--name",
884
1155
  "--title",
885
1156
  "--description",
886
- "--timeout"
1157
+ "--timeout",
1158
+ "--duration"
887
1159
  ]);
888
1160
  let i = 0;
889
1161
  while (i < argv.length) {
@@ -904,6 +1176,15 @@ function parseArgs(argv) {
904
1176
  case "--no-push":
905
1177
  parsed.options.noPush = true;
906
1178
  break;
1179
+ case "--no-build":
1180
+ parsed.options.noBuild = true;
1181
+ break;
1182
+ case "--no-wait":
1183
+ parsed.options.noWait = true;
1184
+ break;
1185
+ case "--terrain":
1186
+ parsed.options.terrain = true;
1187
+ break;
907
1188
  case "--force":
908
1189
  parsed.options.force = true;
909
1190
  break;
@@ -969,6 +1250,14 @@ function applyValueFlag(options, flag, value) {
969
1250
  options.timeoutSec = n;
970
1251
  break;
971
1252
  }
1253
+ case "--duration": {
1254
+ const n = Number(value);
1255
+ if (!Number.isFinite(n) || n <= 0) {
1256
+ throw new Error(`Invalid --duration value: ${value}`);
1257
+ }
1258
+ options.duration = n;
1259
+ break;
1260
+ }
972
1261
  }
973
1262
  }
974
1263
  async function main() {
@@ -995,10 +1284,20 @@ async function main() {
995
1284
  log.plain(HELP);
996
1285
  return;
997
1286
  }
1287
+ if (GEN_KINDS.has(parsed.command)) {
1288
+ await runGenerate(parsed.command, {
1289
+ ...parsed.options,
1290
+ prompt: parsed.options.name
1291
+ });
1292
+ return;
1293
+ }
998
1294
  switch (parsed.command) {
999
1295
  case "init":
1000
1296
  await runInit(parsed.options);
1001
1297
  break;
1298
+ case "preview":
1299
+ await runPreview(parsed.options);
1300
+ break;
1002
1301
  case "publish":
1003
1302
  await runPublish(parsed.options);
1004
1303
  break;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.1.2",
4
- "description": "Set up your ~/.claude workspace, authorize, create a game project, and publish (genex CLI).",
3
+ "version": "0.3.0",
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": {
7
7
  "genex": "./dist/index.js"
@@ -15,6 +15,14 @@
15
15
  "engines": {
16
16
  "node": ">=20"
17
17
  },
18
+ "scripts": {
19
+ "build": "tsup",
20
+ "prepack": "pnpm build",
21
+ "start": "node src/index.ts",
22
+ "dev": "node --watch src/index.ts",
23
+ "typecheck": "tsc --noEmit",
24
+ "test": "node --test test/*.test.ts"
25
+ },
18
26
  "keywords": [
19
27
  "cli",
20
28
  "claude",
@@ -38,12 +46,5 @@
38
46
  "bugs": {
39
47
  "url": "https://github.com/me-ai-org/genex-demo/issues"
40
48
  },
41
- "homepage": "https://github.com/me-ai-org/genex-demo/tree/main/apps/cli#readme",
42
- "scripts": {
43
- "build": "tsup",
44
- "start": "node src/index.ts",
45
- "dev": "node --watch src/index.ts",
46
- "typecheck": "tsc --noEmit",
47
- "test": "node --test test/*.test.ts"
48
- }
49
- }
49
+ "homepage": "https://github.com/me-ai-org/genex-demo/tree/main/apps/cli#readme"
50
+ }
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: genex-ai-model
3
+ description: Generate a real 3D model (GLB mesh) from a text prompt with `npx genex model`, then load it into the Three.js scene. Use when the user wants a specific prop, item, character, vehicle, building, or any concrete object as an actual mesh ("add a wooden barrel", "I need a spaceship", "generate a sword") rather than a procedurally-coded shape.
4
+ ---
5
+
6
+ # Genex AI · Model
7
+
8
+ Turn a text prompt into a real, game-ready **GLB** and drop it into the project.
9
+
10
+ ## When to use this vs. procedural geometry
11
+
12
+ - **Use `npx genex model`** for a specific, recognizable object — a barrel, a chair, a
13
+ sword, a spaceship, an animal. You get a real textured mesh.
14
+ - **Use `$genex-threejs-procedural-geometry`** for parametric/abstract shapes,
15
+ terrain, or anything you want to generate in code (infinite variations, no files).
16
+
17
+ ## Run
18
+
19
+ ```bash
20
+ npx genex model "<prompt>"
21
+ ```
22
+
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:
25
+
26
+ ```
27
+ assets/models/<slug>.glb
28
+ ```
29
+
30
+ `<slug>` is derived from the prompt (e.g. `weathered-wooden-barrel.glb`). The file
31
+ is **committed by `npx genex publish`**, so it ships inside your published game.
32
+
33
+ ## Load it into the scene
34
+
35
+ Use Three.js `GLTFLoader` with the **relative** path (so it works under the
36
+ published subpath `https://<user>.github.io/<slug>/`):
37
+
38
+ ```ts
39
+ import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
40
+
41
+ const loader = new GLTFLoader();
42
+ const gltf = await loader.loadAsync("./assets/models/weathered-wooden-barrel.glb");
43
+ const model = gltf.scene;
44
+ model.scale.setScalar(1); // tune to taste
45
+ model.position.set(0, 0, 0);
46
+ scene.add(model);
47
+ ```
48
+
49
+ To place many copies, `model.clone()` per instance. For animated GLBs, drive
50
+ `gltf.animations` with a `THREE.AnimationMixer`.
51
+
52
+ ## Publish checklist (so players see the model)
53
+
54
+ - Reference assets with **relative** paths (`./assets/...`), never absolute
55
+ (`/assets/...`) — GitHub Pages serves the game at a subpath.
56
+ - Set `base: "./"` in `vite.config.ts` before `npm run build` (see the scaffold
57
+ prompt's publish step).
58
+ - Commit the `assets/` folder — `npx genex publish` pushes it with the game.
59
+
60
+ ## Options
61
+
62
+ - `--no-wait` — enqueue and return immediately (the file won't be downloaded;
63
+ re-run without `--no-wait` to fetch it).
64
+ - `--api-url <url>` — override the API base (local dev).
65
+
66
+ ## Troubleshooting
67
+
68
+ - **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
69
+ - **The mesh looks low-detail / wrong** — make the prompt more specific
70
+ (materials, style, parts) and regenerate; each run is a fresh asset.
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: genex-ai-sfx
3
+ description: Generate a real sound effect (mp3) from a text prompt with `npx genex sfx`, then play it in Three.js (positional or global audio). Use when the user wants a specific sound — a gunshot, footstep, pickup chime, explosion, engine hum, UI click, whoosh — triggered on a game event.
4
+ ---
5
+
6
+ # Genex AI · SFX
7
+
8
+ Turn a prompt into a real **mp3** sound effect and wire it to a game event.
9
+
10
+ ## Run
11
+
12
+ ```bash
13
+ npx genex sfx "<prompt>"
14
+ npx genex sfx "punchy laser zap, short and dry" --duration 2
15
+ ```
16
+
17
+ `--duration <sec>` (0.5–22) sets a target length; omit it to let the model pick.
18
+ Blocks until ready, then saves:
19
+
20
+ ```
21
+ assets/sfx/<slug>.mp3
22
+ ```
23
+
24
+ Committed by `npx genex publish`, so it ships with your game.
25
+
26
+ ## Play it in Three.js
27
+
28
+ Audio needs **one `AudioListener` on the camera**. Then load the clip and play it
29
+ on an event. Browsers block autoplay — start audio after a user gesture (click/keydown).
30
+
31
+ **Positional** (3D, attenuates with distance — attach to an object):
32
+
33
+ ```ts
34
+ import * as THREE from "three";
35
+
36
+ const listener = new THREE.AudioListener();
37
+ camera.add(listener);
38
+
39
+ const buffer = await new THREE.AudioLoader().loadAsync("./assets/sfx/laser-zap.mp3");
40
+
41
+ const sound = new THREE.PositionalAudio(listener);
42
+ sound.setBuffer(buffer);
43
+ sound.setRefDistance(5);
44
+ laserMesh.add(sound); // follows the object
45
+
46
+ // on fire:
47
+ if (!sound.isPlaying) sound.play();
48
+ ```
49
+
50
+ **Global** (UI/non-spatial — same volume everywhere): use `new THREE.Audio(listener)`
51
+ instead of `PositionalAudio` and don't attach it to a mesh.
52
+
53
+ Reuse one loaded `buffer` across many plays; create a fresh `Audio`/`PositionalAudio`
54
+ (or call `sound.play()` again once stopped) per trigger.
55
+
56
+ ## Publish checklist
57
+
58
+ - Relative path `./assets/sfx/...`; `base: "./"` in `vite.config.ts`; commit `assets/`.
59
+
60
+ ## Troubleshooting
61
+
62
+ - **No sound** — the `AudioContext` is suspended until a user gesture; trigger the
63
+ first play from a click/keydown. Confirm the camera has an `AudioListener`.
64
+ - **Too quiet/loud** — `sound.setVolume(0..1)`; for positional, tune
65
+ `setRefDistance` / `setRolloffFactor`.
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: genex-ai-skybox
3
+ description: Generate a real 360° skybox (equirectangular panorama) from a text prompt with `npx genex skybox`, then load it as the scene background + environment lighting in Three.js. Use when the user wants a specific photoreal/painted sky or backdrop ("sunset over the ocean", "alien purple nebula", "foggy pine forest") as an image, rather than a procedural/shader sky.
4
+ ---
5
+
6
+ # Genex AI · Skybox
7
+
8
+ Turn a prompt into a 360° **equirectangular** panorama that wraps the scene and
9
+ also lights it (image-based lighting).
10
+
11
+ ## When to use this vs. a procedural sky
12
+
13
+ - **Use `npx genex skybox`** for a specific, recognizable sky/backdrop you can
14
+ describe — "golden hour over misty mountains", "stormy alien sky", "city at
15
+ night". You get a real image.
16
+ - **Use `$genex-threejs-atmosphere-aerial-perspective`** (or
17
+ `$genex-threejs-volumetric-clouds`) for a fully procedural, animated,
18
+ time-of-day sky generated in shaders.
19
+
20
+ ## Run
21
+
22
+ ```bash
23
+ npx genex skybox "<prompt>"
24
+ ```
25
+
26
+ Blocks until ready, then saves:
27
+
28
+ ```
29
+ assets/skybox/<slug>.jpg
30
+ ```
31
+
32
+ The JPG is **committed by `npx genex publish`**, so it ships with your game.
33
+
34
+ ## Load it as background + environment
35
+
36
+ Load the equirect JPG, mark it equirectangular, and use it for both the visible
37
+ background and the lighting:
38
+
39
+ ```ts
40
+ import * as THREE from "three";
41
+
42
+ const texture = await new THREE.TextureLoader().loadAsync(
43
+ "./assets/skybox/golden-hour-over-misty-mountains.jpg",
44
+ );
45
+ texture.mapping = THREE.EquirectangularReflectionMapping;
46
+ texture.colorSpace = THREE.SRGBColorSpace;
47
+
48
+ scene.background = texture; // visible sky
49
+ scene.environment = texture; // image-based lighting on PBR materials
50
+ ```
51
+
52
+ For sharper reflections/lighting, pre-filter it with `PMREMGenerator`:
53
+
54
+ ```ts
55
+ const pmrem = new THREE.PMREMGenerator(renderer);
56
+ const envMap = pmrem.fromEquirectangular(texture).texture;
57
+ scene.environment = envMap;
58
+ scene.background = texture; // keep the raw texture for the visible sky
59
+ texture.dispose; // (dispose the PMREM source later if you stop using it)
60
+ ```
61
+
62
+ ## Publish checklist
63
+
64
+ - Use the **relative** path `./assets/skybox/...` (GitHub Pages serves at a subpath).
65
+ - `base: "./"` in `vite.config.ts` before `npm run build`.
66
+ - Commit `assets/` — `npx genex publish` pushes it with the game.
67
+
68
+ ## Troubleshooting
69
+
70
+ - **Sky looks too dark/bright** — adjust `renderer.toneMappingExposure`, or scale
71
+ `scene.environment` influence via material `envMapIntensity`.
72
+ - **Seam/pole artifacts** — that's inherent to equirect images; keep the camera
73
+ away from looking straight up/down, or use PMREM for the lighting.
@@ -0,0 +1,76 @@
1
+ ---
2
+ name: genex-ai-texture
3
+ description: Generate a real photoreal surface texture (PBR base-color image) from a text prompt with `npx genex texture`, then apply it as a tiling material on meshes or terrain in Three.js. Use `--terrain` for seamless ground. Use when the user wants a specific photoreal material on a surface ("rusty metal floor", "mossy cobblestone path", "grassy terrain") rather than a procedural/shader material.
4
+ ---
5
+
6
+ # Genex AI · Texture
7
+
8
+ Turn a prompt into a real, tileable **base-color** texture image and apply it as a
9
+ material on a primitive, a mesh, or terrain.
10
+
11
+ ## When to use this vs. procedural materials
12
+
13
+ - **Use `npx genex texture`** for a specific photoreal surface you can describe —
14
+ "weathered Roman cobblestone", "cracked desert clay", "oak planks". You get a
15
+ real raster image.
16
+ - **Use `$genex-threejs-procedural-materials`** for stylized/abstract or
17
+ fully-parametric materials authored in shaders (instant, perfectly tiling, no
18
+ files). The two are complementary.
19
+
20
+ ## Run
21
+
22
+ ```bash
23
+ npx genex texture "<prompt>"
24
+ npx genex texture "lush green grass" --terrain # seamless tiling for ground/terrain
25
+ ```
26
+
27
+ Blocks until ready, then saves:
28
+
29
+ ```
30
+ assets/textures/<slug>/basecolor.<ext>
31
+ ```
32
+
33
+ Committed by `npx genex publish`, so it ships with your game.
34
+
35
+ > **Scope:** v1 generates the **base-color (albedo)** map only. Normal / roughness
36
+ > / AO are a planned follow-up — for now set sensible `roughness`/`metalness`
37
+ > constants on the material.
38
+
39
+ ## Apply it (tiling material)
40
+
41
+ ```ts
42
+ import * as THREE from "three";
43
+
44
+ const map = await new THREE.TextureLoader().loadAsync(
45
+ "./assets/textures/lush-green-grass/basecolor.jpg",
46
+ );
47
+ map.colorSpace = THREE.SRGBColorSpace;
48
+ map.wrapS = map.wrapT = THREE.RepeatWrapping;
49
+ map.repeat.set(8, 8); // tile count — raise for large surfaces
50
+ map.anisotropy = renderer.capabilities.getMaxAnisotropy();
51
+
52
+ const material = new THREE.MeshStandardMaterial({ map, roughness: 0.9, metalness: 0 });
53
+ groundMesh.material = material; // e.g. a PlaneGeometry ground
54
+ ```
55
+
56
+ ### Terrain
57
+
58
+ Generate with `--terrain`, lay a large `PlaneGeometry` (rotated flat), and raise
59
+ `repeat` so the texture tiles densely across it:
60
+
61
+ ```ts
62
+ const ground = new THREE.Mesh(new THREE.PlaneGeometry(200, 200), material);
63
+ ground.rotation.x = -Math.PI / 2;
64
+ map.repeat.set(64, 64);
65
+ scene.add(ground);
66
+ ```
67
+
68
+ ## Publish checklist
69
+
70
+ - Relative path `./assets/textures/...`; `base: "./"` in `vite.config.ts`; commit `assets/`.
71
+
72
+ ## Troubleshooting
73
+
74
+ - **Visible tiling seams** — use `--terrain` (tuned for seamless edges), lower the
75
+ `repeat`, or blend two textures. Perfect seamlessness is a known v1 limitation.
76
+ - **Colors look washed/dark** — ensure `map.colorSpace = THREE.SRGBColorSpace`.
@@ -21,6 +21,24 @@ Start with `$genex-threejs-skill-router` for broad game or graphics requests.
21
21
  It routes the agent to focused skills for cameras, procedural geometry,
22
22
  materials, atmosphere, water, VFX, post-processing, and visual validation.
23
23
 
24
+ ## Generating real assets
25
+
26
+ Beyond procedural code, Genex can generate **real, AI-made assets** from a prompt
27
+ and drop them into `./assets/` (committed when you publish):
28
+
29
+ ```bash
30
+ npx genex model "weathered wooden barrel" # a 3D mesh (GLB)
31
+ npx genex skybox "golden hour over mountains" # a 360° sky + lighting
32
+ npx genex sfx "punchy laser zap" --duration 2 # a sound effect (mp3)
33
+ npx genex texture "mossy cobblestone" --terrain # a tiling surface texture
34
+ ```
35
+
36
+ (Run them inside your project — the `@genex-ai/cli-demo` dev dependency makes
37
+ `npx genex` resolve to the right CLI.)
38
+
39
+ Each has a focused skill with the exact loader code — `$genex-ai-model`,
40
+ `$genex-ai-skybox`, `$genex-ai-sfx`, `$genex-ai-texture`.
41
+
24
42
  Your existing files were left untouched. `genex init` only adds what is missing.
25
43
 
26
44
  ## Re-running setup
@@ -28,7 +46,7 @@ Your existing files were left untouched. `genex init` only adds what is missing.
28
46
  Safe to run any time; it never overwrites your files:
29
47
 
30
48
  ```bash
31
- npx genex init
49
+ npx @genex-ai/cli-demo@latest init
32
50
  ```
33
51
 
34
52
  Use `--force` only if you intentionally want to overwrite installed templates
@@ -36,6 +54,6 @@ with the latest versions.
36
54
 
37
55
  ## Authorization
38
56
 
39
- `genex init` opens the auth site, then writes a `GENEX_TOKEN` into your
40
- project's `.env`. If the browser doesn't open, copy the printed URL into a
41
- browser manually to finish authorizing.
57
+ `genex init` opens the auth site, then saves your token to `~/.genex/env`
58
+ (reused across projects). If the browser doesn't open, copy the printed URL into
59
+ a browser manually to finish authorizing.
@@ -37,6 +37,27 @@ map, execution order, and acceptance gate.
37
37
  | render-target ownership, pass ordering, depth/normal/history signals | `$genex-threejs-image-pipeline` |
38
38
  | fixed-view captures, seed sweeps, browser and GPU evidence | `$genex-threejs-visual-validation` |
39
39
 
40
+ ## Real (AI-generated) assets — `npx genex` commands
41
+
42
+ When the user wants a **specific, recognizable asset** (a named object, a described
43
+ sky, a particular sound or surface) rather than something authored in code, generate
44
+ it with an `npx genex` command (run inside the project, where the `@genex-ai/cli-demo`
45
+ dev dependency makes `genex` resolve to the right CLI). Each drops a real file into
46
+ `./assets/` (committed by `npx genex publish`), and its skill has the exact Three.js
47
+ loader code.
48
+
49
+ | Work needed | Generate with | Skill |
50
+ | --- | --- | --- |
51
+ | a specific prop / item / character / vehicle as a real mesh | `npx genex model "<prompt>"` | `$genex-ai-model` |
52
+ | a described 360° sky / backdrop + image-based lighting | `npx genex skybox "<prompt>"` | `$genex-ai-skybox` |
53
+ | a specific sound effect tied to an event | `npx genex sfx "<prompt>"` | `$genex-ai-sfx` |
54
+ | a photoreal surface/material on a mesh or terrain | `npx genex texture "<prompt>" [--terrain]` | `$genex-ai-texture` |
55
+
56
+ Prefer the **procedural** skills above for abstract/parametric/animated systems
57
+ (geometry, materials, sky, water, VFX) — no files, infinite variation. Prefer the
58
+ **`npx genex` generators** for concrete, describable, photoreal assets. They complement
59
+ each other.
60
+
40
61
  ## Routing rules
41
62
 
42
63
  - Start from the playable game target: player verb, scene scale, camera distance,