@genex-ai/cli-demo 0.47.0 → 0.48.0-dev.75

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
@@ -8,7 +8,7 @@ import fs from "fs";
8
8
  import os from "os";
9
9
  import path from "path";
10
10
  import { fileURLToPath } from "url";
11
- var RAW_CHANNEL = "latest";
11
+ var RAW_CHANNEL = "dev";
12
12
  var CLI_CHANNEL = RAW_CHANNEL === "dev" ? "dev" : "latest";
13
13
  var STANDS = {
14
14
  prod: { api: "https://api.genex.games", dashboard: "https://genex.games" },
@@ -868,6 +868,49 @@ async function writeGitignore(dir, log) {
868
868
  await fs4.writeFile(file, next);
869
869
  log.dim(`Updated .gitignore (${toAdd.join(", ")}).`);
870
870
  }
871
+ var LFS_PATTERNS = [
872
+ // models
873
+ "*.glb",
874
+ "*.gltf",
875
+ "*.bin",
876
+ "*.vrm",
877
+ "*.fbx",
878
+ "*.obj",
879
+ // textures / images
880
+ "*.png",
881
+ "*.jpg",
882
+ "*.jpeg",
883
+ "*.webp",
884
+ "*.ktx2",
885
+ "*.basis",
886
+ "*.hdr",
887
+ "*.exr",
888
+ "*.tga",
889
+ // audio / video
890
+ "*.mp3",
891
+ "*.ogg",
892
+ "*.wav",
893
+ "*.m4a",
894
+ "*.mp4",
895
+ "*.webm"
896
+ ];
897
+ async function writeGitattributes(dir, log) {
898
+ const file = path4.join(dir, ".gitattributes");
899
+ let content = "";
900
+ try {
901
+ content = await fs4.readFile(file, "utf8");
902
+ } catch {
903
+ }
904
+ const present = new Set(content.split("\n").map((l) => l.trim().split(/\s+/)[0]));
905
+ const toAdd = LFS_PATTERNS.filter((p) => !present.has(p));
906
+ if (toAdd.length === 0) return;
907
+ let next = content;
908
+ if (next.length > 0 && !next.endsWith("\n")) next += "\n";
909
+ if (!content.trim()) next += "# Binary game assets go to Git LFS (R2) \u2014 keeps the source push small\n";
910
+ next += toAdd.map((p) => `${p} filter=lfs diff=lfs merge=lfs -text`).join("\n") + "\n";
911
+ await fs4.writeFile(file, next);
912
+ log.dim(`Updated .gitattributes (${toAdd.length} binary globs \u2192 Git LFS).`);
913
+ }
871
914
 
872
915
  // src/lib/store.ts
873
916
  import fs6 from "fs/promises";
@@ -1512,6 +1555,28 @@ async function pushWorktree(cwd, pushUrl, managed, log) {
1512
1555
  ["node_modules/", "dist/", ".git/", ".genex/", ".env", ".env.*", "!.env.example", ""].join("\n")
1513
1556
  );
1514
1557
  const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE: path10.join(gitDir, "index-source") };
1558
+ let lfs = (await run("git", ["lfs", "version"], base)).code !== 0 ? false : true;
1559
+ if (!lfs) {
1560
+ log.step("Installing git-lfs (keeps large binary assets out of the source push)\u2026");
1561
+ const [bin, args] = process.platform === "darwin" ? ["brew", ["install", "git-lfs"]] : ["sudo", ["-n", "apt-get", "install", "-y", "git-lfs"]];
1562
+ await run(bin, args, base);
1563
+ lfs = (await run("git", ["lfs", "version"], base)).code === 0;
1564
+ if (!lfs) {
1565
+ log.warn(
1566
+ "Couldn't install git-lfs \u2014 large binary assets won't be saved to your source (the game still publishes)."
1567
+ );
1568
+ }
1569
+ }
1570
+ if (lfs) {
1571
+ await writeGitattributes(cwd, log);
1572
+ const filters = [
1573
+ ["filter.lfs.clean", "git-lfs clean -- %f"],
1574
+ ["filter.lfs.smudge", "git-lfs smudge -- %f"],
1575
+ ["filter.lfs.process", "git-lfs filter-process"],
1576
+ ["filter.lfs.required", "true"]
1577
+ ];
1578
+ for (const [key, value] of filters) await run("git", ["config", key, value], base);
1579
+ }
1515
1580
  await run("git", ["add", "-A"], env);
1516
1581
  const tree = (await run("git", ["ls-files"], env)).out.trim() ? (await run("git", ["write-tree"], env)).out.trim() : "";
1517
1582
  if (!tree || tree === EMPTY_TREE) {
@@ -1520,6 +1585,10 @@ async function pushWorktree(cwd, pushUrl, managed, log) {
1520
1585
  }
1521
1586
  const commit = (await run("git", ["commit-tree", tree, "-m", "source"], { ...base, ...ident })).out.trim();
1522
1587
  await run("git", ["update-ref", "refs/heads/main", commit], base);
1588
+ if (lfs && (await run("git", ["lfs", "push", "--all", pushUrl, "main"], base)).code !== 0) {
1589
+ log.error("Couldn't upload your game's binary assets to the source store.");
1590
+ return false;
1591
+ }
1523
1592
  const push = await run(
1524
1593
  "git",
1525
1594
  ["-c", "http.postBuffer=536870912", "push", "-q", pushUrl, "+refs/heads/main:main"],
@@ -1731,12 +1800,31 @@ async function detectGameStateUsage(cwd = process.cwd()) {
1731
1800
  }
1732
1801
  return false;
1733
1802
  }
1803
+ var UI_PHASE_MARKERS = /data-phase|setPhase/;
1804
+ async function detectUiPhases(cwd = process.cwd()) {
1805
+ try {
1806
+ const html = await fs10.readFile(path11.join(cwd, "index.html"), "utf8").catch(() => "");
1807
+ if (UI_PHASE_MARKERS.test(html)) return true;
1808
+ const srcDir = path11.join(cwd, "src");
1809
+ const entries = await fs10.readdir(srcDir, { recursive: true });
1810
+ for (const rel of entries) {
1811
+ if (rel.includes("node_modules")) continue;
1812
+ if (!/\.(ts|js|mts|mjs|tsx|jsx|html|css)$/.test(rel)) continue;
1813
+ const content = await fs10.readFile(path11.join(srcDir, rel), "utf8").catch(() => "");
1814
+ if (UI_PHASE_MARKERS.test(content)) return true;
1815
+ }
1816
+ return false;
1817
+ } catch {
1818
+ return true;
1819
+ }
1820
+ }
1734
1821
  async function detectFeatures(log, cwd = process.cwd()) {
1735
1822
  return {
1736
1823
  embedSdkVersion: await detectEmbedSdkVersion(cwd),
1737
1824
  multiplayer: await detectMultiplayer(cwd),
1738
1825
  matchmaking: await detectMatchmaking(log, cwd),
1739
- gameStateUsed: await detectGameStateUsage(cwd)
1826
+ gameStateUsed: await detectGameStateUsage(cwd),
1827
+ uiPhases: await detectUiPhases(cwd)
1740
1828
  };
1741
1829
  }
1742
1830
  function advisoryNudges(log, d) {
@@ -1755,6 +1843,18 @@ function advisoryNudges(log, d) {
1755
1843
  "package.json declares genex.matchmaking but @genex-ai/multiplayer is not installed \u2014 matchmaking cannot work. Install the SDK and use matchmake() (see genex-threejs-multiplayer)."
1756
1844
  );
1757
1845
  }
1846
+ if (!d.uiPhases) {
1847
+ log.warn(
1848
+ "No menu/loader screens detected (no data-phase/setPhase) \u2014 players get a bare HUD with no title screen and no styled loading. Run the genex-threejs-game-ui plan (loader, menu, screen tiers); a cinematic menu is two commands away (genex-ai-menu)."
1849
+ );
1850
+ }
1851
+ }
1852
+ async function firstPreviewNudge(log, cwd = process.cwd()) {
1853
+ const meta = await readProject(cwd);
1854
+ if (!meta || meta.lastDeployAt || meta.status === "published") return;
1855
+ log.dim(
1856
+ " \u279C Draft page still the placeholder? Ship the first rough build now: npx genex preview \u2014 then keep building."
1857
+ );
1758
1858
  }
1759
1859
 
1760
1860
  // src/commands/publish.ts
@@ -1792,6 +1892,8 @@ async function runPublish(opts) {
1792
1892
  process.exitCode = 1;
1793
1893
  return;
1794
1894
  }
1895
+ meta.lastDeployAt = (/* @__PURE__ */ new Date()).toISOString();
1896
+ await writeProject(meta);
1795
1897
  } else {
1796
1898
  log.info("Skipping build + deploy (--no-push).");
1797
1899
  }
@@ -1875,8 +1977,9 @@ async function runPreview(opts) {
1875
1977
  const freshStatus = await fetchProjectStatus(apiUrl, token, meta.slug);
1876
1978
  if (freshStatus && freshStatus !== meta.status) {
1877
1979
  meta.status = freshStatus;
1878
- await writeProject(meta);
1879
1980
  }
1981
+ meta.lastDeployAt = (/* @__PURE__ */ new Date()).toISOString();
1982
+ await writeProject(meta);
1880
1983
  const published = meta.status === "published";
1881
1984
  log.plain("");
1882
1985
  if (published) {
@@ -2041,9 +2144,9 @@ async function awaitAndReport(apiUrl, token, id, kind, log) {
2041
2144
  process.exitCode = 1;
2042
2145
  return;
2043
2146
  }
2044
- reportTerminal(kind, view, log);
2147
+ await reportTerminal(kind, view, log);
2045
2148
  }
2046
- function reportTerminal(kind, view, log) {
2149
+ async function reportTerminal(kind, view, log) {
2047
2150
  if (view.status !== "completed") {
2048
2151
  log.error(`Generation ${view.status}${view.error ? `: ${view.error}` : ""}.`);
2049
2152
  process.exitCode = 1;
@@ -2063,6 +2166,7 @@ function reportTerminal(kind, view, log) {
2063
2166
  }
2064
2167
  log.plain("");
2065
2168
  printHint(kind, files, log);
2169
+ await firstPreviewNudge(log);
2066
2170
  }
2067
2171
  var WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
2068
2172
  var KIND_WAIT_TIMEOUT_MS = {
@@ -2211,7 +2315,7 @@ async function runWait(opts) {
2211
2315
  log.dim(` ${kind} ${id}`);
2212
2316
  log.plain("");
2213
2317
  if (TERMINAL2.has(view.status)) {
2214
- reportTerminal(kind, view, log);
2318
+ await reportTerminal(kind, view, log);
2215
2319
  return;
2216
2320
  }
2217
2321
  await awaitAndReport(apiUrl, token, id, kind, log);
@@ -2690,6 +2794,7 @@ async function runController(opts) {
2690
2794
  for (const line of set.sketch) {
2691
2795
  log.dim(` ${line}`);
2692
2796
  }
2797
+ await firstPreviewNudge(log);
2693
2798
  }
2694
2799
  async function installOwnerAvatar(args) {
2695
2800
  const { root, srcDir, apiUrl, token, log } = args;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.47.0",
3
+ "version": "0.48.0-dev.75",
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": {
@@ -157,7 +157,8 @@ const _identityMatrix = new THREE.Matrix4();
157
157
  * const physics = await PhysicsWorld.create();
158
158
  * physics.onBeforeStep(() => controller.update(physics.timeStep));
159
159
  * renderer.setAnimationLoop(() => {
160
- * physics.step(clock.getDelta()); // fixed substeps + mesh sync + events
160
+ * physics.step(delta); // fixed substeps + mesh sync + events
161
+ * // delta = capped performance.now() difference — avoid THREE.Clock.getDelta()
161
162
  * // ...camera + render (render-delta code lives OUT here, never inside)
162
163
  * });
163
164
  * ```
@@ -346,7 +346,7 @@ after a bad single costs the whole chain).
346
346
 
347
347
  ## Troubleshooting
348
348
 
349
- - **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it
349
+ - **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it
350
350
  writes your `GENEX_TOKEN`).
351
351
  - **"Prompt rejected"** — the provider's content-safety filter blocked the
352
352
  prompt. Non-retryable; rewrite the wording.
@@ -182,7 +182,7 @@ set belongs to `$genex-ai-hud` — both build on this command.
182
182
 
183
183
  ## Troubleshooting
184
184
 
185
- - **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
185
+ - **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
186
186
  - **"Prompt rejected"** — the provider's content-safety filter blocked the prompt.
187
187
  This is non-retryable; retrying the same wording fails again. Rewrite the prompt.
188
188
  - **Decal is an opaque rectangle** — the image has no alpha. Regenerate with
@@ -272,7 +272,7 @@ upgrades, in order of effort:
272
272
 
273
273
  ## Troubleshooting
274
274
 
275
- - **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it
275
+ - **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it
276
276
  writes your `GENEX_TOKEN`).
277
277
  - **"Prompt rejected"** — the provider's content-safety filter blocked the
278
278
  prompt. Non-retryable; rewrite the wording.
@@ -107,7 +107,7 @@ scene is a ghost: players and objects pass straight through it.
107
107
 
108
108
  ## Troubleshooting
109
109
 
110
- - **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
110
+ - **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
111
111
  - **"Out of credits" (`insufficient_credits`)** — the account has no credits left for
112
112
  this model generation. Tell the user the facts the CLI printed: their balance, this
113
113
  generation's cost, and when their credits refill. Then offer to continue the build
@@ -72,7 +72,7 @@ Reuse one loaded `buffer` across many plays; create a fresh `Audio`/`PositionalA
72
72
 
73
73
  ## Troubleshooting
74
74
 
75
- - **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
75
+ - **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
76
76
  - **"Out of credits" (`insufficient_credits`)** — the account has no credits left for
77
77
  this sound generation. Tell the user the facts the CLI printed: their balance, this
78
78
  generation's cost, and when their credits refill. Then offer to continue the build
@@ -76,7 +76,7 @@ scene.background = texture; // keep the raw texture for the visible sky
76
76
 
77
77
  ## Troubleshooting
78
78
 
79
- - **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
79
+ - **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
80
80
  - **"Out of credits" (`insufficient_credits`)** — the account has no credits left for
81
81
  this skybox generation. Tell the user the facts the CLI printed: their balance, this
82
82
  generation's cost, and when their credits refill. Then offer to continue the build
@@ -81,7 +81,7 @@ scene.add(ground);
81
81
 
82
82
  ## Troubleshooting
83
83
 
84
- - **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
84
+ - **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
85
85
  - **"Out of credits" (`insufficient_credits`)** — the account has no credits left for
86
86
  this texture generation. Tell the user the facts the CLI printed: their balance,
87
87
  this generation's cost, and when their credits refill. Then offer to continue the
@@ -145,7 +145,7 @@ set belongs to `$genex-ai-hud` — both build on `npx genex image`/`video`.
145
145
 
146
146
  ## Troubleshooting
147
147
 
148
- - **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
148
+ - **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
149
149
  - **"Prompt rejected"** — the provider's content-safety filter blocked the prompt.
150
150
  This is non-retryable; retrying the same wording fails again. Rewrite the prompt.
151
151
  - **Nothing plays / black surface** — the first `video.play()` must run inside a user
@@ -43,7 +43,7 @@ Add `--json` for machine-readable output.
43
43
  **As a NEW game (start from the whole project):**
44
44
 
45
45
  1. `git clone <clone URL from the output> <name>` — pick a short one-word name.
46
- 2. `cd <name>`, then `npx @genex-ai/cli-demo@latest init <name>` — never use
46
+ 2. `cd <name>`, then `npx @genex-ai/cli-demo@dev init <name>` — never use
47
47
  `--force`. This creates your own project; the original is untouched.
48
48
  3. `npm install`, keep `base: './'` in `vite.config`, then build your changes
49
49
  and ship with `npx genex preview`.
@@ -125,7 +125,7 @@ and re-link the clone to the same live game:
125
125
  ```bash
126
126
  git clone <the game's repo url> my-game && cd my-game
127
127
  npm install
128
- npx @genex-ai/cli-demo@latest link <slug> # slug = the name in the play URL
128
+ npx @genex-ai/cli-demo@dev link <slug> # slug = the name in the play URL
129
129
  ```
130
130
 
131
131
  `link` never creates a project: it signs in if needed (the browser opens once)
@@ -148,7 +148,7 @@ Safe to run any time — genex-owned skills are refreshed to the latest version,
148
148
  and your own files are never touched:
149
149
 
150
150
  ```bash
151
- npx @genex-ai/cli-demo@latest init
151
+ npx @genex-ai/cli-demo@dev init
152
152
  ```
153
153
 
154
154
  Use `--force` only if you intentionally want your own existing files overwritten
@@ -94,9 +94,14 @@ physics.onBeforeStep(() => {
94
94
  });
95
95
 
96
96
  // Render phase: step physics, then camera, then animations.
97
+ // Delta comes from performance.now(), NOT THREE.Clock — Clock.getDelta() has
98
+ // repeatedly frozen at 0 in real games (world renders, nothing moves).
97
99
  const pivot = new THREE.Vector3();
100
+ let lastT = performance.now();
98
101
  renderer.setAnimationLoop(() => {
99
- const delta = clock.getDelta();
102
+ const nowT = performance.now();
103
+ const delta = Math.min((nowT - lastT) / 1000, 0.1);
104
+ lastT = nowT;
100
105
  physics.step(delta); // runs the fixed substeps + world.step() + mesh sync
101
106
 
102
107
  pivot.copy(character.currPos).addScaledVector(character.bodyYAxis, 0.5);
@@ -169,7 +169,11 @@ Per fixed **substep** (inside `physics.onBeforeStep`), in this order:
169
169
  Per **render** frame (inside `renderer.setAnimationLoop`), in this order:
170
170
 
171
171
  ```ts
172
- const delta = clock.getDelta();
172
+ // Delta from performance.now() — THREE.Clock.getDelta() has repeatedly frozen
173
+ // at 0 in real games (world renders, nothing moves).
174
+ const nowT = performance.now();
175
+ const delta = Math.min((nowT - lastT) / 1000, 0.1);
176
+ lastT = nowT;
173
177
  physics.step(delta); // 1. fixed substeps + mesh sync
174
178
 
175
179
  pivot.copy(character.currPos).addScaledVector(character.bodyYAxis, 0.5);
@@ -59,9 +59,11 @@ physics.onBeforeStep(() => {
59
59
  // controller.update(), impulses, kinematic platform poses ...
60
60
  });
61
61
 
62
- const clock = new THREE.Clock();
62
+ let lastT = performance.now(); // NOT THREE.Clock — its getDelta() has frozen at 0 in real games
63
63
  renderer.setAnimationLoop(() => {
64
- physics.step(clock.getDelta()); // fixed substeps + mesh sync + events
64
+ const nowT = performance.now();
65
+ physics.step(Math.min((nowT - lastT) / 1000, 0.1)); // fixed substeps + mesh sync + events
66
+ lastT = nowT;
65
67
  // camera follow, mixers, HUD go HERE (render-delta work, after the step) —
66
68
  // never inside onBeforeStep above
67
69
  renderer.render(scene, camera);
@@ -51,8 +51,11 @@ physics.onBeforeStep(() => {
51
51
  drone.update(dt);
52
52
  }); // 4. world.step() runs after
53
53
 
54
+ let lastT = performance.now();
54
55
  renderer.setAnimationLoop(() => {
55
- physics.step(clock.getDelta()); // fixed substeps + interpolated mesh sync
56
+ const nowT = performance.now();
57
+ physics.step(Math.min((nowT - lastT) / 1000, 0.1)); // fixed substeps + interpolated mesh sync
58
+ lastT = nowT; // performance.now() delta — THREE.Clock.getDelta() freezes at 0 in real games
56
59
  // camera + render here — render-delta code never goes inside onBeforeStep
57
60
  });
58
61
  ```
@@ -26,11 +26,16 @@ evidence, temporal checks, budgets, and explicit rejection criteria.
26
26
 
27
27
  For plain game tasks — nothing from the procedural/visual-system pack loaded —
28
28
  this is the whole acceptance gate, and it is also the *ceiling*: a smoke check,
29
- not a certification. Run it ONCE per milestone to catch obvious breakage, fix
30
- what's clearly broken, and hand the feel/polish judgment to the player don't
31
- loop re-testing the same build, and don't try to exercise every button and edge
32
- case yourself. The player deciding "does it feel right?" is faster and truer
33
- than you clicking everything twice.
29
+ not a certification. Run it ONCE per milestone, and only AFTER that milestone's
30
+ `genex preview` is pushed ship the rough draft first, then check it; the
31
+ player may already be walking around your build while you do. Catch obvious
32
+ breakage, fix what's clearly broken, and hand the feel/polish judgment to the
33
+ player don't loop re-testing the same build, don't re-test after cosmetic
34
+ tweaks (sounds, colors, tuning values), and don't try to exercise every button
35
+ and edge case yourself. This browser check is the only testing a game draft
36
+ needs: never add unit tests or a test framework to a game unless the user asks.
37
+ The player deciding "does it feel right?" is faster and truer than you clicking
38
+ everything twice.
34
39
 
35
40
  1. Load the page in a real browser; the canvas renders (no black screen, no
36
41
  console errors). For an unpublished draft, open the dev server in local
@@ -38,7 +38,7 @@ update, so update immediately.)
38
38
  Run exactly the command the nudge printed, from the game project root:
39
39
 
40
40
  ```bash
41
- npm i -D @genex-ai/cli-demo@latest # the genex CLI (a dev dependency)
41
+ npm i -D @genex-ai/cli-demo@dev # the genex CLI (a dev dependency)
42
42
  npm i @genex-ai/embed-sdk@latest # identity/saves SDK (ships inside the game)
43
43
  npm i @genex-ai/multiplayer@latest # multiplayer SDK (only if the game uses it)
44
44
  ```