@genex-ai/cli-demo 0.37.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dist/index.js +469 -28
- package/package.json +4 -1
- package/templates/controllers/NOTICE.md +8 -5
- package/templates/controllers/assets/animation-library.glb +0 -0
- package/templates/controllers/assets/anims-manifest.json +1306 -0
- package/templates/controllers/character/animation-packs.ts +70 -0
- package/templates/controllers/character/character-animations.ts +51 -8
- package/templates/controllers/character/character-controller.ts +153 -5
- package/templates/controllers/character/keyboard-input.ts +12 -1
- package/templates/controllers/character/presets.ts +19 -1
- package/templates/controllers/character/vrm/foot-ik.ts +71 -15
- package/templates/controllers/character/vrm/vrm-retarget.ts +72 -12
- package/templates/skills/genex-threejs-character-controller/SKILL.md +55 -22
- package/templates/skills/genex-threejs-character-controller/references/animations.md +91 -46
- package/templates/skills/genex-threejs-character-controller/references/tuning-and-presets.md +3 -0
- package/templates/skills/genex-threejs-multiplayer/references/host-physics.md +7 -6
- package/templates/skills/genex-threejs-skill-router/SKILL.md +1 -1
- package/templates/skills/genex-threejs-vehicle-controllers/references/enter-exit.md +3 -2
- package/templates/controllers/assets/character.glb +0 -0
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/
|
|
4
|
-
import
|
|
3
|
+
// src/instrument.ts
|
|
4
|
+
import * as Sentry from "@sentry/node";
|
|
5
5
|
|
|
6
6
|
// src/config.ts
|
|
7
7
|
import fs from "fs";
|
|
@@ -11,6 +11,15 @@ import { fileURLToPath } from "url";
|
|
|
11
11
|
var DEFAULT_AUTH_URL = "https://demo-web.glotech.world";
|
|
12
12
|
var DEFAULT_API_URL = "https://demo-api.glotech.world";
|
|
13
13
|
var DEFAULT_COLYSEUS_URL = "wss://demo-colyseus.glotech.world";
|
|
14
|
+
var DEFAULT_ANIMS_BASE = "https://cdn.genex.technology/anims/ual1/v1/";
|
|
15
|
+
var ANIMS_BASE_ENV = "GENEX_ANIMS_BASE";
|
|
16
|
+
function getAnimsBase(override) {
|
|
17
|
+
const raw = override || process.env[ANIMS_BASE_ENV] || DEFAULT_ANIMS_BASE;
|
|
18
|
+
return raw.replace(/\/+$/, "") + "/";
|
|
19
|
+
}
|
|
20
|
+
function getAnimsCacheDir() {
|
|
21
|
+
return path.join(getGenexDir(), "cache", "anims");
|
|
22
|
+
}
|
|
14
23
|
var ENV_TOKEN_KEY = "GENEX_TOKEN";
|
|
15
24
|
var AUTH_URL_ENV = "GENEX_AUTH_URL";
|
|
16
25
|
var API_URL_ENV = "GENEX_API_URL";
|
|
@@ -80,6 +89,107 @@ function isDir(p) {
|
|
|
80
89
|
}
|
|
81
90
|
}
|
|
82
91
|
|
|
92
|
+
// src/lib/sentry-scrub.ts
|
|
93
|
+
import os2 from "os";
|
|
94
|
+
var BEARER = /\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi;
|
|
95
|
+
var URL_CRED = /(https?:\/\/)[^/@\s:]+(?::[^/@\s]*)?@/gi;
|
|
96
|
+
var TOKENISH_KV = /((?:token|secret|password|api[_-]?key|authorization)=)[^&#\s"']+/gi;
|
|
97
|
+
var SENSITIVE_KEY = /token|secret|password|api[_-]?key|authorization|bearer/i;
|
|
98
|
+
function scrubString(s) {
|
|
99
|
+
return s.replace(URL_CRED, "$1[redacted]@").replace(BEARER, "Bearer [redacted]").replace(TOKENISH_KV, "$1[redacted]");
|
|
100
|
+
}
|
|
101
|
+
function redactHome(s, home = os2.homedir()) {
|
|
102
|
+
if (!home) return s;
|
|
103
|
+
return s.split(home).join("~");
|
|
104
|
+
}
|
|
105
|
+
var clean = (s) => redactHome(scrubString(s));
|
|
106
|
+
function scrubEvent(event) {
|
|
107
|
+
delete event.user;
|
|
108
|
+
delete event.server_name;
|
|
109
|
+
if (typeof event.message === "string") event.message = clean(event.message);
|
|
110
|
+
for (const ex of event.exception?.values ?? []) {
|
|
111
|
+
if (typeof ex.value === "string") ex.value = clean(ex.value);
|
|
112
|
+
for (const f of ex.stacktrace?.frames ?? []) {
|
|
113
|
+
if (typeof f.filename === "string") f.filename = redactHome(f.filename);
|
|
114
|
+
if (typeof f.abs_path === "string") f.abs_path = redactHome(f.abs_path);
|
|
115
|
+
delete f.vars;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (event.request && typeof event.request.url === "string") {
|
|
119
|
+
event.request.url = clean(event.request.url);
|
|
120
|
+
}
|
|
121
|
+
for (const b of event.breadcrumbs ?? []) {
|
|
122
|
+
if (typeof b.message === "string") b.message = clean(b.message);
|
|
123
|
+
const d = b.data;
|
|
124
|
+
if (d) {
|
|
125
|
+
for (const k of Object.keys(d)) {
|
|
126
|
+
if (typeof d[k] === "string") d[k] = clean(d[k]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const bag of [event.extra, event.tags]) {
|
|
131
|
+
if (!bag) continue;
|
|
132
|
+
for (const k of Object.keys(bag)) {
|
|
133
|
+
if (SENSITIVE_KEY.test(k)) bag[k] = "[Filtered]";
|
|
134
|
+
else if (typeof bag[k] === "string") bag[k] = clean(bag[k]);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return event;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/instrument.ts
|
|
141
|
+
var GENEX_CLI_DSN = "https://43efc6f7d16c3e67cad6c60fa8175c20@o4511115493900288.ingest.us.sentry.io/4511706579599360";
|
|
142
|
+
function bakedDsn() {
|
|
143
|
+
return import.meta.url.includes("/dist/") ? GENEX_CLI_DSN : "";
|
|
144
|
+
}
|
|
145
|
+
function resolveDsn() {
|
|
146
|
+
return (process.env.GENEX_SENTRY_DSN || bakedDsn()).trim();
|
|
147
|
+
}
|
|
148
|
+
function isTruthy(v) {
|
|
149
|
+
return v !== void 0 && v.trim() !== "" && !/^(0|false|off|no)$/i.test(v.trim());
|
|
150
|
+
}
|
|
151
|
+
function telemetryDisabled() {
|
|
152
|
+
if (isTruthy(process.env.DO_NOT_TRACK)) return true;
|
|
153
|
+
if (process.env.GENEX_TELEMETRY !== void 0 && !isTruthy(process.env.GENEX_TELEMETRY)) return true;
|
|
154
|
+
if (isTruthy(process.env.GENEX_DISABLE_SENTRY)) return true;
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
var dsn = resolveDsn();
|
|
158
|
+
var sentryEnabled = !!dsn && !telemetryDisabled();
|
|
159
|
+
if (sentryEnabled) {
|
|
160
|
+
Sentry.init({
|
|
161
|
+
dsn,
|
|
162
|
+
// release aligns with the AG-757 `x-genex-cli-version` telemetry stream so
|
|
163
|
+
// the two correlate; environment is prod unless pointed at a non-default API.
|
|
164
|
+
release: `@genex-ai/cli-demo@${getCliVersion()}`,
|
|
165
|
+
environment: getApiUrl() === DEFAULT_API_URL ? "production" : "development",
|
|
166
|
+
// A short-lived CLI has no meaningful tracing workload — errors only.
|
|
167
|
+
tracesSampleRate: 0,
|
|
168
|
+
// No IP / user auto-collection (opposite of the server apps' `userInfo: true`).
|
|
169
|
+
dataCollection: { userInfo: false },
|
|
170
|
+
// Keep @sentry/node's default onUncaughtException + onUnhandledRejection
|
|
171
|
+
// integrations (auto-registered) — they catch the fire-and-forget async
|
|
172
|
+
// paths in lib/updates.ts / lib/deploy.ts that otherwise swallow errors.
|
|
173
|
+
beforeSend(event) {
|
|
174
|
+
scrubEvent(event);
|
|
175
|
+
return event;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
async function flushSentry(timeoutMs = 2e3) {
|
|
180
|
+
if (!sentryEnabled) return;
|
|
181
|
+
try {
|
|
182
|
+
await Sentry.flush(timeoutMs);
|
|
183
|
+
} catch {
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/index.ts
|
|
188
|
+
import * as Sentry2 from "@sentry/node";
|
|
189
|
+
|
|
190
|
+
// src/commands/init.ts
|
|
191
|
+
import path8 from "path";
|
|
192
|
+
|
|
83
193
|
// src/lib/copy-templates.ts
|
|
84
194
|
import fs2 from "fs/promises";
|
|
85
195
|
import path2 from "path";
|
|
@@ -595,10 +705,10 @@ var structuredPrinted = /* @__PURE__ */ new WeakSet();
|
|
|
595
705
|
function printedStructuredError(res) {
|
|
596
706
|
return structuredPrinted.has(res);
|
|
597
707
|
}
|
|
598
|
-
async function apiFetch(url,
|
|
599
|
-
const headers = new Headers(
|
|
708
|
+
async function apiFetch(url, init2 = {}) {
|
|
709
|
+
const headers = new Headers(init2.headers);
|
|
600
710
|
if (!headers.has(CLI_VERSION_HEADER)) headers.set(CLI_VERSION_HEADER, getCliVersion());
|
|
601
|
-
const res = await fetch(url, { ...
|
|
711
|
+
const res = await fetch(url, { ...init2, headers });
|
|
602
712
|
if (res.status === 426) {
|
|
603
713
|
try {
|
|
604
714
|
const body = await res.clone().json();
|
|
@@ -1148,7 +1258,7 @@ async function listOwnSlugs(apiUrl, token, log) {
|
|
|
1148
1258
|
import { spawn as spawn3 } from "child_process";
|
|
1149
1259
|
import crypto3 from "crypto";
|
|
1150
1260
|
import fs9 from "fs/promises";
|
|
1151
|
-
import
|
|
1261
|
+
import os3 from "os";
|
|
1152
1262
|
import path10 from "path";
|
|
1153
1263
|
function run(cmd, args, env) {
|
|
1154
1264
|
return new Promise((resolve) => {
|
|
@@ -1345,7 +1455,7 @@ async function pushWorktree(cwd, pushUrl, managed, log) {
|
|
|
1345
1455
|
log.error("Couldn't save your game's source \u2014 please try again.");
|
|
1346
1456
|
return false;
|
|
1347
1457
|
};
|
|
1348
|
-
const gitDir = await fs9.mkdtemp(path10.join(
|
|
1458
|
+
const gitDir = await fs9.mkdtemp(path10.join(os3.tmpdir(), "genex-source-"));
|
|
1349
1459
|
const base = { GIT_DIR: gitDir };
|
|
1350
1460
|
const ident = {
|
|
1351
1461
|
GIT_AUTHOR_NAME: "genex",
|
|
@@ -1956,8 +2066,305 @@ function printHint(kind, files, log) {
|
|
|
1956
2066
|
}
|
|
1957
2067
|
|
|
1958
2068
|
// src/commands/controller.ts
|
|
2069
|
+
import fs12 from "fs/promises";
|
|
2070
|
+
import path13 from "path";
|
|
2071
|
+
|
|
2072
|
+
// src/lib/anims.ts
|
|
1959
2073
|
import fs11 from "fs/promises";
|
|
1960
2074
|
import path12 from "path";
|
|
2075
|
+
var ANIMS_DEST = path12.join("public", "assets", "anims");
|
|
2076
|
+
var HIDDEN_TAG = "reference";
|
|
2077
|
+
async function runAnims(opts) {
|
|
2078
|
+
const log = createLogger({ quiet: opts.quiet });
|
|
2079
|
+
const root = opts.cwd ?? process.cwd();
|
|
2080
|
+
const selectors = opts.selectors ?? [];
|
|
2081
|
+
log.plain(c.bold("genex controller anims"));
|
|
2082
|
+
log.plain("");
|
|
2083
|
+
const { manifest, source } = await loadManifest(opts.animsBase);
|
|
2084
|
+
if (source === "snapshot") {
|
|
2085
|
+
log.dim(" (offline or CDN unreachable \u2014 using the bundled catalog snapshot)");
|
|
2086
|
+
}
|
|
2087
|
+
if (opts.list) {
|
|
2088
|
+
printCatalog(log, manifest, selectors);
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
const controllerMarker = path12.join(root, "src", "controllers", "character");
|
|
2092
|
+
if (!await exists2(controllerMarker)) {
|
|
2093
|
+
log.error(
|
|
2094
|
+
`No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
|
|
2095
|
+
);
|
|
2096
|
+
log.plain(` Run ${c.cyan("genex controller character")} first, then re-run this command.`);
|
|
2097
|
+
process.exitCode = 1;
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
const destDir = path12.join(root, ANIMS_DEST);
|
|
2101
|
+
const gameManifestPath = path12.join(destDir, "manifest.json");
|
|
2102
|
+
if (opts.reset) {
|
|
2103
|
+
await fs11.rm(destDir, { recursive: true, force: true });
|
|
2104
|
+
log.step(`Cleared ${c.cyan(ANIMS_DEST + path12.sep)} (--reset)`);
|
|
2105
|
+
}
|
|
2106
|
+
if (selectors.length === 0) {
|
|
2107
|
+
const installed = await readGameManifest(gameManifestPath);
|
|
2108
|
+
if (installed === null || installed.clips.length === 0) {
|
|
2109
|
+
log.plain(" No animation packs installed yet.");
|
|
2110
|
+
} else {
|
|
2111
|
+
log.plain(` Installed (${installed.clips.length} clips): ${installed.clips.join(", ")}`);
|
|
2112
|
+
}
|
|
2113
|
+
log.plain("");
|
|
2114
|
+
log.plain(
|
|
2115
|
+
` Install with ${c.cyan("genex controller anims <tag|clip \u2026>")}; browse with ${c.cyan(
|
|
2116
|
+
"genex controller anims --list"
|
|
2117
|
+
)}.`
|
|
2118
|
+
);
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
let resolved;
|
|
2122
|
+
try {
|
|
2123
|
+
resolved = resolveSelectors(manifest, selectors);
|
|
2124
|
+
} catch (err) {
|
|
2125
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
2126
|
+
process.exitCode = 1;
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
2129
|
+
const coreNames = new Set(manifest.core);
|
|
2130
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2131
|
+
let bundledSkips = 0;
|
|
2132
|
+
for (const entries of resolved.values()) {
|
|
2133
|
+
for (const entry of entries) {
|
|
2134
|
+
if (coreNames.has(entry.name)) {
|
|
2135
|
+
bundledSkips++;
|
|
2136
|
+
continue;
|
|
2137
|
+
}
|
|
2138
|
+
byName.set(entry.name, entry);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
2142
|
+
const cacheDir = path12.join(
|
|
2143
|
+
opts.cacheDir ?? getAnimsCacheDir(),
|
|
2144
|
+
`${manifest.library}-v${manifest.version}`
|
|
2145
|
+
);
|
|
2146
|
+
await fs11.mkdir(cacheDir, { recursive: true });
|
|
2147
|
+
await fs11.mkdir(destDir, { recursive: true });
|
|
2148
|
+
const base = getAnimsBase(opts.animsBase);
|
|
2149
|
+
let installedCount = 0;
|
|
2150
|
+
let presentCount = 0;
|
|
2151
|
+
let addedBytes = 0;
|
|
2152
|
+
const failures = [];
|
|
2153
|
+
for (const entry of wanted) {
|
|
2154
|
+
const dest = path12.join(destDir, entry.file);
|
|
2155
|
+
if (await hasSize(dest, entry.bytes)) {
|
|
2156
|
+
presentCount++;
|
|
2157
|
+
continue;
|
|
2158
|
+
}
|
|
2159
|
+
try {
|
|
2160
|
+
const cached = path12.join(cacheDir, entry.file);
|
|
2161
|
+
if (!await hasSize(cached, entry.bytes)) {
|
|
2162
|
+
const res = await fetch(base + entry.file);
|
|
2163
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2164
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
2165
|
+
await fs11.writeFile(cached, buf);
|
|
2166
|
+
}
|
|
2167
|
+
await fs11.copyFile(cached, dest);
|
|
2168
|
+
installedCount++;
|
|
2169
|
+
addedBytes += entry.bytes;
|
|
2170
|
+
log.dim(` ${path12.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
|
|
2171
|
+
} catch (err) {
|
|
2172
|
+
failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
const previous = await readGameManifest(gameManifestPath);
|
|
2176
|
+
const union = new Set(previous?.clips ?? []);
|
|
2177
|
+
for (const entry of wanted) {
|
|
2178
|
+
if (!failures.some((f) => f.startsWith(`${entry.name} (`))) union.add(entry.name);
|
|
2179
|
+
}
|
|
2180
|
+
const gameManifest = {
|
|
2181
|
+
schema: 1,
|
|
2182
|
+
library: manifest.library,
|
|
2183
|
+
version: manifest.version,
|
|
2184
|
+
clips: [...union].sort((a, b) => a.localeCompare(b))
|
|
2185
|
+
};
|
|
2186
|
+
await fs11.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
|
|
2187
|
+
log.plain("");
|
|
2188
|
+
const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
|
|
2189
|
+
if (presentCount > 0) parts.push(`${presentCount} already present`);
|
|
2190
|
+
if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
|
|
2191
|
+
log.success(
|
|
2192
|
+
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path12.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
|
|
2193
|
+
);
|
|
2194
|
+
for (const [selector, entries] of resolved) {
|
|
2195
|
+
const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
|
|
2196
|
+
if (names.length > 0) log.dim(` ${selector}: ${names.join(", ")}`);
|
|
2197
|
+
}
|
|
2198
|
+
log.info(
|
|
2199
|
+
`Wiring: load the ${c.cyan("genex-threejs-character-controller")} skill \u2192 "Animation packs" (loadCharacterClips picks these up automatically).`
|
|
2200
|
+
);
|
|
2201
|
+
if (failures.length > 0) {
|
|
2202
|
+
log.plain("");
|
|
2203
|
+
log.error(
|
|
2204
|
+
`${failures.length} clip${failures.length === 1 ? "" : "s"} failed to download: ${failures.join(", ")}`
|
|
2205
|
+
);
|
|
2206
|
+
log.plain(
|
|
2207
|
+
" Each clip needs the network once per machine \u2014 check your connection and re-run the same command (already-installed clips are skipped)."
|
|
2208
|
+
);
|
|
2209
|
+
process.exitCode = 1;
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
async function loadManifest(baseOverride) {
|
|
2213
|
+
const base = getAnimsBase(baseOverride);
|
|
2214
|
+
try {
|
|
2215
|
+
const res = await fetch(base + "manifest.json", { signal: AbortSignal.timeout(5e3) });
|
|
2216
|
+
if (res.ok) {
|
|
2217
|
+
const manifest2 = await res.json();
|
|
2218
|
+
if (manifest2.schema === 1 && Array.isArray(manifest2.clips)) {
|
|
2219
|
+
return { manifest: manifest2, source: "cdn" };
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
} catch {
|
|
2223
|
+
}
|
|
2224
|
+
const snapshotPath = path12.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
|
|
2225
|
+
const manifest = JSON.parse(await fs11.readFile(snapshotPath, "utf8"));
|
|
2226
|
+
return { manifest, source: "snapshot" };
|
|
2227
|
+
}
|
|
2228
|
+
function resolveSelectors(manifest, selectors) {
|
|
2229
|
+
const byName = new Map(manifest.clips.map((entry) => [entry.name, entry]));
|
|
2230
|
+
const byLowerName = new Map(manifest.clips.map((entry) => [entry.name.toLowerCase(), entry]));
|
|
2231
|
+
const tags = /* @__PURE__ */ new Map();
|
|
2232
|
+
for (const entry of manifest.clips) {
|
|
2233
|
+
for (const tag of entry.tags) {
|
|
2234
|
+
const list = tags.get(tag) ?? [];
|
|
2235
|
+
list.push(entry);
|
|
2236
|
+
tags.set(tag, list);
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
const out = /* @__PURE__ */ new Map();
|
|
2240
|
+
for (const selector of selectors) {
|
|
2241
|
+
const exact = byName.get(selector) ?? byLowerName.get(selector.toLowerCase());
|
|
2242
|
+
if (exact) {
|
|
2243
|
+
out.set(selector, [exact]);
|
|
2244
|
+
continue;
|
|
2245
|
+
}
|
|
2246
|
+
const tagHit = tags.get(selector) ?? tags.get(selector.toLowerCase());
|
|
2247
|
+
if (tagHit) {
|
|
2248
|
+
out.set(selector, tagHit);
|
|
2249
|
+
continue;
|
|
2250
|
+
}
|
|
2251
|
+
const candidates = [...tags.keys(), ...byName.keys()];
|
|
2252
|
+
const close = suggest(selector, candidates);
|
|
2253
|
+
throw new Error(
|
|
2254
|
+
`unknown clip/tag "${selector}"${close.length > 0 ? ` \u2014 closest: ${close.join(", ")}` : ""}. Run ${c.cyan(
|
|
2255
|
+
"genex controller anims --list"
|
|
2256
|
+
)} for the catalog.`
|
|
2257
|
+
);
|
|
2258
|
+
}
|
|
2259
|
+
return out;
|
|
2260
|
+
}
|
|
2261
|
+
function suggest(input, candidates) {
|
|
2262
|
+
const lower = input.toLowerCase();
|
|
2263
|
+
const scored = [];
|
|
2264
|
+
for (const candidate of candidates) {
|
|
2265
|
+
const candidateLower = candidate.toLowerCase();
|
|
2266
|
+
if (candidateLower.includes(lower) || lower.includes(candidateLower)) {
|
|
2267
|
+
scored.push({ name: candidate, score: 0 });
|
|
2268
|
+
continue;
|
|
2269
|
+
}
|
|
2270
|
+
const distance = levenshtein(lower, candidateLower, 2);
|
|
2271
|
+
if (distance <= 2) scored.push({ name: candidate, score: distance });
|
|
2272
|
+
}
|
|
2273
|
+
scored.sort((a, b) => a.score - b.score || a.name.localeCompare(b.name));
|
|
2274
|
+
return scored.slice(0, 4).map((s) => s.name);
|
|
2275
|
+
}
|
|
2276
|
+
function levenshtein(a, b, max) {
|
|
2277
|
+
if (Math.abs(a.length - b.length) > max) return max + 1;
|
|
2278
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
2279
|
+
for (let i = 1; i <= a.length; i++) {
|
|
2280
|
+
const curr = [i];
|
|
2281
|
+
let rowMin = i;
|
|
2282
|
+
for (let j = 1; j <= b.length; j++) {
|
|
2283
|
+
curr[j] = Math.min(
|
|
2284
|
+
prev[j] + 1,
|
|
2285
|
+
curr[j - 1] + 1,
|
|
2286
|
+
prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
|
|
2287
|
+
);
|
|
2288
|
+
if (curr[j] < rowMin) rowMin = curr[j];
|
|
2289
|
+
}
|
|
2290
|
+
if (rowMin > max) return max + 1;
|
|
2291
|
+
prev = curr;
|
|
2292
|
+
}
|
|
2293
|
+
return prev[b.length];
|
|
2294
|
+
}
|
|
2295
|
+
function printCatalog(log, manifest, selectors) {
|
|
2296
|
+
const coreNames = new Set(manifest.core);
|
|
2297
|
+
if (selectors.length > 0) {
|
|
2298
|
+
let resolved;
|
|
2299
|
+
try {
|
|
2300
|
+
resolved = resolveSelectors(manifest, selectors);
|
|
2301
|
+
} catch (err) {
|
|
2302
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
2303
|
+
process.exitCode = 1;
|
|
2304
|
+
return;
|
|
2305
|
+
}
|
|
2306
|
+
for (const [selector, entries] of resolved) {
|
|
2307
|
+
log.plain(c.bold(selector));
|
|
2308
|
+
for (const entry of entries) {
|
|
2309
|
+
const bundled = coreNames.has(entry.name) ? " (bundled)" : "";
|
|
2310
|
+
log.plain(
|
|
2311
|
+
` ${entry.name.padEnd(26)} ${entry.duration.toFixed(1)}s ${formatMb(entry.bytes)}${bundled} ${c.dim(entry.desc)}`
|
|
2312
|
+
);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
const tags = /* @__PURE__ */ new Map();
|
|
2318
|
+
for (const entry of manifest.clips) {
|
|
2319
|
+
for (const tag of entry.tags) {
|
|
2320
|
+
if (tag === HIDDEN_TAG) continue;
|
|
2321
|
+
const list = tags.get(tag) ?? [];
|
|
2322
|
+
list.push(entry);
|
|
2323
|
+
tags.set(tag, list);
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
log.plain(
|
|
2327
|
+
`${c.bold(`Animation packs`)} (${manifest.library} v${manifest.version}, ${manifest.clips.length} clips)`
|
|
2328
|
+
);
|
|
2329
|
+
log.plain(
|
|
2330
|
+
` Install: ${c.cyan("genex controller anims <tag|clip \u2026>")} Details: ${c.cyan(
|
|
2331
|
+
"genex controller anims --list <tag>"
|
|
2332
|
+
)}`
|
|
2333
|
+
);
|
|
2334
|
+
log.plain("");
|
|
2335
|
+
for (const [tag, entries] of [...tags.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
2336
|
+
const bytes = entries.reduce((sum, entry) => sum + entry.bytes, 0);
|
|
2337
|
+
const suffix = tag === "core" ? " \u2014 bundled in animation-library.glb" : ` (${formatMb(bytes)})`;
|
|
2338
|
+
log.plain(` ${c.bold(tag.padEnd(18))} ${entries.map((e) => e.name).join(", ")}${suffix}`);
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
async function readGameManifest(file) {
|
|
2342
|
+
try {
|
|
2343
|
+
return JSON.parse(await fs11.readFile(file, "utf8"));
|
|
2344
|
+
} catch {
|
|
2345
|
+
return null;
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
async function hasSize(file, bytes) {
|
|
2349
|
+
try {
|
|
2350
|
+
return (await fs11.stat(file)).size === bytes;
|
|
2351
|
+
} catch {
|
|
2352
|
+
return false;
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
async function exists2(p) {
|
|
2356
|
+
try {
|
|
2357
|
+
await fs11.access(p);
|
|
2358
|
+
return true;
|
|
2359
|
+
} catch {
|
|
2360
|
+
return false;
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
function formatMb(bytes) {
|
|
2364
|
+
return bytes >= 1e6 ? `${(bytes / 1e6).toFixed(1)} MB` : `${Math.round(bytes / 1e3)} KB`;
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
// src/commands/controller.ts
|
|
1961
2368
|
var CONTROLLER_KINDS = ["character", "car", "drone"];
|
|
1962
2369
|
var SHARED = [
|
|
1963
2370
|
"shared/math.ts",
|
|
@@ -1976,6 +2383,7 @@ var CONTROLLER_FILE_SETS = {
|
|
|
1976
2383
|
...SHARED,
|
|
1977
2384
|
"character/character-controller.ts",
|
|
1978
2385
|
"character/character-animations.ts",
|
|
2386
|
+
"character/animation-packs.ts",
|
|
1979
2387
|
"character/presets.ts",
|
|
1980
2388
|
// VRM avatar support (three-vrm): load + retarget the UAL clips + auto-fit
|
|
1981
2389
|
// the capsule + optional foot IK. Owner's avatar replaces the old mannequin.
|
|
@@ -1988,15 +2396,16 @@ var CONTROLLER_FILE_SETS = {
|
|
|
1988
2396
|
],
|
|
1989
2397
|
// The player's VRM is written to public/assets/avatar.vrm at install time by
|
|
1990
2398
|
// installOwnerAvatar (owner's avatar, or the bundled default) — so it is NOT
|
|
1991
|
-
// a static manifest asset. animation-library.glb (
|
|
2399
|
+
// a static manifest asset. animation-library.glb (the 12-clip core) still is;
|
|
2400
|
+
// extra clips arrive via `genex controller anims` into public/assets/anims/.
|
|
1992
2401
|
assets: ["assets/animation-library.glb"],
|
|
1993
2402
|
skill: "genex-threejs-character-controller",
|
|
1994
2403
|
sketch: [
|
|
1995
2404
|
`const physics = await PhysicsWorld.create();`,
|
|
1996
2405
|
`const { scene, vrm } = await loadVrm("./assets/avatar.vrm");`,
|
|
1997
|
-
`const
|
|
2406
|
+
`const clips = await loadCharacterClips(vrm); // core library + every genex-controller-anims pack`,
|
|
1998
2407
|
`const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...capsuleFromModel(scene), position: { x: 0, y: 2, z: 0 } });`,
|
|
1999
|
-
`character.root.add(scene); const anims = new CharacterAnimations(scene,
|
|
2408
|
+
`character.root.add(scene); const anims = new CharacterAnimations(scene, clips);`,
|
|
2000
2409
|
`addEventListener("pointerdown", () => anims.playOneShot("Punch_Jab")); // per frame: anims.update(character, dt); vrm.update(dt);`
|
|
2001
2410
|
]
|
|
2002
2411
|
},
|
|
@@ -2036,45 +2445,49 @@ var CONTROLLER_FILE_SETS = {
|
|
|
2036
2445
|
]
|
|
2037
2446
|
}
|
|
2038
2447
|
};
|
|
2039
|
-
var CODE_DEST =
|
|
2040
|
-
var ASSETS_DEST =
|
|
2448
|
+
var CODE_DEST = path13.join("src", "controllers");
|
|
2449
|
+
var ASSETS_DEST = path13.join("public", "assets");
|
|
2041
2450
|
async function runController(opts) {
|
|
2042
2451
|
const log = createLogger({ quiet: opts.quiet });
|
|
2452
|
+
if (opts.kind?.trim() === "anims") {
|
|
2453
|
+
await runAnims(opts);
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2043
2456
|
const kind = opts.kind?.trim();
|
|
2044
2457
|
if (!kind || !CONTROLLER_KINDS.includes(kind)) {
|
|
2045
2458
|
log.error(
|
|
2046
2459
|
`Missing or unknown controller type${kind ? ` "${kind}"` : ""}. Usage: ${c.cyan(
|
|
2047
2460
|
"genex controller <character|car|drone> [--force]"
|
|
2048
|
-
)}`
|
|
2461
|
+
)} or ${c.cyan("genex controller anims <tag|clip \u2026>")}`
|
|
2049
2462
|
);
|
|
2050
2463
|
process.exitCode = 1;
|
|
2051
2464
|
return;
|
|
2052
2465
|
}
|
|
2053
|
-
const srcDir =
|
|
2466
|
+
const srcDir = path13.join(getTemplatesDir(), "controllers");
|
|
2054
2467
|
const root = opts.cwd ?? process.cwd();
|
|
2055
2468
|
const set = CONTROLLER_FILE_SETS[kind];
|
|
2056
2469
|
log.plain(c.bold(`genex controller ${kind}`));
|
|
2057
2470
|
log.plain("");
|
|
2058
|
-
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST +
|
|
2471
|
+
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path13.sep)}`);
|
|
2059
2472
|
const plan = [
|
|
2060
|
-
...set.code.map((rel) => ({ from: rel, rel:
|
|
2473
|
+
...set.code.map((rel) => ({ from: rel, rel: path13.join(CODE_DEST, rel) })),
|
|
2061
2474
|
...set.assets.map((rel) => ({
|
|
2062
2475
|
from: rel,
|
|
2063
|
-
rel:
|
|
2476
|
+
rel: path13.join(ASSETS_DEST, path13.basename(rel))
|
|
2064
2477
|
}))
|
|
2065
2478
|
];
|
|
2066
2479
|
let copied = 0;
|
|
2067
2480
|
let skipped = 0;
|
|
2068
2481
|
try {
|
|
2069
2482
|
for (const file of plan) {
|
|
2070
|
-
const dest =
|
|
2071
|
-
if (!opts.force && await
|
|
2483
|
+
const dest = path13.join(root, file.rel);
|
|
2484
|
+
if (!opts.force && await exists3(dest)) {
|
|
2072
2485
|
skipped++;
|
|
2073
2486
|
log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
|
|
2074
2487
|
continue;
|
|
2075
2488
|
}
|
|
2076
|
-
await
|
|
2077
|
-
await
|
|
2489
|
+
await fs12.mkdir(path13.dirname(dest), { recursive: true });
|
|
2490
|
+
await fs12.copyFile(path13.join(srcDir, file.from), dest);
|
|
2078
2491
|
copied++;
|
|
2079
2492
|
log.dim(` ${file.rel}`);
|
|
2080
2493
|
}
|
|
@@ -2106,8 +2519,8 @@ async function runController(opts) {
|
|
|
2106
2519
|
}
|
|
2107
2520
|
async function installOwnerAvatar(args) {
|
|
2108
2521
|
const { root, srcDir, apiUrl, token, log } = args;
|
|
2109
|
-
const dest =
|
|
2110
|
-
await
|
|
2522
|
+
const dest = path13.join(root, ASSETS_DEST, "avatar.vrm");
|
|
2523
|
+
await fs12.mkdir(path13.dirname(dest), { recursive: true });
|
|
2111
2524
|
if (token) {
|
|
2112
2525
|
try {
|
|
2113
2526
|
const res = await apiFetch(`${apiUrl}/api/avatars/me`, {
|
|
@@ -2119,7 +2532,7 @@ async function installOwnerAvatar(args) {
|
|
|
2119
2532
|
const vrmRes = await fetch(me.vrmUrl);
|
|
2120
2533
|
if (vrmRes.ok) {
|
|
2121
2534
|
const buf = Buffer.from(await vrmRes.arrayBuffer());
|
|
2122
|
-
await
|
|
2535
|
+
await fs12.writeFile(dest, buf);
|
|
2123
2536
|
log.dim(` public/assets/avatar.vrm (your avatar \u2014 ${(buf.length / 1e6).toFixed(1)} MB)`);
|
|
2124
2537
|
return;
|
|
2125
2538
|
}
|
|
@@ -2130,14 +2543,14 @@ async function installOwnerAvatar(args) {
|
|
|
2130
2543
|
log.dim(" avatar fetch failed (offline?); using the bundled default.");
|
|
2131
2544
|
}
|
|
2132
2545
|
}
|
|
2133
|
-
await
|
|
2546
|
+
await fs12.copyFile(path13.join(srcDir, "assets", "default-avatar.vrm"), dest);
|
|
2134
2547
|
log.dim(
|
|
2135
2548
|
token ? " public/assets/avatar.vrm (bundled default)" : " public/assets/avatar.vrm (bundled default \u2014 sign in and re-run for your own)"
|
|
2136
2549
|
);
|
|
2137
2550
|
}
|
|
2138
|
-
async function
|
|
2551
|
+
async function exists3(p) {
|
|
2139
2552
|
try {
|
|
2140
|
-
await
|
|
2553
|
+
await fs12.access(p);
|
|
2141
2554
|
return true;
|
|
2142
2555
|
} catch {
|
|
2143
2556
|
return false;
|
|
@@ -2230,6 +2643,10 @@ ${c.bold("Usage")}
|
|
|
2230
2643
|
genex video "<prompt>" [options] Generate a video (mp4); prints a public asset URL.
|
|
2231
2644
|
genex controller <type> [--force] Install a physics controller (character|car|drone)
|
|
2232
2645
|
into src/controllers (+ assets into public/assets).
|
|
2646
|
+
genex controller anims <sel \u2026> Download extra character animation clips by tag or
|
|
2647
|
+
exact name (sword, stealth, Celebration, \u2026) into
|
|
2648
|
+
public/assets/anims/. --list shows the catalog;
|
|
2649
|
+
--reset forgets the previous selection first.
|
|
2233
2650
|
genex explore ["<query>"] [options] Search the curated community gallery \u2014 proven
|
|
2234
2651
|
Three.js systems you can clone or borrow parts
|
|
2235
2652
|
from. No query lists the whole catalog.
|
|
@@ -2303,6 +2720,8 @@ ${c.bold("Environment")}
|
|
|
2303
2720
|
GENEX_API_URL Overrides the default API base URL.
|
|
2304
2721
|
GENEX_COLYSEUS_URL Overrides the default multiplayer URL.
|
|
2305
2722
|
GENEX_BROWSER Command used to open the browser (falls back to BROWSER).
|
|
2723
|
+
GENEX_TELEMETRY Set to 0 to disable anonymous crash reporting (Sentry).
|
|
2724
|
+
DO_NOT_TRACK Standard opt-out; any value disables crash reporting.
|
|
2306
2725
|
|
|
2307
2726
|
${c.bold("Examples")}
|
|
2308
2727
|
genex init my-game
|
|
@@ -2390,6 +2809,12 @@ function parseArgs(argv) {
|
|
|
2390
2809
|
case "--force":
|
|
2391
2810
|
parsed.options.force = true;
|
|
2392
2811
|
break;
|
|
2812
|
+
case "--list":
|
|
2813
|
+
parsed.options.list = true;
|
|
2814
|
+
break;
|
|
2815
|
+
case "--reset":
|
|
2816
|
+
parsed.options.reset = true;
|
|
2817
|
+
break;
|
|
2393
2818
|
case "--regenerate-cover":
|
|
2394
2819
|
parsed.options.regenerateCover = true;
|
|
2395
2820
|
break;
|
|
@@ -2419,6 +2844,8 @@ function parseArgs(argv) {
|
|
|
2419
2844
|
parsed.options.name = arg;
|
|
2420
2845
|
} else if (parsed.command === "explore") {
|
|
2421
2846
|
parsed.options.name = `${parsed.options.name} ${arg}`;
|
|
2847
|
+
} else if (parsed.command === "controller") {
|
|
2848
|
+
(parsed.options.selectors ??= []).push(arg);
|
|
2422
2849
|
} else {
|
|
2423
2850
|
parsed.error = `Unexpected argument: ${arg}`;
|
|
2424
2851
|
return parsed;
|
|
@@ -2530,6 +2957,17 @@ async function main() {
|
|
|
2530
2957
|
const updateLog = createLogger({ quiet: parsed.options.quiet });
|
|
2531
2958
|
if (parsed.command !== "init") await syncSkills(updateLog);
|
|
2532
2959
|
const updateCheck = startUpdateCheck();
|
|
2960
|
+
Sentry2.setTag("command", parsed.command);
|
|
2961
|
+
Sentry2.setTag("cli.version", getCliVersion());
|
|
2962
|
+
Sentry2.setTag("node.version", process.versions.node);
|
|
2963
|
+
Sentry2.setTag("os.platform", process.platform);
|
|
2964
|
+
Sentry2.setContext("runtime", {
|
|
2965
|
+
cliVersion: getCliVersion(),
|
|
2966
|
+
node: process.version,
|
|
2967
|
+
platform: process.platform,
|
|
2968
|
+
arch: process.arch,
|
|
2969
|
+
ci: !!process.env.CI
|
|
2970
|
+
});
|
|
2533
2971
|
try {
|
|
2534
2972
|
if (GEN_KINDS.has(parsed.command)) {
|
|
2535
2973
|
await runGenerate(parsed.command, {
|
|
@@ -2567,10 +3005,13 @@ async function main() {
|
|
|
2567
3005
|
}
|
|
2568
3006
|
} finally {
|
|
2569
3007
|
await reportUpdateNudges(updateCheck, updateLog);
|
|
3008
|
+
await flushSentry();
|
|
2570
3009
|
}
|
|
2571
3010
|
}
|
|
2572
|
-
main().catch((err) => {
|
|
3011
|
+
main().catch(async (err) => {
|
|
3012
|
+
Sentry2.captureException(err);
|
|
2573
3013
|
const log = createLogger();
|
|
2574
3014
|
log.error(err instanceof Error ? err.message : String(err));
|
|
3015
|
+
await flushSentry();
|
|
2575
3016
|
process.exitCode = 1;
|
|
2576
3017
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genex-ai/cli-demo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.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": {
|
|
@@ -35,6 +35,9 @@
|
|
|
35
35
|
"publishConfig": {
|
|
36
36
|
"access": "public"
|
|
37
37
|
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@sentry/node": "^10.63.0"
|
|
40
|
+
},
|
|
38
41
|
"devDependencies": {
|
|
39
42
|
"@dimforge/rapier3d-compat": "^0.19.3",
|
|
40
43
|
"@pixiv/three-vrm": "^3.5.4",
|