@genex-ai/cli-demo 0.5.0 → 0.6.1
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/dist/index.js +73 -25
- package/package.json +1 -1
- package/templates/skills/genex-ai-model/SKILL.md +26 -3
- package/templates/skills/genex-ai-sfx/SKILL.md +3 -2
- package/templates/skills/genex-ai-skybox/SKILL.md +3 -2
- package/templates/skills/genex-ai-texture/SKILL.md +3 -2
- package/templates/skills/genex-threejs-multiplayer/SKILL.md +35 -0
package/dist/index.js
CHANGED
|
@@ -816,6 +816,9 @@ async function deployGame(sshUrl, opts, log) {
|
|
|
816
816
|
});
|
|
817
817
|
if (push.code === 0) {
|
|
818
818
|
log.success("Pushed.");
|
|
819
|
+
if (opts.playUrl) {
|
|
820
|
+
await waitUntilLive(opts.playUrl, await buildFingerprint(siteDir), opts.liveTimeoutMs ?? 12e4, log);
|
|
821
|
+
}
|
|
819
822
|
return true;
|
|
820
823
|
}
|
|
821
824
|
log.warn("git push failed \u2014 your live game was NOT updated.");
|
|
@@ -850,6 +853,41 @@ async function fileExists(p) {
|
|
|
850
853
|
return false;
|
|
851
854
|
}
|
|
852
855
|
}
|
|
856
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
857
|
+
async function buildFingerprint(siteDir) {
|
|
858
|
+
let html;
|
|
859
|
+
try {
|
|
860
|
+
html = await fs6.readFile(path7.join(siteDir, "index.html"), "utf8");
|
|
861
|
+
} catch {
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
const m = html.match(/[A-Za-z0-9_]+-[A-Za-z0-9_-]{8}\.(?:js|css)/);
|
|
865
|
+
return m ? m[0] : null;
|
|
866
|
+
}
|
|
867
|
+
async function waitUntilLive(playUrl, fingerprint, timeoutMs, log) {
|
|
868
|
+
if (!fingerprint) {
|
|
869
|
+
log.dim(` ${playUrl}`);
|
|
870
|
+
log.dim(" (GitHub Pages rebuilds ~30\u201390s after a push.)");
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
log.step("Waiting for GitHub Pages to serve the new build\u2026");
|
|
874
|
+
const deadline = Date.now() + timeoutMs;
|
|
875
|
+
let n = 0;
|
|
876
|
+
while (Date.now() < deadline) {
|
|
877
|
+
try {
|
|
878
|
+
const res = await fetch(`${playUrl}?_genexlive=${n++}`, { cache: "no-store" });
|
|
879
|
+
if (res.ok && (await res.text()).includes(fingerprint)) {
|
|
880
|
+
log.success(`Live. \u{1F310} ${playUrl}`);
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
} catch {
|
|
884
|
+
}
|
|
885
|
+
await sleep(4e3);
|
|
886
|
+
}
|
|
887
|
+
log.warn("Still publishing \u2014 GitHub Pages can take a bit longer.");
|
|
888
|
+
log.dim(` ${playUrl}`);
|
|
889
|
+
log.dim(" (Open it in ~30\u201360s; the new build is on its way.)");
|
|
890
|
+
}
|
|
853
891
|
async function warnIfAbsolutePaths(siteDir, log) {
|
|
854
892
|
let html;
|
|
855
893
|
try {
|
|
@@ -884,7 +922,7 @@ async function runPublish(opts) {
|
|
|
884
922
|
if (opts.noPush) {
|
|
885
923
|
log.info("Skipping build + git push (--no-push).");
|
|
886
924
|
} else {
|
|
887
|
-
pushed = await deployGame(meta.sshUrl, { noBuild: opts.noBuild }, log);
|
|
925
|
+
pushed = await deployGame(meta.sshUrl, { noBuild: opts.noBuild, playUrl: meta.playUrl }, log);
|
|
888
926
|
}
|
|
889
927
|
log.step("Listing it in the gallery\u2026");
|
|
890
928
|
let res;
|
|
@@ -892,6 +930,7 @@ async function runPublish(opts) {
|
|
|
892
930
|
const body = {};
|
|
893
931
|
if (opts.title) body.title = opts.title;
|
|
894
932
|
if (opts.description) body.description = opts.description;
|
|
933
|
+
if (opts.regenerateCover) body.regenerateCover = true;
|
|
895
934
|
res = await fetch(`${apiUrl}/api/projects/${meta.id}/publish`, {
|
|
896
935
|
method: "POST",
|
|
897
936
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
@@ -918,7 +957,6 @@ async function runPublish(opts) {
|
|
|
918
957
|
}
|
|
919
958
|
if (meta.playUrl) {
|
|
920
959
|
log.dim(` play: ${meta.playUrl}`);
|
|
921
|
-
if (pushed === true) log.dim(" (GitHub Pages rebuilds ~30\u201390s after a push.)");
|
|
922
960
|
}
|
|
923
961
|
}
|
|
924
962
|
|
|
@@ -933,18 +971,18 @@ async function runPreview(opts) {
|
|
|
933
971
|
process.exitCode = 1;
|
|
934
972
|
return;
|
|
935
973
|
}
|
|
936
|
-
const pushed = await deployGame(
|
|
974
|
+
const pushed = await deployGame(
|
|
975
|
+
meta.sshUrl,
|
|
976
|
+
{ noBuild: opts.noBuild, playUrl: meta.playUrl },
|
|
977
|
+
log
|
|
978
|
+
);
|
|
937
979
|
log.plain("");
|
|
938
980
|
if (!pushed) {
|
|
939
981
|
log.warn("Preview not updated \u2014 see the error above.");
|
|
940
982
|
process.exitCode = 1;
|
|
941
983
|
return;
|
|
942
984
|
}
|
|
943
|
-
log.success("Preview deployed
|
|
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.)");
|
|
947
|
-
}
|
|
985
|
+
log.success("Preview deployed \u2014 unlisted draft (run `genex publish` to list it).");
|
|
948
986
|
}
|
|
949
987
|
|
|
950
988
|
// src/commands/generate.ts
|
|
@@ -967,17 +1005,21 @@ async function downloadToFile(url, dest, headers) {
|
|
|
967
1005
|
}
|
|
968
1006
|
|
|
969
1007
|
// src/commands/generate.ts
|
|
1008
|
+
var PUBLIC_PREFIX = "public/";
|
|
970
1009
|
var KIND_DIR = {
|
|
971
|
-
model: "assets/models",
|
|
972
|
-
skybox: "assets/skybox",
|
|
973
|
-
sfx: "assets/sfx",
|
|
974
|
-
texture: "assets/textures"
|
|
1010
|
+
model: "public/assets/models",
|
|
1011
|
+
skybox: "public/assets/skybox",
|
|
1012
|
+
sfx: "public/assets/sfx",
|
|
1013
|
+
texture: "public/assets/textures"
|
|
975
1014
|
};
|
|
976
|
-
|
|
1015
|
+
function runtimePath(rel) {
|
|
1016
|
+
return `./${rel.replace(PUBLIC_PREFIX, "")}`;
|
|
1017
|
+
}
|
|
1018
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
977
1019
|
function fileDest(kind, slug, f) {
|
|
978
1020
|
if (kind === "texture") {
|
|
979
1021
|
const name = f.role.replace(/^texture-/, "");
|
|
980
|
-
return `assets/textures/${slug}/${name}.${f.ext}`;
|
|
1022
|
+
return `public/assets/textures/${slug}/${name}.${f.ext}`;
|
|
981
1023
|
}
|
|
982
1024
|
return `${KIND_DIR[kind]}/${slug}.${f.ext}`;
|
|
983
1025
|
}
|
|
@@ -1082,19 +1124,21 @@ async function poll(apiUrl, token, id, onProgress) {
|
|
|
1082
1124
|
}
|
|
1083
1125
|
} catch {
|
|
1084
1126
|
}
|
|
1085
|
-
await
|
|
1127
|
+
await sleep2(3e3);
|
|
1086
1128
|
}
|
|
1087
1129
|
return null;
|
|
1088
1130
|
}
|
|
1089
1131
|
function printHint(kind, written, log) {
|
|
1090
|
-
const
|
|
1132
|
+
const load = runtimePath(written[0] ?? "");
|
|
1091
1133
|
const hint = {
|
|
1092
|
-
model: `Load with GLTFLoader and add to the scene \u2014 see the genex-ai-model skill. Use the relative path "
|
|
1093
|
-
skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment \u2014 see genex-ai-skybox. Path "
|
|
1094
|
-
sfx: `Load with AudioLoader into a THREE.PositionalAudio (camera needs an AudioListener) \u2014 see genex-ai-sfx. Path "
|
|
1095
|
-
texture: `Load with TextureLoader, set RepeatWrapping, build a MeshStandardMaterial \u2014 see genex-ai-texture. Path "
|
|
1134
|
+
model: `Load with GLTFLoader and add to the scene \u2014 see the genex-ai-model skill. Use the relative path "${load}".`,
|
|
1135
|
+
skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment \u2014 see genex-ai-skybox. Path "${load}".`,
|
|
1136
|
+
sfx: `Load with AudioLoader into a THREE.PositionalAudio (camera needs an AudioListener) \u2014 see genex-ai-sfx. Path "${load}".`,
|
|
1137
|
+
texture: `Load with TextureLoader, set RepeatWrapping, build a MeshStandardMaterial \u2014 see genex-ai-texture. Path "${load}".`
|
|
1096
1138
|
};
|
|
1097
|
-
log.success(
|
|
1139
|
+
log.success(
|
|
1140
|
+
"Done. Asset saved into public/assets (so Vite ships it; load it as ./assets/\u2026 \u2014 `genex publish` commits and ships it with the game)."
|
|
1141
|
+
);
|
|
1098
1142
|
log.dim(` ${hint[kind]}`);
|
|
1099
1143
|
}
|
|
1100
1144
|
|
|
@@ -1117,10 +1161,10 @@ ${c.bold("Usage")}
|
|
|
1117
1161
|
genex init [<name>] [options] Scaffold + authorize + create the draft project.
|
|
1118
1162
|
genex preview [options] Build + push to the hosted draft URL (unlisted).
|
|
1119
1163
|
genex publish [options] Build + push, then list the game in the gallery.
|
|
1120
|
-
genex model "<prompt>" [options] Generate a 3D model (GLB) into
|
|
1121
|
-
genex skybox "<prompt>" [options] Generate a skybox (equirect) into
|
|
1122
|
-
genex sfx "<prompt>" [options] Generate a sound effect (mp3) into
|
|
1123
|
-
genex texture "<prompt>" [options] Generate a PBR texture into
|
|
1164
|
+
genex model "<prompt>" [options] Generate a 3D model (GLB) into public/assets/models.
|
|
1165
|
+
genex skybox "<prompt>" [options] Generate a skybox (equirect) into public/assets/skybox.
|
|
1166
|
+
genex sfx "<prompt>" [options] Generate a sound effect (mp3) into public/assets/sfx.
|
|
1167
|
+
genex texture "<prompt>" [options] Generate a PBR texture into public/assets/textures.
|
|
1124
1168
|
|
|
1125
1169
|
${c.bold("Options for the generators (`model` `skybox` `sfx` `texture`)")}
|
|
1126
1170
|
--terrain (texture) seamless tiling surface for terrain/ground.
|
|
@@ -1147,6 +1191,7 @@ ${c.bold("Options for `preview` / `publish`")}
|
|
|
1147
1191
|
--no-push (publish) Skip build + push; only flip the gallery flag.
|
|
1148
1192
|
--title <title> (publish) Gallery title.
|
|
1149
1193
|
--description <text> (publish) Gallery description.
|
|
1194
|
+
--regenerate-cover (publish) Re-mint the disc cover (concept changed).
|
|
1150
1195
|
--api-url <url> (publish) Override the API base URL.
|
|
1151
1196
|
--env <path> Token env file (default: ~/.genex/env).
|
|
1152
1197
|
|
|
@@ -1223,6 +1268,9 @@ function parseArgs(argv) {
|
|
|
1223
1268
|
case "--force":
|
|
1224
1269
|
parsed.options.force = true;
|
|
1225
1270
|
break;
|
|
1271
|
+
case "--regenerate-cover":
|
|
1272
|
+
parsed.options.regenerateCover = true;
|
|
1273
|
+
break;
|
|
1226
1274
|
case "--quiet":
|
|
1227
1275
|
parsed.options.quiet = true;
|
|
1228
1276
|
break;
|
package/package.json
CHANGED
|
@@ -24,11 +24,33 @@ Write a specific prompt — "weathered wooden barrel with rusted iron bands" bea
|
|
|
24
24
|
"barrel". The command blocks until the mesh is ready (up to ~a minute), then saves:
|
|
25
25
|
|
|
26
26
|
```
|
|
27
|
-
assets/models/<slug>.glb
|
|
27
|
+
public/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
|
-
|
|
31
|
+
lives under `public/` so Vite ships it in the build (load it as `./assets/models/...` —
|
|
32
|
+
the `public/` prefix is stripped when served). It is **committed by `npx genex publish`**,
|
|
33
|
+
so it ships inside your published game.
|
|
34
|
+
|
|
35
|
+
## Hero models must face forward
|
|
36
|
+
|
|
37
|
+
For the model the player controls (a ship, car, character — the "hero"), the mesh should
|
|
38
|
+
**face forward**: its front pointing down the direction of travel (conventionally `+Z`,
|
|
39
|
+
i.e. "looking forward"). A hero whose nose points sideways reads as broken at a glance.
|
|
40
|
+
|
|
41
|
+
- **In the prompt:** when it's the player's craft, ask for it facing forward — e.g.
|
|
42
|
+
`"...game-ready, facing forward, front toward +Z"`.
|
|
43
|
+
- **On load (fallback):** if the imported GLB still isn't forward, rotate it once so its
|
|
44
|
+
nose aligns with travel. Wrap the model in a parent `Object3D` and apply the correction
|
|
45
|
+
to the child, so your movement code can rotate the parent cleanly:
|
|
46
|
+
```ts
|
|
47
|
+
const hero = new THREE.Object3D();
|
|
48
|
+
gltf.scene.rotation.y = Math.PI / 2; // one-time facing correction — tune per model
|
|
49
|
+
hero.add(gltf.scene);
|
|
50
|
+
scene.add(hero); // move/rotate `hero`, not gltf.scene
|
|
51
|
+
```
|
|
52
|
+
- Orientation **is** visible in a still — confirm the hero faces forward in your
|
|
53
|
+
self-check screenshot before moving on.
|
|
32
54
|
|
|
33
55
|
## Load it into the scene
|
|
34
56
|
|
|
@@ -55,7 +77,8 @@ To place many copies, `model.clone()` per instance. For animated GLBs, drive
|
|
|
55
77
|
(`/assets/...`) — GitHub Pages serves the game at a subpath.
|
|
56
78
|
- Set `base: "./"` in `vite.config.ts` before `npm run build` (see the scaffold
|
|
57
79
|
prompt's publish step).
|
|
58
|
-
-
|
|
80
|
+
- Keep generated assets under `public/assets/` — Vite only ships files under `public/`
|
|
81
|
+
(or files you `import`); `npx genex publish` commits and pushes that folder with the game.
|
|
59
82
|
|
|
60
83
|
## Options
|
|
61
84
|
|
|
@@ -18,7 +18,7 @@ npx genex sfx "punchy laser zap, short and dry" --duration 2
|
|
|
18
18
|
Blocks until ready, then saves:
|
|
19
19
|
|
|
20
20
|
```
|
|
21
|
-
assets/sfx/<slug>.mp3
|
|
21
|
+
public/assets/sfx/<slug>.mp3
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
Committed by `npx genex publish`, so it ships with your game.
|
|
@@ -55,7 +55,8 @@ Reuse one loaded `buffer` across many plays; create a fresh `Audio`/`PositionalA
|
|
|
55
55
|
|
|
56
56
|
## Publish checklist
|
|
57
57
|
|
|
58
|
-
- Relative path `./assets/sfx/...`; `base: "./"` in `vite.config.ts`;
|
|
58
|
+
- Relative path `./assets/sfx/...`; `base: "./"` in `vite.config.ts`; files live under
|
|
59
|
+
`public/assets/` so Vite ships them; `npx genex publish` commits and pushes them.
|
|
59
60
|
|
|
60
61
|
## Troubleshooting
|
|
61
62
|
|
|
@@ -26,7 +26,7 @@ npx genex skybox "<prompt>"
|
|
|
26
26
|
Blocks until ready, then saves:
|
|
27
27
|
|
|
28
28
|
```
|
|
29
|
-
assets/skybox/<slug>.jpg
|
|
29
|
+
public/assets/skybox/<slug>.jpg
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
The JPG is **committed by `npx genex publish`**, so it ships with your game.
|
|
@@ -63,7 +63,8 @@ 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
|
-
-
|
|
66
|
+
- Generated files live under `public/assets/` so Vite ships them; `npx genex publish`
|
|
67
|
+
commits and pushes that folder with the game.
|
|
67
68
|
|
|
68
69
|
## Troubleshooting
|
|
69
70
|
|
|
@@ -27,7 +27,7 @@ npx genex texture "lush green grass" --terrain # seamless tiling for ground/t
|
|
|
27
27
|
Blocks until ready, then saves:
|
|
28
28
|
|
|
29
29
|
```
|
|
30
|
-
assets/textures/<slug>/basecolor.<ext>
|
|
30
|
+
public/assets/textures/<slug>/basecolor.<ext>
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
Committed by `npx genex publish`, so it ships with your game.
|
|
@@ -67,7 +67,8 @@ scene.add(ground);
|
|
|
67
67
|
|
|
68
68
|
## Publish checklist
|
|
69
69
|
|
|
70
|
-
- Relative path `./assets/textures/...`; `base: "./"` in `vite.config.ts`;
|
|
70
|
+
- Relative path `./assets/textures/...`; `base: "./"` in `vite.config.ts`; files live under
|
|
71
|
+
`public/assets/` so Vite ships them; `npx genex publish` commits and pushes them.
|
|
71
72
|
|
|
72
73
|
## Troubleshooting
|
|
73
74
|
|
|
@@ -72,6 +72,41 @@ const room = await connect<State>({
|
|
|
72
72
|
See [references/realtime-patterns.md](references/realtime-patterns.md) for the ready-made
|
|
73
73
|
`RemoteInterpolator` helper and a complete movement example.
|
|
74
74
|
|
|
75
|
+
## Smoothness is human-tested — you cannot see it
|
|
76
|
+
|
|
77
|
+
Lag, stutter, and jitter are *motion over time*. A screenshot is one frozen instant, so
|
|
78
|
+
**you cannot tell from a screenshot (or any still capture) whether remote movement feels
|
|
79
|
+
smooth.** Don't try. Trying to "verify smoothness" from captures leads to blind parameter
|
|
80
|
+
tuning, regressions, and reverts.
|
|
81
|
+
|
|
82
|
+
Your job is bounded:
|
|
83
|
+
|
|
84
|
+
1. **Use the reference `RemoteInterpolator` as written.** Render-delay + lerp between the
|
|
85
|
+
last two snapshots, with a clamp (no extrapolation). Don't hand-roll your own smoother.
|
|
86
|
+
2. **Verify it *runs*, not that it *feels good*:** two clients, two distinct meshes, both
|
|
87
|
+
move, no console errors, each player sees the other. That's all a capture can prove.
|
|
88
|
+
3. **Then hand the feel off to the human.** Say plainly: *"Multiplayer smoothness depends on
|
|
89
|
+
your network and can only be felt by a person — open it in two tabs or with a friend and
|
|
90
|
+
tell me how it feels."* Stop there; don't keep tuning on speculation.
|
|
91
|
+
|
|
92
|
+
**Do NOT add velocity extrapolation, "keep gliding," dead-reckoning, or prediction for
|
|
93
|
+
*remote* players** until a human reports a specific symptom. The reference deliberately
|
|
94
|
+
clamps instead of extrapolating — extrapolation overshoots on turns and looks worse, which
|
|
95
|
+
is exactly the trap to avoid.
|
|
96
|
+
|
|
97
|
+
### When the human reports a problem, match the symptom (don't guess)
|
|
98
|
+
|
|
99
|
+
- **"My own plane feels laggy."** You're rendering yourself from the echoed server copy.
|
|
100
|
+
Render *yourself* from your local predicted object instead (zero latency).
|
|
101
|
+
- **"Other players teleport / stutter / move in steps."** Snapping to raw state, or
|
|
102
|
+
`RENDER_DELAY_MS` too low for the jitter. Confirm remotes go through the interpolator,
|
|
103
|
+
then raise `RENDER_DELAY_MS` (e.g. 100 → 150).
|
|
104
|
+
- **"Other players rubber-band / float past corners / overshoot."** That's extrapolation or
|
|
105
|
+
raw network round-trip latency — *more smoothing won't fix it.* Remove any extrapolation
|
|
106
|
+
and accept the small honest delay; true RTT is not a client-side bug.
|
|
107
|
+
- **"Everyone lags equally, including stationary objects."** Likely `me.set` per frame
|
|
108
|
+
flooding the relay — move publishing to the fixed 10–20 Hz tick.
|
|
109
|
+
|
|
75
110
|
## Config wiring (required — the browser can't read `.genex/project.json`)
|
|
76
111
|
|
|
77
112
|
The game runs in the browser; `.genex/project.json` (which holds `slug`, `colyseusUrl`,
|