@genex-ai/cli-demo 0.5.0 → 0.6.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/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;
@@ -918,7 +956,6 @@ async function runPublish(opts) {
918
956
  }
919
957
  if (meta.playUrl) {
920
958
  log.dim(` play: ${meta.playUrl}`);
921
- if (pushed === true) log.dim(" (GitHub Pages rebuilds ~30\u201390s after a push.)");
922
959
  }
923
960
  }
924
961
 
@@ -933,18 +970,18 @@ async function runPreview(opts) {
933
970
  process.exitCode = 1;
934
971
  return;
935
972
  }
936
- const pushed = await deployGame(meta.sshUrl, { noBuild: opts.noBuild }, log);
973
+ const pushed = await deployGame(
974
+ meta.sshUrl,
975
+ { noBuild: opts.noBuild, playUrl: meta.playUrl },
976
+ log
977
+ );
937
978
  log.plain("");
938
979
  if (!pushed) {
939
980
  log.warn("Preview not updated \u2014 see the error above.");
940
981
  process.exitCode = 1;
941
982
  return;
942
983
  }
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.)");
947
- }
984
+ log.success("Preview deployed \u2014 unlisted draft (run `genex publish` to list it).");
948
985
  }
949
986
 
950
987
  // src/commands/generate.ts
@@ -967,17 +1004,21 @@ async function downloadToFile(url, dest, headers) {
967
1004
  }
968
1005
 
969
1006
  // src/commands/generate.ts
1007
+ var PUBLIC_PREFIX = "public/";
970
1008
  var KIND_DIR = {
971
- model: "assets/models",
972
- skybox: "assets/skybox",
973
- sfx: "assets/sfx",
974
- texture: "assets/textures"
1009
+ model: "public/assets/models",
1010
+ skybox: "public/assets/skybox",
1011
+ sfx: "public/assets/sfx",
1012
+ texture: "public/assets/textures"
975
1013
  };
976
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1014
+ function runtimePath(rel) {
1015
+ return `./${rel.replace(PUBLIC_PREFIX, "")}`;
1016
+ }
1017
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
977
1018
  function fileDest(kind, slug, f) {
978
1019
  if (kind === "texture") {
979
1020
  const name = f.role.replace(/^texture-/, "");
980
- return `assets/textures/${slug}/${name}.${f.ext}`;
1021
+ return `public/assets/textures/${slug}/${name}.${f.ext}`;
981
1022
  }
982
1023
  return `${KIND_DIR[kind]}/${slug}.${f.ext}`;
983
1024
  }
@@ -1082,19 +1123,21 @@ async function poll(apiUrl, token, id, onProgress) {
1082
1123
  }
1083
1124
  } catch {
1084
1125
  }
1085
- await sleep(3e3);
1126
+ await sleep2(3e3);
1086
1127
  }
1087
1128
  return null;
1088
1129
  }
1089
1130
  function printHint(kind, written, log) {
1090
- const primary = written[0] ?? "";
1131
+ const load = runtimePath(written[0] ?? "");
1091
1132
  const hint = {
1092
- model: `Load with GLTFLoader and add to the scene \u2014 see the genex-ai-model skill. Use the relative path "./${primary}".`,
1093
- skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment \u2014 see genex-ai-skybox. Path "./${primary}".`,
1094
- sfx: `Load with AudioLoader into a THREE.PositionalAudio (camera needs an AudioListener) \u2014 see genex-ai-sfx. Path "./${primary}".`,
1095
- texture: `Load with TextureLoader, set RepeatWrapping, build a MeshStandardMaterial \u2014 see genex-ai-texture. Path "./${primary}".`
1133
+ model: `Load with GLTFLoader and add to the scene \u2014 see the genex-ai-model skill. Use the relative path "${load}".`,
1134
+ skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment \u2014 see genex-ai-skybox. Path "${load}".`,
1135
+ sfx: `Load with AudioLoader into a THREE.PositionalAudio (camera needs an AudioListener) \u2014 see genex-ai-sfx. Path "${load}".`,
1136
+ texture: `Load with TextureLoader, set RepeatWrapping, build a MeshStandardMaterial \u2014 see genex-ai-texture. Path "${load}".`
1096
1137
  };
1097
- log.success("Done. Asset saved into ./assets (commit it \u2014 `genex publish` ships it with the game).");
1138
+ log.success(
1139
+ "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)."
1140
+ );
1098
1141
  log.dim(` ${hint[kind]}`);
1099
1142
  }
1100
1143
 
@@ -1117,10 +1160,10 @@ ${c.bold("Usage")}
1117
1160
  genex init [<name>] [options] Scaffold + authorize + create the draft project.
1118
1161
  genex preview [options] Build + push to the hosted draft URL (unlisted).
1119
1162
  genex publish [options] Build + push, then list the game in the gallery.
1120
- genex model "<prompt>" [options] Generate a 3D model (GLB) into ./assets/models.
1121
- genex skybox "<prompt>" [options] Generate a skybox (equirect) into ./assets/skybox.
1122
- genex sfx "<prompt>" [options] Generate a sound effect (mp3) into ./assets/sfx.
1123
- genex texture "<prompt>" [options] Generate a PBR texture into ./assets/textures.
1163
+ genex model "<prompt>" [options] Generate a 3D model (GLB) into public/assets/models.
1164
+ genex skybox "<prompt>" [options] Generate a skybox (equirect) into public/assets/skybox.
1165
+ genex sfx "<prompt>" [options] Generate a sound effect (mp3) into public/assets/sfx.
1166
+ genex texture "<prompt>" [options] Generate a PBR texture into public/assets/textures.
1124
1167
 
1125
1168
  ${c.bold("Options for the generators (`model` `skybox` `sfx` `texture`)")}
1126
1169
  --terrain (texture) seamless tiling surface for terrain/ground.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.5.0",
3
+ "version": "0.6.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": {
@@ -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
- is **committed by `npx genex publish`**, so it ships inside your published game.
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
- - Commit the `assets/` folder `npx genex publish` pushes it with the game.
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`; commit `assets/`.
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
- - Commit `assets/` `npx genex publish` pushes it with the game.
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`; commit `assets/`.
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`,