@genex-ai/cli-demo 0.43.0 → 0.45.0-dev.61
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 +13 -5
- package/dist/index.js +12 -27
- package/package.json +1 -1
- package/templates/skills/genex-ai-image/SKILL.md +1 -1
- package/templates/skills/genex-ai-model/SKILL.md +5 -3
- package/templates/skills/genex-ai-sfx/SKILL.md +1 -1
- package/templates/skills/genex-ai-skybox/SKILL.md +1 -1
- package/templates/skills/genex-ai-texture/SKILL.md +1 -1
- package/templates/skills/genex-ai-video/SKILL.md +1 -1
- package/templates/skills/genex-explore/SKILL.md +1 -1
- package/templates/skills/genex-getting-started/SKILL.md +2 -2
- package/templates/skills/genex-threejs-embed-auth/SKILL.md +70 -27
- package/templates/skills/genex-threejs-multiplayer/SKILL.md +18 -14
- package/templates/skills/genex-threejs-multiplayer/references/realtime-patterns.md +1 -1
- package/templates/skills/genex-threejs-visual-validation/SKILL.md +13 -5
- package/templates/skills/genex-updates/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -69,9 +69,12 @@ built.
|
|
|
69
69
|
**published** so it appears in the public gallery. The deploy and the gallery
|
|
70
70
|
flag are independent; `--no-push` flips the flag only.
|
|
71
71
|
|
|
72
|
-
Defaults: API `https://
|
|
73
|
-
`https://
|
|
74
|
-
`GENEX_API_URL` / `GENEX_AUTH_URL`) for local dev.
|
|
72
|
+
Defaults: API `https://api.genex.games`, auth site
|
|
73
|
+
`https://genex.games` — override with `--api-url` / `--auth-url` (or
|
|
74
|
+
`GENEX_API_URL` / `GENEX_AUTH_URL`) for local dev. Those are the `@latest`
|
|
75
|
+
(prod) channel's defaults; the `@dev` dist-tag ships the same CLI built for the
|
|
76
|
+
dev stand (`api-dev` / `dev` / `relay-dev` `.genex.games`) — the channel is
|
|
77
|
+
baked at build time via `GENEX_CLI_CHANNEL` (see `src/config.ts`).
|
|
75
78
|
|
|
76
79
|
## Generating assets
|
|
77
80
|
|
|
@@ -135,7 +138,7 @@ Options
|
|
|
135
138
|
--agents <list> Agents to install for: claude,codex,cursor (default: auto-detect)
|
|
136
139
|
--dir <path> Single destination workspace (overrides --agents)
|
|
137
140
|
--env <path> Token env file (default: ~/.genex/env)
|
|
138
|
-
--auth-url <url> Override the auth site (default: https://
|
|
141
|
+
--auth-url <url> Override the auth site (default: https://genex.games)
|
|
139
142
|
--no-auth Only scaffold templates; skip authorization
|
|
140
143
|
--force Overwrite existing files (default: never overwrite)
|
|
141
144
|
--timeout <seconds> How long to wait for the auth redirect (default: 300)
|
|
@@ -166,7 +169,7 @@ genex init --auth-url http://localhost:3000
|
|
|
166
169
|
|
|
167
170
|
The auth site URL is a single configurable value:
|
|
168
171
|
|
|
169
|
-
- **Default:** `https://
|
|
172
|
+
- **Default:** `https://genex.games` (the `DEFAULT_AUTH_URL` constant
|
|
170
173
|
in [`src/config.ts`](src/config.ts)).
|
|
171
174
|
- **Override at runtime:** set `GENEX_AUTH_URL`, or pass `--auth-url`.
|
|
172
175
|
|
|
@@ -174,6 +177,11 @@ The auth site URL is a single configurable value:
|
|
|
174
177
|
GENEX_AUTH_URL=https://staging.genex.dev genex init
|
|
175
178
|
```
|
|
176
179
|
|
|
180
|
+
The token env file path is configurable the same way: `--env <path>` per
|
|
181
|
+
command, or `GENEX_ENV_FILE=<path>` for a whole session (an explicit `--env`
|
|
182
|
+
still wins). Useful for running as a second account — a curator or a test
|
|
183
|
+
runner — without touching the default `~/.genex/env`.
|
|
184
|
+
|
|
177
185
|
To control which browser is launched, set `GENEX_BROWSER` (or the conventional
|
|
178
186
|
`BROWSER`) to an opener command. It's parsed like a shell command, so arguments
|
|
179
187
|
work and paths containing spaces should be quoted:
|
package/dist/index.js
CHANGED
|
@@ -8,9 +8,10 @@ import fs from "fs";
|
|
|
8
8
|
import os from "os";
|
|
9
9
|
import path from "path";
|
|
10
10
|
import { fileURLToPath } from "url";
|
|
11
|
-
var
|
|
12
|
-
var
|
|
13
|
-
var
|
|
11
|
+
var RAW_CHANNEL = "dev";
|
|
12
|
+
var CLI_CHANNEL = RAW_CHANNEL === "dev" ? "dev" : "latest";
|
|
13
|
+
var DEFAULT_AUTH_URL = CLI_CHANNEL === "dev" ? "https://dev.genex.games" : "https://genex.games";
|
|
14
|
+
var DEFAULT_API_URL = CLI_CHANNEL === "dev" ? "https://api-dev.genex.games" : "https://api.genex.games";
|
|
14
15
|
var DEFAULT_ANIMS_BASE = "https://cdn.genex.technology/anims/ual1/v1/";
|
|
15
16
|
var ANIMS_BASE_ENV = "GENEX_ANIMS_BASE";
|
|
16
17
|
function getAnimsBase(override) {
|
|
@@ -23,7 +24,7 @@ function getAnimsCacheDir() {
|
|
|
23
24
|
var ENV_TOKEN_KEY = "GENEX_TOKEN";
|
|
24
25
|
var AUTH_URL_ENV = "GENEX_AUTH_URL";
|
|
25
26
|
var API_URL_ENV = "GENEX_API_URL";
|
|
26
|
-
var
|
|
27
|
+
var ENV_FILE_ENV = "GENEX_ENV_FILE";
|
|
27
28
|
function getAuthUrl(override) {
|
|
28
29
|
const raw = override || process.env[AUTH_URL_ENV] || DEFAULT_AUTH_URL;
|
|
29
30
|
return raw.replace(/\/+$/, "");
|
|
@@ -32,15 +33,13 @@ function getApiUrl(override) {
|
|
|
32
33
|
const raw = override || process.env[API_URL_ENV] || DEFAULT_API_URL;
|
|
33
34
|
return raw.replace(/\/+$/, "");
|
|
34
35
|
}
|
|
35
|
-
function getColyseusUrl(override) {
|
|
36
|
-
const raw = override || process.env[COLYSEUS_URL_ENV] || DEFAULT_COLYSEUS_URL;
|
|
37
|
-
return raw.replace(/\/+$/, "");
|
|
38
|
-
}
|
|
39
36
|
function getGenexDir() {
|
|
40
37
|
return path.join(os.homedir(), ".genex");
|
|
41
38
|
}
|
|
42
39
|
function getGenexEnvPath(override) {
|
|
43
40
|
if (override) return path.resolve(override);
|
|
41
|
+
const fromEnv = process.env[ENV_FILE_ENV];
|
|
42
|
+
if (fromEnv) return path.resolve(fromEnv);
|
|
44
43
|
return path.join(getGenexDir(), "env");
|
|
45
44
|
}
|
|
46
45
|
function getTemplatesDir() {
|
|
@@ -162,7 +161,7 @@ if (sentryEnabled) {
|
|
|
162
161
|
// release aligns with the AG-757 `x-genex-cli-version` telemetry stream so
|
|
163
162
|
// the two correlate; environment is prod unless pointed at a non-default API.
|
|
164
163
|
release: `@genex-ai/cli-demo@${getCliVersion()}`,
|
|
165
|
-
environment: getApiUrl() === DEFAULT_API_URL ? "production" : "development",
|
|
164
|
+
environment: CLI_CHANNEL === "dev" ? "development" : getApiUrl() === DEFAULT_API_URL ? "production" : "development",
|
|
166
165
|
// A short-lived CLI has no meaningful tracing workload — errors only.
|
|
167
166
|
tracesSampleRate: 0,
|
|
168
167
|
// No IP / user auto-collection (opposite of the server apps' `userInfo: true`).
|
|
@@ -385,6 +384,7 @@ async function reportUpdateNudges(check, log, cwd = process.cwd()) {
|
|
|
385
384
|
const lines = [];
|
|
386
385
|
let whatsNew = null;
|
|
387
386
|
for (const pkg of PUBLISHED_PACKAGES) {
|
|
387
|
+
if (pkg.name === "@genex-ai/cli-demo" && CLI_CHANNEL === "dev") continue;
|
|
388
388
|
const latest = cache.latest[pkg.name];
|
|
389
389
|
if (!latest) continue;
|
|
390
390
|
const installed = pkg.name === "@genex-ai/cli-demo" ? getCliVersion() : await installedPackageVersion(cwd, pkg.name);
|
|
@@ -670,7 +670,7 @@ var c = {
|
|
|
670
670
|
// src/lib/api.ts
|
|
671
671
|
var CLI_VERSION_HEADER = "x-genex-cli-version";
|
|
672
672
|
function formatUpdateRequired(body) {
|
|
673
|
-
const action = body.action ??
|
|
673
|
+
const action = body.action ?? `npm i -D @genex-ai/cli-demo@${CLI_CHANNEL}`;
|
|
674
674
|
const message = body.message ?? `Genex CLI ${body.clientVersion ?? getCliVersion()} is below the minimum supported version${body.minVersion ? ` ${body.minVersion}` : ""}.`;
|
|
675
675
|
return [`${c.red("\u2717")} ${message}`, ` Update now \u2014 run: ${action} (then re-run this command)`];
|
|
676
676
|
}
|
|
@@ -766,7 +766,7 @@ async function fetchSignedInEmail(apiUrl, token) {
|
|
|
766
766
|
// src/lib/project.ts
|
|
767
767
|
import crypto2 from "crypto";
|
|
768
768
|
async function createDraftProject(opts) {
|
|
769
|
-
const { apiUrl, token,
|
|
769
|
+
const { apiUrl, token, dashboardUrl, log } = opts;
|
|
770
770
|
log.step("Creating your project\u2026");
|
|
771
771
|
const names = [opts.name, `${opts.name}-${randomSuffix()}`];
|
|
772
772
|
for (let i = 0; i < names.length; i++) {
|
|
@@ -817,7 +817,6 @@ async function createDraftProject(opts) {
|
|
|
817
817
|
slug: project.slug,
|
|
818
818
|
cloneUrl: project.cloneUrl,
|
|
819
819
|
apiUrl,
|
|
820
|
-
colyseusUrl,
|
|
821
820
|
playUrl: project.playUrl ?? void 0,
|
|
822
821
|
status: "draft",
|
|
823
822
|
// Derived client-side from the auth/web origin the CLI just authorized
|
|
@@ -946,7 +945,7 @@ async function writeUserToken(token, envPath) {
|
|
|
946
945
|
async function readUserToken(envPath) {
|
|
947
946
|
const fromGenex = await readTokenFromFile(getGenexEnvPath(envPath));
|
|
948
947
|
if (fromGenex) return fromGenex;
|
|
949
|
-
if (!envPath) {
|
|
948
|
+
if (!envPath && !process.env[ENV_FILE_ENV]) {
|
|
950
949
|
return readTokenFromFile(path6.join(process.cwd(), ".env"));
|
|
951
950
|
}
|
|
952
951
|
return null;
|
|
@@ -1000,7 +999,6 @@ function renderGenexConfig() {
|
|
|
1000
999
|
export const GENEX = {
|
|
1001
1000
|
slug: import.meta.env.VITE_GENEX_SLUG as string,
|
|
1002
1001
|
apiUrl: (import.meta.env.VITE_GENEX_API_URL as string | undefined) ?? "${DEFAULT_API_URL}",
|
|
1003
|
-
colyseusUrl: (import.meta.env.VITE_GENEX_COLYSEUS_URL as string | undefined) ?? "${DEFAULT_COLYSEUS_URL}",
|
|
1004
1002
|
dashboardOrigins: ((import.meta.env.VITE_GENEX_DASHBOARD_ORIGINS as string | undefined) ?? "${DEFAULT_DASHBOARD_ORIGIN}").split(","),
|
|
1005
1003
|
} as const;
|
|
1006
1004
|
`;
|
|
@@ -1014,9 +1012,6 @@ function renderDevOverrides(meta) {
|
|
|
1014
1012
|
const dashboardOrigins = meta.dashboardOrigins?.join(",") ?? DEFAULT_DASHBOARD_ORIGIN;
|
|
1015
1013
|
const overrides = [];
|
|
1016
1014
|
if (meta.apiUrl !== DEFAULT_API_URL) overrides.push(`VITE_GENEX_API_URL=${meta.apiUrl}`);
|
|
1017
|
-
if (meta.colyseusUrl !== DEFAULT_COLYSEUS_URL) {
|
|
1018
|
-
overrides.push(`VITE_GENEX_COLYSEUS_URL=${meta.colyseusUrl}`);
|
|
1019
|
-
}
|
|
1020
1015
|
if (dashboardOrigins !== DEFAULT_DASHBOARD_ORIGIN) {
|
|
1021
1016
|
overrides.push(`VITE_GENEX_DASHBOARD_ORIGINS=${dashboardOrigins}`);
|
|
1022
1017
|
}
|
|
@@ -1121,7 +1116,6 @@ async function runInit(opts) {
|
|
|
1121
1116
|
}
|
|
1122
1117
|
await writeGitignore(process.cwd(), log);
|
|
1123
1118
|
const apiUrl = getApiUrl(opts.apiUrl);
|
|
1124
|
-
const colyseusUrl = getColyseusUrl(opts.colyseusUrl);
|
|
1125
1119
|
const signedInEmail = await fetchSignedInEmail(apiUrl, token);
|
|
1126
1120
|
if (signedInEmail) log.plain(` signed in as ${c.cyan(signedInEmail)}`);
|
|
1127
1121
|
const projectName = opts.name?.trim() || path8.basename(process.cwd());
|
|
@@ -1132,7 +1126,6 @@ async function runInit(opts) {
|
|
|
1132
1126
|
repoUrl: opts.repo?.trim() || void 0,
|
|
1133
1127
|
private: opts.private,
|
|
1134
1128
|
remixedFromSlug: opts.remixedFrom?.trim() || void 0,
|
|
1135
|
-
colyseusUrl,
|
|
1136
1129
|
dashboardUrl: authBaseUrl,
|
|
1137
1130
|
log
|
|
1138
1131
|
});
|
|
@@ -1196,7 +1189,6 @@ async function runLink(opts) {
|
|
|
1196
1189
|
slug: project.slug,
|
|
1197
1190
|
cloneUrl: project.cloneUrl ?? "",
|
|
1198
1191
|
apiUrl,
|
|
1199
|
-
colyseusUrl: getColyseusUrl(opts.colyseusUrl),
|
|
1200
1192
|
playUrl: project.playUrl ?? void 0,
|
|
1201
1193
|
status: project.status ?? "draft",
|
|
1202
1194
|
dashboardOrigins: [new URL(authBaseUrl).origin]
|
|
@@ -2743,7 +2735,6 @@ ${c.bold("Options for `init`")}
|
|
|
2743
2735
|
--env <path> Token env file (default: ~/.genex/env).
|
|
2744
2736
|
--auth-url <url> Override the auth site (default: ${DEFAULT_AUTH_URL}).
|
|
2745
2737
|
--api-url <url> Override the API base URL (default: ${DEFAULT_API_URL}).
|
|
2746
|
-
--colyseus-url <url> Override the multiplayer URL (stored in project metadata).
|
|
2747
2738
|
--no-auth Only scaffold templates; skip authorization.
|
|
2748
2739
|
--force Overwrite existing files (default: never overwrite).
|
|
2749
2740
|
--timeout <seconds> How long to wait for the auth redirect (default: 300).
|
|
@@ -2754,7 +2745,6 @@ ${c.bold("Options for `link`")}
|
|
|
2754
2745
|
--env <path> Token env file (default: ~/.genex/env).
|
|
2755
2746
|
--auth-url <url> Override the auth site (used only if sign-in is needed).
|
|
2756
2747
|
--api-url <url> Override the API base URL.
|
|
2757
|
-
--colyseus-url <url> Override the multiplayer URL (stored in project metadata).
|
|
2758
2748
|
--timeout <seconds> How long to wait for the auth redirect (default: 300).
|
|
2759
2749
|
|
|
2760
2750
|
${c.bold("Options for `preview` / `publish`")}
|
|
@@ -2788,7 +2778,6 @@ ${c.bold("Global")}
|
|
|
2788
2778
|
${c.bold("Environment")}
|
|
2789
2779
|
GENEX_AUTH_URL Overrides the default auth site URL.
|
|
2790
2780
|
GENEX_API_URL Overrides the default API base URL.
|
|
2791
|
-
GENEX_COLYSEUS_URL Overrides the default multiplayer URL.
|
|
2792
2781
|
GENEX_BROWSER Command used to open the browser (falls back to BROWSER).
|
|
2793
2782
|
GENEX_TELEMETRY Set to 0 to disable anonymous crash reporting (Sentry).
|
|
2794
2783
|
DO_NOT_TRACK Standard opt-out; any value disables crash reporting.
|
|
@@ -2826,7 +2815,6 @@ function parseArgs(argv) {
|
|
|
2826
2815
|
"--env",
|
|
2827
2816
|
"--auth-url",
|
|
2828
2817
|
"--api-url",
|
|
2829
|
-
"--colyseus-url",
|
|
2830
2818
|
"--agents",
|
|
2831
2819
|
"--name",
|
|
2832
2820
|
"--repo",
|
|
@@ -2941,9 +2929,6 @@ function applyValueFlag(options, flag, value) {
|
|
|
2941
2929
|
case "--api-url":
|
|
2942
2930
|
options.apiUrl = value;
|
|
2943
2931
|
break;
|
|
2944
|
-
case "--colyseus-url":
|
|
2945
|
-
options.colyseusUrl = value;
|
|
2946
|
-
break;
|
|
2947
2932
|
case "--agents":
|
|
2948
2933
|
options.agents = value.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
2949
2934
|
break;
|
package/package.json
CHANGED
|
@@ -172,7 +172,7 @@ See `$genex-threejs-multiplayer` for the `shared` channel rules and the room API
|
|
|
172
172
|
|
|
173
173
|
## Troubleshooting
|
|
174
174
|
|
|
175
|
-
- **"Not authorized"** — run `npx @genex-ai/cli-demo@
|
|
175
|
+
- **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
|
|
176
176
|
- **"Prompt rejected"** — the provider's content-safety filter blocked the prompt.
|
|
177
177
|
This is non-retryable; retrying the same wording fails again. Rewrite the prompt.
|
|
178
178
|
- **Decal is an opaque rectangle** — the image has no alpha. Regenerate with
|
|
@@ -58,8 +58,10 @@ side — reads as broken at a glance.
|
|
|
58
58
|
- **Verify it, don't assume it.** Orientation **is** visible in a still — in your
|
|
59
59
|
self-check screenshot confirm the hero faces its travel direction AND that NPCs driven
|
|
60
60
|
by chase/aim code face their target (an enemy rotated 90° from its victim is this
|
|
61
|
-
pipeline's most common visible bug).
|
|
62
|
-
|
|
61
|
+
pipeline's most common visible bug). On an unpublished draft, capture real gameplay in
|
|
62
|
+
local test mode (`?genex_local_test=1` on the dev server — the embed-auth skill's
|
|
63
|
+
"Self-testing a draft" section); if you still can't capture gameplay, say so plainly
|
|
64
|
+
instead of skipping the check silently.
|
|
63
65
|
|
|
64
66
|
## Load it into the scene
|
|
65
67
|
|
|
@@ -105,7 +107,7 @@ scene is a ghost: players and objects pass straight through it.
|
|
|
105
107
|
|
|
106
108
|
## Troubleshooting
|
|
107
109
|
|
|
108
|
-
- **"Not authorized"** — run `npx @genex-ai/cli-demo@
|
|
110
|
+
- **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
|
|
109
111
|
- **"Out of credits" (`insufficient_credits`)** — the account has no credits left for
|
|
110
112
|
this model generation. Tell the user the facts the CLI printed: their balance, this
|
|
111
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@
|
|
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@
|
|
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@
|
|
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
|
|
@@ -138,7 +138,7 @@ See `$genex-threejs-multiplayer` for the `shared` channel rules and the room API
|
|
|
138
138
|
|
|
139
139
|
## Troubleshooting
|
|
140
140
|
|
|
141
|
-
- **"Not authorized"** — run `npx @genex-ai/cli-demo@
|
|
141
|
+
- **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
|
|
142
142
|
- **"Prompt rejected"** — the provider's content-safety filter blocked the prompt.
|
|
143
143
|
This is non-retryable; retrying the same wording fails again. Rewrite the prompt.
|
|
144
144
|
- **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@
|
|
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`.
|
|
@@ -122,7 +122,7 @@ and re-link the clone to the same live game:
|
|
|
122
122
|
```bash
|
|
123
123
|
git clone <the game's repo url> my-game && cd my-game
|
|
124
124
|
npm install
|
|
125
|
-
npx @genex-ai/cli-demo@
|
|
125
|
+
npx @genex-ai/cli-demo@dev link <slug> # slug = the name in the play URL
|
|
126
126
|
```
|
|
127
127
|
|
|
128
128
|
`link` never creates a project: it signs in if needed (the browser opens once)
|
|
@@ -145,7 +145,7 @@ Safe to run any time — genex-owned skills are refreshed to the latest version,
|
|
|
145
145
|
and your own files are never touched:
|
|
146
146
|
|
|
147
147
|
```bash
|
|
148
|
-
npx @genex-ai/cli-demo@
|
|
148
|
+
npx @genex-ai/cli-demo@dev init
|
|
149
149
|
```
|
|
150
150
|
|
|
151
151
|
Use `--force` only if you intentionally want your own existing files overwritten
|
|
@@ -23,6 +23,9 @@ for both, using the SDK's token. The SDK handles every context with one
|
|
|
23
23
|
zero clicks; everyone else arrives as a guest with a small dismissible
|
|
24
24
|
"sign in to save progress" popover (rendered by the SDK — don't build your
|
|
25
25
|
own). Nobody ever hits a login wall on a published game.
|
|
26
|
+
- **Local test mode (your own self-testing):** `?genex_local_test=1` on the
|
|
27
|
+
local dev server boots a credential-less local session — see "Self-testing
|
|
28
|
+
a draft" below.
|
|
26
29
|
|
|
27
30
|
While identity is resolving (or blocked) the SDK shows its own full-screen
|
|
28
31
|
overlay over the game, so never build a separate "connecting" screen for auth.
|
|
@@ -102,7 +105,7 @@ boot-path gate; `waitForAuth()` guards saves only.
|
|
|
102
105
|
- `getAuthState()` → `"pending" | "authenticated" | "guest" | "blocked"` —
|
|
103
106
|
synchronous.
|
|
104
107
|
- `getUser()` → `{ id, name, image? } | null` — non-null once authenticated OR
|
|
105
|
-
guest. Guest ids are prefixed `guest
|
|
108
|
+
guest. Guest ids are prefixed `guest:` (local test mode: `local:test`).
|
|
106
109
|
- `getEmbedToken()` → `string | undefined` — the raw token, for the RARE
|
|
107
110
|
advanced case of calling the Genex API by hand. The state/leaderboard
|
|
108
111
|
helpers below attach it automatically — prefer them; never hand-roll fetch
|
|
@@ -138,7 +141,8 @@ session is blocked):
|
|
|
138
141
|
- `getLeaderboard({ board?, limit?, order? }?)` → `Promise<{ items, me }>` —
|
|
139
142
|
top entries (verified display names — never trust client-side name input
|
|
140
143
|
for this) + the signed-in player's own `{ rank, score }`. Works for guests
|
|
141
|
-
too (`me: null`). `limit` caps at 100 server-side.
|
|
144
|
+
too (`me: null`). `limit` caps at 100 server-side. Local test mode resolves
|
|
145
|
+
`{ items: [], me: null }` locally.
|
|
142
146
|
|
|
143
147
|
Server write limits (per player, per minute): **60 player-saves, 120
|
|
144
148
|
world-saves, 30 score submits**. A debounced ~1/sec checkpoint never gets near
|
|
@@ -256,34 +260,63 @@ SDK's own top-right "sign in to save progress" popover. The return trip
|
|
|
256
260
|
carries a one-time pass (or an inert guest marker) in the URL that the SDK
|
|
257
261
|
consumes and removes immediately. Unpublished drafts are the exception:
|
|
258
262
|
strangers can't play them, so a draft link shows the SDK's sign-in gate
|
|
259
|
-
instead. Don't code around any
|
|
260
|
-
be affected, and `isEmbedded()`
|
|
261
|
-
concerns.
|
|
263
|
+
instead (for self-testing, see local test mode below). Don't code around any
|
|
264
|
+
of this: no `?`/`#` URL params of yours will be affected, and `isEmbedded()`
|
|
265
|
+
/ the return-trip handling are internal SDK concerns.
|
|
262
266
|
|
|
263
|
-
###
|
|
267
|
+
### Self-testing a draft: local test mode
|
|
264
268
|
|
|
265
269
|
An unpublished draft shows the sign-in gate to any browser that isn't signed
|
|
266
270
|
in as the owner — **including your own test browser** (Playwright, headless
|
|
267
|
-
Chrome)
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
271
|
+
Chrome) — and the hosted draft URL applies the same identity rule. The one
|
|
272
|
+
supported way to see and play the game yourself is **local test mode**: open
|
|
273
|
+
the local dev server with the explicit opt-in marker —
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
http://localhost:5173/?genex_local_test=1
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
(any port; append with `&` if the URL already has a query). On an exact http
|
|
280
|
+
loopback origin (`localhost`, `127.0.0.1`, `[::1]`) the SDK skips the
|
|
281
|
+
identity flow entirely and boots a guest-like session: no redirect, no
|
|
282
|
+
overlay, `waitForPlayer()` resolves with the unmistakable local identity
|
|
283
|
+
`{ id: "local:test", name: "Local Tester" }`, and the console prints a
|
|
284
|
+
"local test mode" notice. Requires `@genex-ai/embed-sdk` 0.5.0+ — on an older
|
|
285
|
+
project the marker does nothing; apply the pending platform update first (any
|
|
286
|
+
`genex` command prints the update nudge and how to apply it), then retry.
|
|
287
|
+
|
|
288
|
+
**It validates:** rendering, camera, controls, HUD, game feel, and real
|
|
289
|
+
gameplay screenshots — everything local.
|
|
290
|
+
|
|
291
|
+
**It does NOT validate** (nothing online exists in this mode; no credential
|
|
292
|
+
is ever minted): real sign-in (`waitForAuth()` stays pending, exactly like a
|
|
293
|
+
guest), saves (they queue in memory), leaderboards (`getLeaderboard()`
|
|
294
|
+
resolves `{ items: [], me: null }` locally), score submits, and multiplayer
|
|
295
|
+
(`getColyseusAuth()` is `undefined`, so `connect()` fails at the relay —
|
|
296
|
+
expected in this mode, not a bug to chase).
|
|
297
|
+
|
|
298
|
+
Rules:
|
|
299
|
+
|
|
300
|
+
- **Label the evidence** in your handoff: "validated in local test mode —
|
|
301
|
+
auth, saves, and multiplayer not exercised." Presenting a local-test
|
|
302
|
+
capture as full validation is an over-claim.
|
|
303
|
+
- Pointer lock still can't be acquired headlessly (`requestPointerLock`
|
|
304
|
+
throws in headless Chromium): for aim games validate the unlocked "click
|
|
305
|
+
to aim" cue and the wiring, not the lock itself (see
|
|
306
|
+
`$genex-threejs-visual-validation` step 6).
|
|
307
|
+
- The marker is inert on any hosted URL, on https, and inside any iframe —
|
|
308
|
+
local test mode cannot open the hosted draft; hosted draft access stays
|
|
309
|
+
owner-only. Do NOT work around that gate: no undocumented URL fragments,
|
|
310
|
+
no auth mocks, and never drive the user's own signed-in browser.
|
|
311
|
+
- Opening localhost WITHOUT the marker keeps the normal real-auth flow (the
|
|
312
|
+
identity bounce) — that's for testing real sign-in, not for self-testing.
|
|
313
|
+
- The owner's draft run-through stays a required beat, not a fallback: push
|
|
314
|
+
`genex preview` at each playable milestone and hand it off plainly ("check
|
|
315
|
+
the draft — you can now X; ping me if something feels off"), then keep
|
|
316
|
+
building while they look. Hosted QA catches what local test mode can't:
|
|
317
|
+
real identity, saves, multiplayer, feel. Once the game is **published**,
|
|
318
|
+
any fresh browser gets in as a guest, so full visual validation also works
|
|
319
|
+
without the marker.
|
|
287
320
|
|
|
288
321
|
## Checklist
|
|
289
322
|
|
|
@@ -304,6 +337,8 @@ and a gate capture is NOT visual evidence.
|
|
|
304
337
|
- [ ] No token value is ever logged or sent to analytics.
|
|
305
338
|
- [ ] No custom sign-in prompt, guest badge, or auth overlay — the SDK popover/
|
|
306
339
|
overlay and the dashboard own all of that UX.
|
|
340
|
+
- [ ] Self-test evidence captured in local test mode is labeled as such in the
|
|
341
|
+
handoff ("local test mode — auth, saves, and multiplayer not exercised").
|
|
307
342
|
|
|
308
343
|
## Troubleshooting
|
|
309
344
|
|
|
@@ -316,7 +351,15 @@ and a gate capture is NOT visual evidence.
|
|
|
316
351
|
- **State is `"blocked"` / `waitForPlayer()` rejects** — an unpublished draft
|
|
317
352
|
opened by a non-owner, or auth infrastructure was unreachable. The SDK
|
|
318
353
|
overlay (or the dashboard, when embedded) shows the sign-in prompt; the game
|
|
319
|
-
just stays paused behind it. Don't retry in a loop.
|
|
354
|
+
just stays paused behind it. Don't retry in a loop. Self-testing a draft
|
|
355
|
+
locally? Use local test mode (`?genex_local_test=1`) instead.
|
|
356
|
+
- **Local test mode doesn't activate** — check all four: the value is exactly
|
|
357
|
+
`genex_local_test=1`, the origin is http loopback (`localhost`/`127.0.0.1`/
|
|
358
|
+
`[::1]` — not https, not a LAN IP), the page is not inside an iframe, and
|
|
359
|
+
`@genex-ai/embed-sdk` is 0.5.0+ (older: apply the pending platform update).
|
|
360
|
+
- **Multiplayer `connect()` fails in local test mode** — by design: no relay
|
|
361
|
+
credential exists there. Validate multiplayer on the hosted draft (the
|
|
362
|
+
owner's session) or the published game, and say plainly when it wasn't.
|
|
320
363
|
- **Multiplayer join rejected with 401** — `connect()` ran before
|
|
321
364
|
`waitForPlayer()` resolved, without `auth: getColyseusAuth()!`, or with a
|
|
322
365
|
stale cached token on reconnect (read it fresh each call).
|
|
@@ -34,17 +34,17 @@ example, the shared-object/ball code, rotation, and host usage. Read
|
|
|
34
34
|
## Install
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
npm i @genex-ai/multiplayer@^0.
|
|
37
|
+
npm i @genex-ai/multiplayer@^0.10.0
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
> Pin `@^0.
|
|
41
|
-
>
|
|
42
|
-
>
|
|
40
|
+
> Pin `@^0.10.0` (not a bare `npm i`): regional relay selection (`getColyseusUrls()` + `urls`)
|
|
41
|
+
> landed in 0.10; confirmed object controls, snaps, host-tick teardown, and reconnect rebasing
|
|
42
|
+
> in 0.9. An older resolve does not have those.
|
|
43
43
|
|
|
44
|
-
This skill targets `@genex-ai/multiplayer` **≥ 0.
|
|
44
|
+
This skill targets `@genex-ai/multiplayer` **≥ 0.10.0** (`objects`/`host` since 0.4;
|
|
45
45
|
`matchmake()` since 0.5; private lobbies since 0.7; auto-reconnect + `inputs`/`onHostTick`
|
|
46
46
|
since 0.8; soft ownership handoff since 0.8.4; confirmed controls, snap epochs, and host-tick
|
|
47
|
-
lifecycle guarantees since 0.9).
|
|
47
|
+
lifecycle guarantees since 0.9; regional relay selection via `getColyseusUrls()` since 0.10).
|
|
48
48
|
|
|
49
49
|
## Trust model (say it plainly in your game's copy)
|
|
50
50
|
|
|
@@ -68,7 +68,7 @@ HUD from `mm.matchmaking` (its `status`, `queue.position`, `players`/`opponents`
|
|
|
68
68
|
// requeue) over a session, and embed tokens rotate (~10 min). A function is read fresh each (re)join;
|
|
69
69
|
// a static object goes stale and gets rejected mid-session.
|
|
70
70
|
await waitForPlayer(); // identity gate first (guest OR signed-in) — same rule as connect()
|
|
71
|
-
const mm = await matchmake<MyState>({
|
|
71
|
+
const mm = await matchmake<MyState>({ urls, room: slug, auth: () => getColyseusAuth() });
|
|
72
72
|
mm.on('matched', () => {/* session is live — start the game */});
|
|
73
73
|
// each frame: if (mm.session) renderGame(mm.session); else renderSearchingHud(mm.matchmaking);
|
|
74
74
|
mm.on('matchEnded', ({ winnerId, scores, draw }) => {/* result screen */});
|
|
@@ -182,10 +182,10 @@ and friends join it; the lobby is persistent (rounds replay, nobody is evicted):
|
|
|
182
182
|
|
|
183
183
|
```ts
|
|
184
184
|
import { createPrivate, joinPrivate } from "@genex-ai/multiplayer";
|
|
185
|
-
const lobby = await createPrivate<MyState>({
|
|
185
|
+
const lobby = await createPrivate<MyState>({ urls, room: slug, auth: () => getColyseusAuth() }); // live NOW
|
|
186
186
|
showCode(lobby.code); // share this
|
|
187
187
|
// a friend, elsewhere:
|
|
188
|
-
const lobby = await joinPrivate<MyState>(code, {
|
|
188
|
+
const lobby = await joinPrivate<MyState>(code, { urls, room: slug, auth: () => getColyseusAuth() });
|
|
189
189
|
// same handle API as matchmake(): lobby.session, lobby.matchmaking, eliminated()/score()/finish(), cancel()
|
|
190
190
|
```
|
|
191
191
|
|
|
@@ -202,13 +202,13 @@ pending for guests and would keep them out of multiplayer forever:
|
|
|
202
202
|
|
|
203
203
|
```ts
|
|
204
204
|
import { connect } from "@genex-ai/multiplayer";
|
|
205
|
-
import { waitForPlayer, getColyseusAuth } from "@genex-ai/embed-sdk";
|
|
205
|
+
import { waitForPlayer, getColyseusAuth, getColyseusUrls } from "@genex-ai/embed-sdk";
|
|
206
206
|
|
|
207
207
|
type State = { x: number; z: number; q: number[] }; // YOUR per-player state (rotation as quaternion)
|
|
208
208
|
|
|
209
209
|
const { user } = await waitForPlayer(); // player gate (guest OR signed-in) — rejects only if blocked
|
|
210
210
|
const room = await connect<State>({
|
|
211
|
-
|
|
211
|
+
urls: getColyseusUrls(), // regional relays for this session (server-owned); SDK joins the fastest
|
|
212
212
|
room: GENEX.slug, // the project slug — everyone with this id shares a room
|
|
213
213
|
name: user.name, // display name — the server prefers the verified identity's name
|
|
214
214
|
auth: getColyseusAuth()!, // REQUIRED — tokenless joins are rejected. Read fresh each connect; NEVER log it.
|
|
@@ -451,7 +451,11 @@ from any still capture whether movement feels smooth.** Don't try — it leads t
|
|
|
451
451
|
|
|
452
452
|
1. **Trust the SDK's smoothing.** Draw `state` directly; don't add your own.
|
|
453
453
|
2. **Verify it *runs*:** two clients, distinct meshes, both move, no console errors, each sees the
|
|
454
|
-
other (and the ball, if any). That's all a capture can prove.
|
|
454
|
+
other (and the ball, if any). That's all a capture can prove. Local test mode (the embed-auth
|
|
455
|
+
skill's `?genex_local_test=1`) can NOT do this: it mints no relay credential, so `connect()`
|
|
456
|
+
fails there by design and two local-test tabs never see each other — run the two-client check
|
|
457
|
+
on the published game (or have the owner open their draft), and if multiplayer wasn't
|
|
458
|
+
exercised, say exactly that in your handoff instead of implying it was.
|
|
455
459
|
3. **Then say plainly:** *"Multiplayer smoothness depends on your network and can only be felt by a
|
|
456
460
|
person — open it in two tabs or with a friend and tell me how it feels."* Stop there.
|
|
457
461
|
|
|
@@ -479,7 +483,7 @@ it; never hardcode a URL or edit the file:
|
|
|
479
483
|
|
|
480
484
|
```ts
|
|
481
485
|
import { GENEX } from "./genex.config";
|
|
482
|
-
// GENEX.slug / GENEX.
|
|
486
|
+
// GENEX.slug / GENEX.apiUrl / GENEX.dashboardOrigins
|
|
483
487
|
```
|
|
484
488
|
|
|
485
489
|
See the `genex-threejs-embed-auth` skill's "Config wiring" section for the full
|
|
@@ -527,7 +531,7 @@ host-driven saving works as long as ANY account is in the room.
|
|
|
527
531
|
|
|
528
532
|
## Checklist
|
|
529
533
|
|
|
530
|
-
- [ ] `npm i @genex-ai/multiplayer@^0.
|
|
534
|
+
- [ ] `npm i @genex-ai/multiplayer@^0.10.0` (confirmed controls, snap epochs, reconnect-safe host ticks); config wired into the build.
|
|
531
535
|
- [ ] `reconnecting`/`reconnected`/`disconnect` render an overlay (don't tear the scene down).
|
|
532
536
|
- [ ] Numbers rounded (~2 decimals) before `me.set`/`objects.set`.
|
|
533
537
|
- [ ] Pushable/ownable objects (ball, box, prop) use claim-on-touch + a Rapier proxy (the soft handoff glides the handoff); only a genuine simultaneous tug-of-war (sumo) uses the host-authoritative pattern. See host-physics.md.
|
|
@@ -27,7 +27,7 @@ type S = { x: number; z: number; q: number[]; hp: number };
|
|
|
27
27
|
|
|
28
28
|
const { user } = await waitForPlayer(); // player gate (guest OR signed-in) — never waitForAuth()
|
|
29
29
|
const room = await connect<S>({
|
|
30
|
-
|
|
30
|
+
urls: getColyseusUrls(),
|
|
31
31
|
room: GENEX.slug,
|
|
32
32
|
name: user.name,
|
|
33
33
|
auth: getColyseusAuth()!, // REQUIRED — tokenless joins are rejected. Read fresh each connect; NEVER log it.
|
|
@@ -25,18 +25,26 @@ evidence, temporal checks, budgets, and explicit rejection criteria.
|
|
|
25
25
|
## Interaction smoke check (the game fast path)
|
|
26
26
|
|
|
27
27
|
For plain game tasks — nothing from the procedural/visual-system pack loaded —
|
|
28
|
-
this is the whole acceptance gate, and it is also the
|
|
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
34
|
|
|
30
35
|
1. Load the page in a real browser; the canvas renders (no black screen, no
|
|
31
|
-
console errors).
|
|
36
|
+
console errors). For an unpublished draft, open the dev server in local
|
|
37
|
+
test mode — `http://localhost:5173/?genex_local_test=1` (the embed-auth
|
|
38
|
+
skill's "Self-testing a draft" section) — so you see the game, not the
|
|
39
|
+
sign-in gate.
|
|
32
40
|
2. Press each documented control once (keys, pointer); assert a **visible
|
|
33
41
|
response** to every one — the player moves, the camera turns, the button
|
|
34
42
|
fires.
|
|
35
43
|
3. Capture one screenshot of live gameplay — of the **game**, not a sign-in
|
|
36
44
|
gate or loading screen. A capture of the SDK's "Sign in to play" overlay is
|
|
37
|
-
NOT gameplay evidence
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
NOT gameplay evidence. Evidence captured in local test mode must be labeled
|
|
46
|
+
as such in the handoff ("local test mode — auth, saves, and multiplayer not
|
|
47
|
+
exercised"); presenting it as full validation is an over-claim.
|
|
40
48
|
4. In that screenshot, check oriented models: the hero faces its travel
|
|
41
49
|
direction, and NPCs driven by chase/aim code face their target. A model
|
|
42
50
|
rotated 90° reads as broken — `$genex-ai-model` has the one-time facing
|
|
@@ -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@
|
|
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
|
```
|