@genex-ai/cli-demo 0.2.0 → 0.4.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 +45 -22
- package/dist/index.js +238 -90
- package/package.json +11 -10
- package/templates/skills/genex-ai-model/SKILL.md +6 -6
- package/templates/skills/genex-ai-sfx/SKILL.md +4 -4
- package/templates/skills/genex-ai-skybox/SKILL.md +5 -5
- package/templates/skills/genex-ai-texture/SKILL.md +5 -5
- package/templates/skills/genex-getting-started/SKILL.md +19 -12
- package/templates/skills/genex-threejs-skill-router/SKILL.md +10 -8
package/README.md
CHANGED
|
@@ -5,18 +5,29 @@ This is the `cli` app of the [genex monorepo](../../README.md).
|
|
|
5
5
|
|
|
6
6
|
```bash
|
|
7
7
|
genex init <name> # authorize + create the draft project
|
|
8
|
-
genex
|
|
8
|
+
genex preview # build + push to the hosted draft URL (unlisted)
|
|
9
|
+
genex publish # build + push, then list the game in the gallery
|
|
9
10
|
genex model "<prompt>" # generate a 3D model → assets/models/
|
|
10
11
|
genex skybox "<prompt>" # generate a 360° sky → assets/skybox/
|
|
11
12
|
genex sfx "<prompt>" # generate a sound fx → assets/sfx/
|
|
12
13
|
genex texture "<prompt>" # generate a texture → assets/textures/
|
|
13
14
|
```
|
|
14
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
|
+
|
|
15
22
|
`genex init` does four things:
|
|
16
23
|
|
|
17
|
-
1. **
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
1. **Installs the skills for your coding agents** — copies the bundled templates
|
|
25
|
+
into every agent it detects: Claude Code (`~/.claude`, full set incl.
|
|
26
|
+
agents/ + commands/), Codex (`~/.codex/skills`), and Cursor
|
|
27
|
+
(`~/.cursor/skills`). genex-owned skills (`genex-*`) are **always refreshed**
|
|
28
|
+
to the package version so a stale copy can't linger; your own files are never
|
|
29
|
+
overwritten. Pick agents explicitly with `--agents claude,codex,cursor`, or a
|
|
30
|
+
single custom dir with `--dir`.
|
|
20
31
|
2. **Authorizes you** — opens the Genex auth site (web) in your browser. If the
|
|
21
32
|
browser can't open, it prints the URL to open manually.
|
|
22
33
|
3. **Saves your token** — writes `GENEX_TOKEN` to `~/.genex/env` (per-user;
|
|
@@ -27,11 +38,21 @@ genex texture "<prompt>" # generate a texture → assets/textures/
|
|
|
27
38
|
`sshUrl`, urls) in `./.genex/project.json`. The game shows up in your
|
|
28
39
|
dashboard's **My games** immediately.
|
|
29
40
|
|
|
30
|
-
`genex
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
`genex preview` and `genex publish` share a build-aware deploy core: each runs
|
|
42
|
+
`npm run build` (when the project has a build script), then pushes the built
|
|
43
|
+
`dist/` **as the repo root** over the deploy key — no copying files or swapping
|
|
44
|
+
`index.html` by hand. A `.nojekyll` marker is written so GitHub Pages serves the
|
|
45
|
+
bundle verbatim, and the deploy never stages `genex_key` (the private key can't
|
|
46
|
+
leak). Pass `--no-build` to deploy whatever is already built.
|
|
47
|
+
|
|
48
|
+
- **`genex preview`** pushes the current build to the project's hosted draft URL
|
|
49
|
+
(`https://<user>.github.io/<slug>/`) without listing it publicly — re-run it
|
|
50
|
+
after each change to update the shareable draft.
|
|
51
|
+
- **`genex publish`** does the same deploy, then flips the project to
|
|
52
|
+
**published** so it appears in the public gallery. The deploy and the gallery
|
|
53
|
+
flag are independent; `--no-push` flips the flag only.
|
|
54
|
+
|
|
55
|
+
Defaults: API `https://demo-api.glotech.world`, auth site
|
|
35
56
|
`https://demo-web.glotech.world` — override with `--api-url` / `--auth-url` (or
|
|
36
57
|
`GENEX_API_URL` / `GENEX_AUTH_URL`) for local dev.
|
|
37
58
|
|
|
@@ -77,11 +98,12 @@ pnpm --filter @genex-ai/cli-demo start init
|
|
|
77
98
|
Once published, end users run it with `npx`:
|
|
78
99
|
|
|
79
100
|
```bash
|
|
80
|
-
npx @genex-ai/cli-demo init
|
|
101
|
+
npx @genex-ai/cli-demo@latest init
|
|
81
102
|
```
|
|
82
103
|
|
|
83
|
-
>
|
|
84
|
-
>
|
|
104
|
+
> The published package ships a bundled `dist/` (built with tsup) and runs on
|
|
105
|
+
> **Node ≥ 20**. Running from *source* in the monorepo needs **Node ≥ 24** (the
|
|
106
|
+
> source is executed as TypeScript via Node's native type stripping).
|
|
85
107
|
|
|
86
108
|
## Usage
|
|
87
109
|
|
|
@@ -89,9 +111,10 @@ npx @genex-ai/cli-demo init
|
|
|
89
111
|
genex init [options]
|
|
90
112
|
|
|
91
113
|
Options
|
|
92
|
-
--
|
|
93
|
-
--
|
|
94
|
-
--
|
|
114
|
+
--agents <list> Agents to install for: claude,codex,cursor (default: auto-detect)
|
|
115
|
+
--dir <path> Single destination workspace (overrides --agents)
|
|
116
|
+
--env <path> Token env file (default: ~/.genex/env)
|
|
117
|
+
--auth-url <url> Override the auth site (default: https://demo-web.glotech.world)
|
|
95
118
|
--no-auth Only scaffold templates; skip authorization
|
|
96
119
|
--force Overwrite existing files (default: never overwrite)
|
|
97
120
|
--timeout <seconds> How long to wait for the auth redirect (default: 300)
|
|
@@ -122,8 +145,8 @@ genex init --auth-url http://localhost:3000
|
|
|
122
145
|
|
|
123
146
|
The auth site URL is a single configurable value:
|
|
124
147
|
|
|
125
|
-
- **Default:** `https://
|
|
126
|
-
[`src/config.ts`](src/config.ts)).
|
|
148
|
+
- **Default:** `https://demo-web.glotech.world` (the `DEFAULT_AUTH_URL` constant
|
|
149
|
+
in [`src/config.ts`](src/config.ts)).
|
|
127
150
|
- **Override at runtime:** set `GENEX_AUTH_URL`, or pass `--auth-url`.
|
|
128
151
|
|
|
129
152
|
```bash
|
|
@@ -168,8 +191,8 @@ The CLI uses a loopback-redirect flow (the same pattern as `gh auth login` and
|
|
|
168
191
|
> the exact `state` it received. A missing or mismatched `state` is rejected
|
|
169
192
|
> (CSRF protection).
|
|
170
193
|
|
|
171
|
-
3. The CLI verifies `state`, writes `GENEX_TOKEN=<TOKEN>` to
|
|
172
|
-
success page in the browser.
|
|
194
|
+
3. The CLI verifies `state`, writes `GENEX_TOKEN=<TOKEN>` to `~/.genex/env`, and
|
|
195
|
+
shows a success page in the browser.
|
|
173
196
|
|
|
174
197
|
If the browser can't be opened, the URL is printed and — when run in an
|
|
175
198
|
interactive terminal — a pasted token is also accepted.
|
|
@@ -236,9 +259,9 @@ templates/ copied into ~/.claude by `genex init`
|
|
|
236
259
|
|
|
237
260
|
## Publishing
|
|
238
261
|
|
|
239
|
-
The package ships
|
|
240
|
-
Node ≥
|
|
241
|
-
and `LICENSE` (see the `files` field in `package.json`).
|
|
262
|
+
The package ships a bundled `dist/` (built with tsup via `prepack`); consumers
|
|
263
|
+
run it on Node ≥ 20. The published tarball includes `dist/`, `templates/`,
|
|
264
|
+
`README.md`, and `LICENSE` (see the `files` field in `package.json`).
|
|
242
265
|
|
|
243
266
|
```bash
|
|
244
267
|
pnpm --filter @genex-ai/cli-demo publish --access public
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
|
|
|
9
9
|
import path6 from "path";
|
|
10
10
|
|
|
11
11
|
// src/config.ts
|
|
12
|
+
import fs from "fs";
|
|
12
13
|
import os from "os";
|
|
13
14
|
import path from "path";
|
|
14
15
|
import { fileURLToPath } from "url";
|
|
@@ -42,45 +43,77 @@ function getTemplatesDir() {
|
|
|
42
43
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
43
44
|
return path.resolve(here, "..", "templates");
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
var KNOWN_AGENTS = {
|
|
47
|
+
claude: { label: "Claude Code", dirName: ".claude", full: true },
|
|
48
|
+
codex: { label: "Codex", dirName: ".codex", full: false },
|
|
49
|
+
cursor: { label: "Cursor", dirName: ".cursor", full: false }
|
|
50
|
+
};
|
|
51
|
+
var KNOWN_AGENT_IDS = Object.keys(KNOWN_AGENTS);
|
|
52
|
+
function resolveAgentTargets(opts = {}) {
|
|
53
|
+
if (opts.dir) {
|
|
54
|
+
return [{ id: "custom", label: "workspace", baseDir: path.resolve(opts.dir), full: true }];
|
|
55
|
+
}
|
|
56
|
+
const home = os.homedir();
|
|
57
|
+
let ids;
|
|
58
|
+
if (opts.agents && opts.agents.length > 0) {
|
|
59
|
+
ids = opts.agents.filter((id) => KNOWN_AGENTS[id]);
|
|
60
|
+
} else {
|
|
61
|
+
ids = KNOWN_AGENT_IDS.filter((id) => isDir(path.join(home, KNOWN_AGENTS[id].dirName)));
|
|
62
|
+
if (ids.length === 0) ids = ["claude"];
|
|
63
|
+
}
|
|
64
|
+
return ids.map((id) => {
|
|
65
|
+
const def = KNOWN_AGENTS[id];
|
|
66
|
+
return { id, label: def.label, baseDir: path.join(home, def.dirName), full: def.full };
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function isDir(p) {
|
|
70
|
+
try {
|
|
71
|
+
return fs.statSync(p).isDirectory();
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
48
75
|
}
|
|
49
76
|
|
|
50
77
|
// src/lib/copy-templates.ts
|
|
51
|
-
import
|
|
78
|
+
import fs2 from "fs/promises";
|
|
52
79
|
import path2 from "path";
|
|
80
|
+
function isGenexManaged(rel) {
|
|
81
|
+
return rel.split(path2.sep).some((seg) => seg.startsWith("genex"));
|
|
82
|
+
}
|
|
53
83
|
async function copyTemplates(srcDir, destDir, opts = {}) {
|
|
54
|
-
const result = { copied: [], skipped: [] };
|
|
84
|
+
const result = { copied: [], updated: [], skipped: [] };
|
|
55
85
|
await walk(srcDir, srcDir, destDir, opts, result);
|
|
56
86
|
return result;
|
|
57
87
|
}
|
|
58
88
|
async function walk(rootSrc, src, dest, opts, result) {
|
|
59
|
-
const entries = await
|
|
89
|
+
const entries = await fs2.readdir(src, { withFileTypes: true });
|
|
60
90
|
for (const entry of entries) {
|
|
61
91
|
const srcPath = path2.join(src, entry.name);
|
|
62
92
|
const destPath = path2.join(dest, entry.name);
|
|
63
93
|
const rel = path2.relative(rootSrc, srcPath);
|
|
64
94
|
if (entry.isDirectory()) {
|
|
65
|
-
await
|
|
95
|
+
await fs2.mkdir(destPath, { recursive: true });
|
|
66
96
|
await walk(rootSrc, srcPath, destPath, opts, result);
|
|
67
97
|
continue;
|
|
68
98
|
}
|
|
69
99
|
if (!entry.isFile()) {
|
|
70
100
|
continue;
|
|
71
101
|
}
|
|
72
|
-
|
|
102
|
+
const present = await exists(destPath);
|
|
103
|
+
const mayOverwrite = opts.force || isGenexManaged(rel);
|
|
104
|
+
if (present && !mayOverwrite) {
|
|
73
105
|
result.skipped.push(rel);
|
|
74
106
|
continue;
|
|
75
107
|
}
|
|
76
|
-
await
|
|
77
|
-
await
|
|
108
|
+
await fs2.mkdir(path2.dirname(destPath), { recursive: true });
|
|
109
|
+
await fs2.copyFile(srcPath, destPath);
|
|
78
110
|
result.copied.push(rel);
|
|
111
|
+
if (present) result.updated.push(rel);
|
|
79
112
|
}
|
|
80
113
|
}
|
|
81
114
|
async function exists(p) {
|
|
82
115
|
try {
|
|
83
|
-
await
|
|
116
|
+
await fs2.access(p);
|
|
84
117
|
return true;
|
|
85
118
|
} catch {
|
|
86
119
|
return false;
|
|
@@ -396,7 +429,7 @@ async function createDraftProject(opts) {
|
|
|
396
429
|
return null;
|
|
397
430
|
}
|
|
398
431
|
log.success(`Created project ${c.cyan(project.slug)}.`);
|
|
399
|
-
if (project.playUrl) log.dim(` play (after publish): ${project.playUrl}`);
|
|
432
|
+
if (project.playUrl) log.dim(` play (after preview/publish): ${project.playUrl}`);
|
|
400
433
|
log.dim(` dashboard: ${dashboardUrl}/dashboard`);
|
|
401
434
|
return {
|
|
402
435
|
id: project.id,
|
|
@@ -416,7 +449,7 @@ function randomSuffix() {
|
|
|
416
449
|
}
|
|
417
450
|
|
|
418
451
|
// src/lib/ssh.ts
|
|
419
|
-
import
|
|
452
|
+
import fs3 from "fs/promises";
|
|
420
453
|
import path3 from "path";
|
|
421
454
|
import { spawn as spawn2 } from "child_process";
|
|
422
455
|
var KEY_NAME = "genex_key";
|
|
@@ -424,7 +457,7 @@ async function generateSshKeypair(dir, log) {
|
|
|
424
457
|
const keyPath = path3.join(dir, KEY_NAME);
|
|
425
458
|
const pubPath = `${keyPath}.pub`;
|
|
426
459
|
try {
|
|
427
|
-
const existing = (await
|
|
460
|
+
const existing = (await fs3.readFile(pubPath, "utf8")).trim();
|
|
428
461
|
if (existing) {
|
|
429
462
|
log.dim(`Reusing existing deploy key (${KEY_NAME}).`);
|
|
430
463
|
return { publicKey: existing };
|
|
@@ -435,12 +468,12 @@ async function generateSshKeypair(dir, log) {
|
|
|
435
468
|
const ok = await runSshKeygen(keyPath, log);
|
|
436
469
|
if (!ok) return null;
|
|
437
470
|
try {
|
|
438
|
-
const pub = (await
|
|
471
|
+
const pub = (await fs3.readFile(pubPath, "utf8")).trim();
|
|
439
472
|
if (!pub) {
|
|
440
473
|
log.warn("ssh-keygen produced no public key.");
|
|
441
474
|
return null;
|
|
442
475
|
}
|
|
443
|
-
await
|
|
476
|
+
await fs3.chmod(keyPath, 384).catch(() => {
|
|
444
477
|
});
|
|
445
478
|
return { publicKey: pub };
|
|
446
479
|
} catch (err) {
|
|
@@ -473,7 +506,7 @@ async function writeGitignore(dir, log) {
|
|
|
473
506
|
const file = path3.join(dir, ".gitignore");
|
|
474
507
|
let content = "";
|
|
475
508
|
try {
|
|
476
|
-
content = await
|
|
509
|
+
content = await fs3.readFile(file, "utf8");
|
|
477
510
|
} catch {
|
|
478
511
|
}
|
|
479
512
|
const present = new Set(content.split("\n").map((l) => l.trim()));
|
|
@@ -483,23 +516,23 @@ async function writeGitignore(dir, log) {
|
|
|
483
516
|
if (next.length > 0 && !next.endsWith("\n")) next += "\n";
|
|
484
517
|
if (!content.trim()) next += "# genex (deploy key + local metadata \u2014 never publish)\n";
|
|
485
518
|
next += toAdd.join("\n") + "\n";
|
|
486
|
-
await
|
|
519
|
+
await fs3.writeFile(file, next);
|
|
487
520
|
log.dim(`Updated .gitignore (${toAdd.join(", ")}).`);
|
|
488
521
|
}
|
|
489
522
|
|
|
490
523
|
// src/lib/store.ts
|
|
491
|
-
import
|
|
524
|
+
import fs5 from "fs/promises";
|
|
492
525
|
import path5 from "path";
|
|
493
526
|
|
|
494
527
|
// src/lib/env.ts
|
|
495
|
-
import
|
|
528
|
+
import fs4 from "fs/promises";
|
|
496
529
|
import path4 from "path";
|
|
497
530
|
import { spawn as spawn3 } from "child_process";
|
|
498
531
|
async function writeEnvVar(envPath, key, value) {
|
|
499
532
|
let content = "";
|
|
500
533
|
let existed = false;
|
|
501
534
|
try {
|
|
502
|
-
content = await
|
|
535
|
+
content = await fs4.readFile(envPath, "utf8");
|
|
503
536
|
existed = true;
|
|
504
537
|
} catch {
|
|
505
538
|
}
|
|
@@ -519,14 +552,14 @@ async function writeEnvVar(envPath, key, value) {
|
|
|
519
552
|
next = prefix + assignment + "\n";
|
|
520
553
|
mode = existed ? "appended" : "created";
|
|
521
554
|
}
|
|
522
|
-
await
|
|
523
|
-
await
|
|
555
|
+
await fs4.mkdir(path4.dirname(envPath), { recursive: true });
|
|
556
|
+
await fs4.writeFile(envPath, next, { mode: 384 });
|
|
524
557
|
await restrictFilePermissions(envPath);
|
|
525
558
|
return { mode, path: envPath };
|
|
526
559
|
}
|
|
527
560
|
async function restrictFilePermissions(filePath) {
|
|
528
561
|
if (process.platform !== "win32") {
|
|
529
|
-
await
|
|
562
|
+
await fs4.chmod(filePath, 384).catch(() => {
|
|
530
563
|
});
|
|
531
564
|
return;
|
|
532
565
|
}
|
|
@@ -575,7 +608,7 @@ async function readUserToken(envPath) {
|
|
|
575
608
|
async function readTokenFromFile(file) {
|
|
576
609
|
let content;
|
|
577
610
|
try {
|
|
578
|
-
content = await
|
|
611
|
+
content = await fs5.readFile(file, "utf8");
|
|
579
612
|
} catch {
|
|
580
613
|
return null;
|
|
581
614
|
}
|
|
@@ -591,7 +624,7 @@ function stripQuotes(v) {
|
|
|
591
624
|
}
|
|
592
625
|
async function readProject(cwd = process.cwd()) {
|
|
593
626
|
try {
|
|
594
|
-
const raw = await
|
|
627
|
+
const raw = await fs5.readFile(getProjectMetadataPath(cwd), "utf8");
|
|
595
628
|
return JSON.parse(raw);
|
|
596
629
|
} catch {
|
|
597
630
|
return null;
|
|
@@ -599,9 +632,9 @@ async function readProject(cwd = process.cwd()) {
|
|
|
599
632
|
}
|
|
600
633
|
async function writeProject(meta, cwd = process.cwd()) {
|
|
601
634
|
const file = getProjectMetadataPath(cwd);
|
|
602
|
-
await
|
|
603
|
-
await
|
|
604
|
-
await
|
|
635
|
+
await fs5.mkdir(path5.dirname(file), { recursive: true });
|
|
636
|
+
await fs5.writeFile(file, JSON.stringify(meta, null, 2) + "\n", { mode: 384 });
|
|
637
|
+
await fs5.chmod(file, 384).catch(() => {
|
|
605
638
|
});
|
|
606
639
|
return { path: file };
|
|
607
640
|
}
|
|
@@ -631,24 +664,24 @@ async function runInit(opts) {
|
|
|
631
664
|
log.plain(c.bold("genex init"));
|
|
632
665
|
log.plain("");
|
|
633
666
|
const templatesDir = getTemplatesDir();
|
|
634
|
-
const
|
|
635
|
-
log.step(
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
}
|
|
649
|
-
if (copied.length === 0 && skipped.length === 0) {
|
|
650
|
-
log.info("No template files to install.");
|
|
667
|
+
const targets = resolveAgentTargets({ dir: opts.dir, agents: opts.agents });
|
|
668
|
+
log.step(
|
|
669
|
+
`Installing Genex skills for: ${targets.map((t) => c.cyan(t.label)).join(", ")}`
|
|
670
|
+
);
|
|
671
|
+
let totalNew = 0;
|
|
672
|
+
let totalUpdated = 0;
|
|
673
|
+
for (const t of targets) {
|
|
674
|
+
const src = t.full ? templatesDir : path6.join(templatesDir, "skills");
|
|
675
|
+
const dest = t.full ? t.baseDir : path6.join(t.baseDir, "skills");
|
|
676
|
+
const { copied, updated } = await copyTemplates(src, dest, { force: opts.force });
|
|
677
|
+
const added = copied.length - updated.length;
|
|
678
|
+
totalNew += added;
|
|
679
|
+
totalUpdated += updated.length;
|
|
680
|
+
log.dim(` ${t.label}: ${added} added, ${updated.length} refreshed \u2192 ${dest}`);
|
|
651
681
|
}
|
|
682
|
+
log.success(
|
|
683
|
+
`Skills ready (${totalNew} added, ${totalUpdated} refreshed across ${targets.length} agent${targets.length === 1 ? "" : "s"}).`
|
|
684
|
+
);
|
|
652
685
|
log.plain("");
|
|
653
686
|
if (opts.noAuth) {
|
|
654
687
|
log.info("Skipping authorization (--no-auth).");
|
|
@@ -701,9 +734,10 @@ async function runInit(opts) {
|
|
|
701
734
|
log.success("All set. \u{1F680}");
|
|
702
735
|
}
|
|
703
736
|
|
|
704
|
-
// src/
|
|
737
|
+
// src/lib/deploy.ts
|
|
705
738
|
import { spawn as spawn4 } from "child_process";
|
|
706
|
-
import
|
|
739
|
+
import fs6 from "fs/promises";
|
|
740
|
+
import os2 from "os";
|
|
707
741
|
import path7 from "path";
|
|
708
742
|
function run(cmd, args, env) {
|
|
709
743
|
return new Promise((resolve) => {
|
|
@@ -722,6 +756,113 @@ function run(cmd, args, env) {
|
|
|
722
756
|
child.on("close", (code2) => resolve({ code: code2 ?? -1, out, err }));
|
|
723
757
|
});
|
|
724
758
|
}
|
|
759
|
+
async function deployGame(sshUrl, opts, log) {
|
|
760
|
+
const cwd = process.cwd();
|
|
761
|
+
if (!opts.noBuild && await hasBuildScript(cwd)) {
|
|
762
|
+
log.step("Building the production bundle\u2026");
|
|
763
|
+
const built = await run("npm", ["run", "build"]);
|
|
764
|
+
if (built.code !== 0) {
|
|
765
|
+
log.error("Build failed \u2014 your game was NOT deployed.");
|
|
766
|
+
const tail = (built.err || built.out).trim().split("\n").slice(-12).join("\n");
|
|
767
|
+
if (tail) log.dim(tail);
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
log.success("Built.");
|
|
771
|
+
}
|
|
772
|
+
const distDir = path7.join(cwd, "dist");
|
|
773
|
+
const siteDir = await isDir2(distDir) ? distDir : cwd;
|
|
774
|
+
if (siteDir === cwd) {
|
|
775
|
+
await writeGitignore(cwd, log);
|
|
776
|
+
}
|
|
777
|
+
const rel = path7.relative(cwd, siteDir) || ".";
|
|
778
|
+
try {
|
|
779
|
+
await fs6.access(path7.join(siteDir, "index.html"));
|
|
780
|
+
} catch {
|
|
781
|
+
log.warn(`No index.html in ${rel} \u2014 GitHub Pages needs one to serve the game.`);
|
|
782
|
+
}
|
|
783
|
+
await warnIfAbsolutePaths(siteDir, log);
|
|
784
|
+
await fs6.writeFile(path7.join(siteDir, ".nojekyll"), "");
|
|
785
|
+
const keyPath = path7.resolve(cwd, KEY_NAME);
|
|
786
|
+
if (!await fileExists(keyPath)) {
|
|
787
|
+
log.warn(`No deploy key (${KEY_NAME}) here \u2014 run \`genex init\` in this folder first.`);
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
790
|
+
const gitDir = await fs6.mkdtemp(path7.join(os2.tmpdir(), "genex-deploy-"));
|
|
791
|
+
const gitEnv = { GIT_DIR: gitDir, GIT_WORK_TREE: siteDir };
|
|
792
|
+
try {
|
|
793
|
+
if ((await run("git", ["init", "-q"], gitEnv)).code !== 0) {
|
|
794
|
+
log.warn("git init failed \u2014 the game was not pushed.");
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
await run("git", ["add", "-A"], gitEnv);
|
|
798
|
+
const tracked = await run("git", ["ls-files"], gitEnv);
|
|
799
|
+
if (/(^|\/)genex_key(\.pub)?$/m.test(tracked.out)) {
|
|
800
|
+
log.error(`Refusing to deploy: ${KEY_NAME} is staged. Add it to .gitignore and retry.`);
|
|
801
|
+
return false;
|
|
802
|
+
}
|
|
803
|
+
const commit = await run(
|
|
804
|
+
"git",
|
|
805
|
+
["-c", "user.email=agent@genex.local", "-c", "user.name=genex", "commit", "-q", "-m", "build"],
|
|
806
|
+
gitEnv
|
|
807
|
+
);
|
|
808
|
+
if (commit.code !== 0 && !/nothing to commit/i.test(commit.out + commit.err)) {
|
|
809
|
+
log.warn("Nothing to commit \u2014 the build produced no files.");
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
log.step("Pushing your game over SSH\u2026");
|
|
813
|
+
const push = await run("git", ["push", "-q", sshUrl, "+HEAD:main"], {
|
|
814
|
+
...gitEnv,
|
|
815
|
+
GIT_SSH_COMMAND: `ssh -i ${keyPath} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new`
|
|
816
|
+
});
|
|
817
|
+
if (push.code === 0) {
|
|
818
|
+
log.success("Pushed.");
|
|
819
|
+
return true;
|
|
820
|
+
}
|
|
821
|
+
log.warn("git push failed \u2014 your live game was NOT updated.");
|
|
822
|
+
const tail = push.err.trim().split("\n").slice(-2).join(" ");
|
|
823
|
+
if (tail) log.dim(` ${tail}`);
|
|
824
|
+
return false;
|
|
825
|
+
} finally {
|
|
826
|
+
await fs6.rm(gitDir, { recursive: true, force: true }).catch(() => {
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
async function hasBuildScript(cwd) {
|
|
831
|
+
try {
|
|
832
|
+
const pkg = JSON.parse(await fs6.readFile(path7.join(cwd, "package.json"), "utf8"));
|
|
833
|
+
return Boolean(pkg.scripts?.build);
|
|
834
|
+
} catch {
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
async function isDir2(p) {
|
|
839
|
+
try {
|
|
840
|
+
return (await fs6.stat(p)).isDirectory();
|
|
841
|
+
} catch {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
async function fileExists(p) {
|
|
846
|
+
try {
|
|
847
|
+
await fs6.access(p);
|
|
848
|
+
return true;
|
|
849
|
+
} catch {
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
async function warnIfAbsolutePaths(siteDir, log) {
|
|
854
|
+
let html;
|
|
855
|
+
try {
|
|
856
|
+
html = await fs6.readFile(path7.join(siteDir, "index.html"), "utf8");
|
|
857
|
+
} catch {
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
if (/(?:src|href)="\/(?!\/)/.test(html)) {
|
|
861
|
+
log.warn("Built index.html uses absolute paths \u2014 set `base: './'` in vite.config so assets load under the Pages subpath.");
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// src/commands/publish.ts
|
|
725
866
|
async function runPublish(opts) {
|
|
726
867
|
const log = createLogger({ quiet: opts.quiet });
|
|
727
868
|
log.plain(c.bold("genex publish"));
|
|
@@ -741,11 +882,11 @@ async function runPublish(opts) {
|
|
|
741
882
|
const apiUrl = getApiUrl(opts.apiUrl ?? meta.apiUrl);
|
|
742
883
|
let pushed = null;
|
|
743
884
|
if (opts.noPush) {
|
|
744
|
-
log.info("Skipping git push (--no-push).");
|
|
885
|
+
log.info("Skipping build + git push (--no-push).");
|
|
745
886
|
} else {
|
|
746
|
-
pushed = await
|
|
887
|
+
pushed = await deployGame(meta.sshUrl, { noBuild: opts.noBuild }, log);
|
|
747
888
|
}
|
|
748
|
-
log.step("
|
|
889
|
+
log.step("Listing it in the gallery\u2026");
|
|
749
890
|
let res;
|
|
750
891
|
try {
|
|
751
892
|
const body = {};
|
|
@@ -771,7 +912,7 @@ async function runPublish(opts) {
|
|
|
771
912
|
log.warn(
|
|
772
913
|
"Listed in the gallery, but the game wasn't pushed \u2014 the play URL won't work until the push succeeds."
|
|
773
914
|
);
|
|
774
|
-
log.dim(" Fix the
|
|
915
|
+
log.dim(" Fix the error above and re-run `genex publish`.");
|
|
775
916
|
} else {
|
|
776
917
|
log.success("Published. \u{1F389}");
|
|
777
918
|
}
|
|
@@ -780,44 +921,37 @@ async function runPublish(opts) {
|
|
|
780
921
|
if (pushed === true) log.dim(" (GitHub Pages rebuilds ~30\u201390s after a push.)");
|
|
781
922
|
}
|
|
782
923
|
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
const
|
|
790
|
-
if (!
|
|
791
|
-
log.
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
return false;
|
|
795
|
-
}
|
|
924
|
+
|
|
925
|
+
// src/commands/preview.ts
|
|
926
|
+
async function runPreview(opts) {
|
|
927
|
+
const log = createLogger({ quiet: opts.quiet });
|
|
928
|
+
log.plain(c.bold("genex preview"));
|
|
929
|
+
log.plain("");
|
|
930
|
+
const meta = await readProject();
|
|
931
|
+
if (!meta) {
|
|
932
|
+
log.error("No genex project here. Run `genex init` in this directory first.");
|
|
933
|
+
process.exitCode = 1;
|
|
934
|
+
return;
|
|
796
935
|
}
|
|
797
|
-
await
|
|
798
|
-
|
|
799
|
-
if (
|
|
800
|
-
log.
|
|
936
|
+
const pushed = await deployGame(meta.sshUrl, { noBuild: opts.noBuild }, log);
|
|
937
|
+
log.plain("");
|
|
938
|
+
if (!pushed) {
|
|
939
|
+
log.warn("Preview not updated \u2014 see the error above.");
|
|
940
|
+
process.exitCode = 1;
|
|
941
|
+
return;
|
|
801
942
|
}
|
|
802
|
-
log.
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
if (push.code === 0) {
|
|
807
|
-
log.success("Pushed to main.");
|
|
808
|
-
return true;
|
|
943
|
+
log.success("Preview deployed. \u{1F310}");
|
|
944
|
+
if (meta.playUrl) {
|
|
945
|
+
log.dim(` ${meta.playUrl}`);
|
|
946
|
+
log.dim(" (GitHub Pages rebuilds ~30\u201390s after a push; it stays an unlisted draft until you publish.)");
|
|
809
947
|
}
|
|
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;
|
|
814
948
|
}
|
|
815
949
|
|
|
816
950
|
// src/commands/generate.ts
|
|
817
951
|
import path9 from "path";
|
|
818
952
|
|
|
819
953
|
// src/lib/assets.ts
|
|
820
|
-
import
|
|
954
|
+
import fs7 from "fs/promises";
|
|
821
955
|
import path8 from "path";
|
|
822
956
|
function slugify(input) {
|
|
823
957
|
const s = input.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50).replace(/-+$/g, "");
|
|
@@ -827,8 +961,8 @@ async function downloadToFile(url, dest, headers) {
|
|
|
827
961
|
const res = await fetch(url, { headers });
|
|
828
962
|
if (!res.ok) throw new Error(`download failed (HTTP ${res.status}) for ${url}`);
|
|
829
963
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
830
|
-
await
|
|
831
|
-
await
|
|
964
|
+
await fs7.mkdir(path8.dirname(dest), { recursive: true });
|
|
965
|
+
await fs7.writeFile(dest, buf);
|
|
832
966
|
return buf.byteLength;
|
|
833
967
|
}
|
|
834
968
|
|
|
@@ -981,7 +1115,8 @@ var HELP = `${c.bold("genex")} \u2014 set up your ~/.claude workspace, authorize
|
|
|
981
1115
|
|
|
982
1116
|
${c.bold("Usage")}
|
|
983
1117
|
genex init [<name>] [options] Scaffold + authorize + create the draft project.
|
|
984
|
-
genex
|
|
1118
|
+
genex preview [options] Build + push to the hosted draft URL (unlisted).
|
|
1119
|
+
genex publish [options] Build + push, then list the game in the gallery.
|
|
985
1120
|
genex model "<prompt>" [options] Generate a 3D model (GLB) into ./assets/models.
|
|
986
1121
|
genex skybox "<prompt>" [options] Generate a skybox (equirect) into ./assets/skybox.
|
|
987
1122
|
genex sfx "<prompt>" [options] Generate a sound effect (mp3) into ./assets/sfx.
|
|
@@ -997,7 +1132,8 @@ ${c.bold("Options for the generators (`model` `skybox` `sfx` `texture`)")}
|
|
|
997
1132
|
${c.bold("Options for `init`")}
|
|
998
1133
|
<name> Project name (positional; default: current directory name).
|
|
999
1134
|
--name <name> Same as the positional name.
|
|
1000
|
-
--
|
|
1135
|
+
--agents <list> Agents to install skills for (claude,codex,cursor; default: auto-detect).
|
|
1136
|
+
--dir <path> Single destination workspace (overrides --agents).
|
|
1001
1137
|
--env <path> Token env file (default: ~/.genex/env).
|
|
1002
1138
|
--auth-url <url> Override the auth site (default: ${DEFAULT_AUTH_URL}).
|
|
1003
1139
|
--api-url <url> Override the API base URL (default: ${DEFAULT_API_URL}).
|
|
@@ -1006,11 +1142,12 @@ ${c.bold("Options for `init`")}
|
|
|
1006
1142
|
--force Overwrite existing files (default: never overwrite).
|
|
1007
1143
|
--timeout <seconds> How long to wait for the auth redirect (default: 300).
|
|
1008
1144
|
|
|
1009
|
-
${c.bold("Options for `publish`")}
|
|
1010
|
-
--no-
|
|
1011
|
-
--
|
|
1012
|
-
--
|
|
1013
|
-
--
|
|
1145
|
+
${c.bold("Options for `preview` / `publish`")}
|
|
1146
|
+
--no-build Skip the build step; deploy whatever is already built.
|
|
1147
|
+
--no-push (publish) Skip build + push; only flip the gallery flag.
|
|
1148
|
+
--title <title> (publish) Gallery title.
|
|
1149
|
+
--description <text> (publish) Gallery description.
|
|
1150
|
+
--api-url <url> (publish) Override the API base URL.
|
|
1014
1151
|
--env <path> Token env file (default: ~/.genex/env).
|
|
1015
1152
|
|
|
1016
1153
|
${c.bold("Global")}
|
|
@@ -1027,6 +1164,7 @@ ${c.bold("Environment")}
|
|
|
1027
1164
|
${c.bold("Examples")}
|
|
1028
1165
|
genex init my-game
|
|
1029
1166
|
genex init my-game --api-url http://localhost:3000 --auth-url http://localhost:5173
|
|
1167
|
+
genex preview
|
|
1030
1168
|
genex publish
|
|
1031
1169
|
genex publish --no-push --title "My Game"
|
|
1032
1170
|
genex model "weathered wooden barrel with iron bands"
|
|
@@ -1047,6 +1185,7 @@ function parseArgs(argv) {
|
|
|
1047
1185
|
"--auth-url",
|
|
1048
1186
|
"--api-url",
|
|
1049
1187
|
"--colyseus-url",
|
|
1188
|
+
"--agents",
|
|
1050
1189
|
"--name",
|
|
1051
1190
|
"--title",
|
|
1052
1191
|
"--description",
|
|
@@ -1072,6 +1211,9 @@ function parseArgs(argv) {
|
|
|
1072
1211
|
case "--no-push":
|
|
1073
1212
|
parsed.options.noPush = true;
|
|
1074
1213
|
break;
|
|
1214
|
+
case "--no-build":
|
|
1215
|
+
parsed.options.noBuild = true;
|
|
1216
|
+
break;
|
|
1075
1217
|
case "--no-wait":
|
|
1076
1218
|
parsed.options.noWait = true;
|
|
1077
1219
|
break;
|
|
@@ -1126,6 +1268,9 @@ function applyValueFlag(options, flag, value) {
|
|
|
1126
1268
|
case "--colyseus-url":
|
|
1127
1269
|
options.colyseusUrl = value;
|
|
1128
1270
|
break;
|
|
1271
|
+
case "--agents":
|
|
1272
|
+
options.agents = value.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
1273
|
+
break;
|
|
1129
1274
|
case "--name":
|
|
1130
1275
|
options.name = value;
|
|
1131
1276
|
break;
|
|
@@ -1188,6 +1333,9 @@ async function main() {
|
|
|
1188
1333
|
case "init":
|
|
1189
1334
|
await runInit(parsed.options);
|
|
1190
1335
|
break;
|
|
1336
|
+
case "preview":
|
|
1337
|
+
await runPreview(parsed.options);
|
|
1338
|
+
break;
|
|
1191
1339
|
case "publish":
|
|
1192
1340
|
await runPublish(parsed.options);
|
|
1193
1341
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genex-ai/cli-demo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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": {
|
|
@@ -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
|
-
|
|
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
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: genex-ai-model
|
|
3
|
-
description: Generate a real 3D model (GLB mesh) from a text prompt with `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.
|
|
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
4
|
---
|
|
5
5
|
|
|
6
6
|
# Genex AI · Model
|
|
@@ -9,7 +9,7 @@ Turn a text prompt into a real, game-ready **GLB** and drop it into the project.
|
|
|
9
9
|
|
|
10
10
|
## When to use this vs. procedural geometry
|
|
11
11
|
|
|
12
|
-
- **Use `genex model`** for a specific, recognizable object — a barrel, a chair, a
|
|
12
|
+
- **Use `npx genex model`** for a specific, recognizable object — a barrel, a chair, a
|
|
13
13
|
sword, a spaceship, an animal. You get a real textured mesh.
|
|
14
14
|
- **Use `$genex-threejs-procedural-geometry`** for parametric/abstract shapes,
|
|
15
15
|
terrain, or anything you want to generate in code (infinite variations, no files).
|
|
@@ -17,7 +17,7 @@ Turn a text prompt into a real, game-ready **GLB** and drop it into the project.
|
|
|
17
17
|
## Run
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
genex model "<prompt>"
|
|
20
|
+
npx genex model "<prompt>"
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
Write a specific prompt — "weathered wooden barrel with rusted iron bands" beats
|
|
@@ -28,7 +28,7 @@ assets/models/<slug>.glb
|
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
`<slug>` is derived from the prompt (e.g. `weathered-wooden-barrel.glb`). The file
|
|
31
|
-
is **committed by `genex publish`**, so it ships inside your published game.
|
|
31
|
+
is **committed by `npx genex publish`**, so it ships inside your published game.
|
|
32
32
|
|
|
33
33
|
## Load it into the scene
|
|
34
34
|
|
|
@@ -55,7 +55,7 @@ To place many copies, `model.clone()` per instance. For animated GLBs, drive
|
|
|
55
55
|
(`/assets/...`) — GitHub Pages serves the game at a subpath.
|
|
56
56
|
- Set `base: "./"` in `vite.config.ts` before `npm run build` (see the scaffold
|
|
57
57
|
prompt's publish step).
|
|
58
|
-
- Commit the `assets/` folder — `genex publish` pushes it with the game.
|
|
58
|
+
- Commit the `assets/` folder — `npx genex publish` pushes it with the game.
|
|
59
59
|
|
|
60
60
|
## Options
|
|
61
61
|
|
|
@@ -65,6 +65,6 @@ To place many copies, `model.clone()` per instance. For animated GLBs, drive
|
|
|
65
65
|
|
|
66
66
|
## Troubleshooting
|
|
67
67
|
|
|
68
|
-
- **"Not authorized"** — run `genex init` first (it writes your `GENEX_TOKEN`).
|
|
68
|
+
- **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
|
|
69
69
|
- **The mesh looks low-detail / wrong** — make the prompt more specific
|
|
70
70
|
(materials, style, parts) and regenerate; each run is a fresh asset.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: genex-ai-sfx
|
|
3
|
-
description: Generate a real sound effect (mp3) from a text prompt with `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.
|
|
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
4
|
---
|
|
5
5
|
|
|
6
6
|
# Genex AI · SFX
|
|
@@ -10,8 +10,8 @@ Turn a prompt into a real **mp3** sound effect and wire it to a game event.
|
|
|
10
10
|
## Run
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
genex sfx "<prompt>"
|
|
14
|
-
genex sfx "punchy laser zap, short and dry" --duration 2
|
|
13
|
+
npx genex sfx "<prompt>"
|
|
14
|
+
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.
|
|
@@ -21,7 +21,7 @@ Blocks until ready, then saves:
|
|
|
21
21
|
assets/sfx/<slug>.mp3
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
Committed by `genex publish`, so it ships with your game.
|
|
24
|
+
Committed by `npx genex publish`, so it ships with your game.
|
|
25
25
|
|
|
26
26
|
## Play it in Three.js
|
|
27
27
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: genex-ai-skybox
|
|
3
|
-
description: Generate a real 360° skybox (equirectangular panorama) from a text prompt with `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.
|
|
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
4
|
---
|
|
5
5
|
|
|
6
6
|
# Genex AI · Skybox
|
|
@@ -10,7 +10,7 @@ also lights it (image-based lighting).
|
|
|
10
10
|
|
|
11
11
|
## When to use this vs. a procedural sky
|
|
12
12
|
|
|
13
|
-
- **Use `genex skybox`** for a specific, recognizable sky/backdrop you can
|
|
13
|
+
- **Use `npx genex skybox`** for a specific, recognizable sky/backdrop you can
|
|
14
14
|
describe — "golden hour over misty mountains", "stormy alien sky", "city at
|
|
15
15
|
night". You get a real image.
|
|
16
16
|
- **Use `$genex-threejs-atmosphere-aerial-perspective`** (or
|
|
@@ -20,7 +20,7 @@ also lights it (image-based lighting).
|
|
|
20
20
|
## Run
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
genex skybox "<prompt>"
|
|
23
|
+
npx genex skybox "<prompt>"
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
Blocks until ready, then saves:
|
|
@@ -29,7 +29,7 @@ Blocks until ready, then saves:
|
|
|
29
29
|
assets/skybox/<slug>.jpg
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
The JPG is **committed by `genex publish`**, so it ships with your game.
|
|
32
|
+
The JPG is **committed by `npx genex publish`**, so it ships with your game.
|
|
33
33
|
|
|
34
34
|
## Load it as background + environment
|
|
35
35
|
|
|
@@ -63,7 +63,7 @@ texture.dispose; // (dispose the PMREM source later if you stop using it)
|
|
|
63
63
|
|
|
64
64
|
- Use the **relative** path `./assets/skybox/...` (GitHub Pages serves at a subpath).
|
|
65
65
|
- `base: "./"` in `vite.config.ts` before `npm run build`.
|
|
66
|
-
- Commit `assets/` — `genex publish` pushes it with the game.
|
|
66
|
+
- Commit `assets/` — `npx genex publish` pushes it with the game.
|
|
67
67
|
|
|
68
68
|
## Troubleshooting
|
|
69
69
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: genex-ai-texture
|
|
3
|
-
description: Generate a real photoreal surface texture (PBR base-color image) from a text prompt with `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.
|
|
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
4
|
---
|
|
5
5
|
|
|
6
6
|
# Genex AI · Texture
|
|
@@ -10,7 +10,7 @@ material on a primitive, a mesh, or terrain.
|
|
|
10
10
|
|
|
11
11
|
## When to use this vs. procedural materials
|
|
12
12
|
|
|
13
|
-
- **Use `genex texture`** for a specific photoreal surface you can describe —
|
|
13
|
+
- **Use `npx genex texture`** for a specific photoreal surface you can describe —
|
|
14
14
|
"weathered Roman cobblestone", "cracked desert clay", "oak planks". You get a
|
|
15
15
|
real raster image.
|
|
16
16
|
- **Use `$genex-threejs-procedural-materials`** for stylized/abstract or
|
|
@@ -20,8 +20,8 @@ material on a primitive, a mesh, or terrain.
|
|
|
20
20
|
## Run
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
genex texture "<prompt>"
|
|
24
|
-
genex texture "lush green grass" --terrain # seamless tiling for ground/terrain
|
|
23
|
+
npx genex texture "<prompt>"
|
|
24
|
+
npx genex texture "lush green grass" --terrain # seamless tiling for ground/terrain
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
Blocks until ready, then saves:
|
|
@@ -30,7 +30,7 @@ Blocks until ready, then saves:
|
|
|
30
30
|
assets/textures/<slug>/basecolor.<ext>
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
Committed by `genex publish`, so it ships with your game.
|
|
33
|
+
Committed by `npx genex publish`, so it ships with your game.
|
|
34
34
|
|
|
35
35
|
> **Scope:** v1 generates the **base-color (albedo)** map only. Normal / roughness
|
|
36
36
|
> / AO are a planned follow-up — for now set sensible `roughness`/`metalness`
|
|
@@ -11,11 +11,15 @@ architecture, and team-ready workflows.
|
|
|
11
11
|
|
|
12
12
|
## What got installed
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
`genex init` installs the Genex skills into **every coding agent it detects** —
|
|
15
|
+
Claude Code (`~/.claude`), Codex (`~/.codex`), and Cursor (`~/.cursor`) — so the
|
|
16
|
+
same skills are available whichever agent you build with. Re-running `init`
|
|
17
|
+
always refreshes the genex-owned skills to the latest version (your own files
|
|
18
|
+
are never touched).
|
|
15
19
|
|
|
16
|
-
- **skills/** - reusable Genex skills for 3D browser-game work.
|
|
17
|
-
- **agents/** - example subagent definitions.
|
|
18
|
-
- **commands/** - example slash commands.
|
|
20
|
+
- **skills/** - reusable Genex skills for 3D browser-game work (all agents).
|
|
21
|
+
- **agents/** - example subagent definitions (Claude Code).
|
|
22
|
+
- **commands/** - example slash commands (Claude Code).
|
|
19
23
|
|
|
20
24
|
Start with `$genex-threejs-skill-router` for broad game or graphics requests.
|
|
21
25
|
It routes the agent to focused skills for cameras, procedural geometry,
|
|
@@ -27,12 +31,15 @@ Beyond procedural code, Genex can generate **real, AI-made assets** from a promp
|
|
|
27
31
|
and drop them into `./assets/` (committed when you publish):
|
|
28
32
|
|
|
29
33
|
```bash
|
|
30
|
-
genex model "weathered wooden barrel" # a 3D mesh (GLB)
|
|
31
|
-
genex skybox "golden hour over mountains" # a 360° sky + lighting
|
|
32
|
-
genex sfx "punchy laser zap" --duration 2 # a sound effect (mp3)
|
|
33
|
-
genex texture "mossy cobblestone" --terrain # a tiling surface texture
|
|
34
|
+
npx genex model "weathered wooden barrel" # a 3D mesh (GLB)
|
|
35
|
+
npx genex skybox "golden hour over mountains" # a 360° sky + lighting
|
|
36
|
+
npx genex sfx "punchy laser zap" --duration 2 # a sound effect (mp3)
|
|
37
|
+
npx genex texture "mossy cobblestone" --terrain # a tiling surface texture
|
|
34
38
|
```
|
|
35
39
|
|
|
40
|
+
(Run them inside your project — the `@genex-ai/cli-demo` dev dependency makes
|
|
41
|
+
`npx genex` resolve to the right CLI.)
|
|
42
|
+
|
|
36
43
|
Each has a focused skill with the exact loader code — `$genex-ai-model`,
|
|
37
44
|
`$genex-ai-skybox`, `$genex-ai-sfx`, `$genex-ai-texture`.
|
|
38
45
|
|
|
@@ -43,7 +50,7 @@ Your existing files were left untouched. `genex init` only adds what is missing.
|
|
|
43
50
|
Safe to run any time; it never overwrites your files:
|
|
44
51
|
|
|
45
52
|
```bash
|
|
46
|
-
npx genex init
|
|
53
|
+
npx @genex-ai/cli-demo@latest init
|
|
47
54
|
```
|
|
48
55
|
|
|
49
56
|
Use `--force` only if you intentionally want to overwrite installed templates
|
|
@@ -51,6 +58,6 @@ with the latest versions.
|
|
|
51
58
|
|
|
52
59
|
## Authorization
|
|
53
60
|
|
|
54
|
-
`genex init` opens the auth site, then
|
|
55
|
-
|
|
56
|
-
browser manually to finish authorizing.
|
|
61
|
+
`genex init` opens the auth site, then saves your token to `~/.genex/env`
|
|
62
|
+
(reused across projects). If the browser doesn't open, copy the printed URL into
|
|
63
|
+
a browser manually to finish authorizing.
|
|
@@ -37,23 +37,25 @@ 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 — `genex` commands
|
|
40
|
+
## Real (AI-generated) assets — `npx genex` commands
|
|
41
41
|
|
|
42
42
|
When the user wants a **specific, recognizable asset** (a named object, a described
|
|
43
43
|
sky, a particular sound or surface) rather than something authored in code, generate
|
|
44
|
-
it with
|
|
45
|
-
`genex
|
|
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.
|
|
46
48
|
|
|
47
49
|
| Work needed | Generate with | Skill |
|
|
48
50
|
| --- | --- | --- |
|
|
49
|
-
| a specific prop / item / character / vehicle as a real mesh | `genex model "<prompt>"` | `$genex-ai-model` |
|
|
50
|
-
| a described 360° sky / backdrop + image-based lighting | `genex skybox "<prompt>"` | `$genex-ai-skybox` |
|
|
51
|
-
| a specific sound effect tied to an event | `genex sfx "<prompt>"` | `$genex-ai-sfx` |
|
|
52
|
-
| a photoreal surface/material on a mesh or terrain | `genex texture "<prompt>" [--terrain]` | `$genex-ai-texture` |
|
|
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` |
|
|
53
55
|
|
|
54
56
|
Prefer the **procedural** skills above for abstract/parametric/animated systems
|
|
55
57
|
(geometry, materials, sky, water, VFX) — no files, infinite variation. Prefer the
|
|
56
|
-
**`genex` generators** for concrete, describable, photoreal assets. They complement
|
|
58
|
+
**`npx genex` generators** for concrete, describable, photoreal assets. They complement
|
|
57
59
|
each other.
|
|
58
60
|
|
|
59
61
|
## Routing rules
|