@genex-ai/cli-demo 1.2.4-dev.363 → 1.3.0-dev.372
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 +344 -179
- package/package.json +1 -1
- package/templates/controllers/chat/chat-overlay.ts +305 -0
- package/templates/controllers/voice/voice-proximity.ts +247 -0
- package/templates/skills/genex-game-director/references/routing-map.md +2 -0
- package/templates/skills/genex-threejs-multiplayer/SKILL.md +29 -4
- package/templates/skills/genex-threejs-multiplayer/references/realtime-patterns.md +44 -2
package/dist/index.js
CHANGED
|
@@ -991,7 +991,7 @@ var CONTRACT_END = "<!-- genex:contract:end -->";
|
|
|
991
991
|
var GENEX_CONTRACT_BLOCK = `${CONTRACT_BEGIN}
|
|
992
992
|
# Genex build contract (always in effect for this game)
|
|
993
993
|
|
|
994
|
-
Your capabilities (all via \`npx genex \u2026\`): generate \`model\` \xB7 \`skybox\` \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 \`texture\` \xB7 \`image\` (\`--edit\` \xB7 \`--inpaint\` \xB7 \`--glass\` \xB7 \`--clean\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 rigged \`character\` / \`creature\` \xB7 \`character animate <id> "<verb>"\` (also \`creature animate\`; \`--locomotion\` for the 8-way movement set, \`--video\` for your own footage) \xB7 the pixel toolbox \`ui extract|masks|plate|text-color|trim|audit\` \xB7 vendored \`controller character|car|drone|touch|quality\` \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`preview\` / \`publish
|
|
994
|
+
Your capabilities (all via \`npx genex \u2026\`): generate \`model\` \xB7 \`skybox\` \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 \`texture\` \xB7 \`image\` (\`--edit\` \xB7 \`--inpaint\` \xB7 \`--glass\` \xB7 \`--clean\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 rigged \`character\` / \`creature\` \xB7 \`character animate <id> "<verb>"\` (also \`creature animate\`; \`--locomotion\` for the 8-way movement set, \`--video\` for your own footage) \xB7 the pixel toolbox \`ui extract|masks|plate|text-color|trim|audit\` \xB7 vendored \`controller character|car|drone|touch|quality|chat\` \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`preview\` / \`publish\` \xB7 \`rename <name>\` (move the game to a new address \u2014 keeps its plays and likes, kills the old link, needs \`--yes\` once published, and the game must be rebuilt after). Full options: \`npx genex --help\`. Task\u2192lane routing lives in the \`genex-game-director\` skill's routing map \u2014 re-load it whenever you're unsure which lane owns a task.
|
|
995
995
|
|
|
996
996
|
1. Load the \`genex-game-director\` skill before starting the requested work. Route from the player's latest clear request: a focused request starts directly without replaying discovery or commissioning unrelated lanes. After ANY context compaction or session resume, re-read this file and \`DESIGN.md\`, re-load the skill for the stage you are executing, and continue from the Build plan's \`Now:\` line \u2014 never from memory alone.
|
|
997
997
|
2. Ask only when one unresolved answer materially changes the work. If the request is clear, do not repeat an interview, confirm the pitch, force a concept round, or ask whole-game-vs-one-part again. For a genuinely broad new game, ask one decision: whole coordinated build or one request-relevant part first. If there is no clear request \u2014 a setup prompt pasted with nothing of their own \u2014 ask in PLAIN CHAT what they want to make, in their own words, and never fill the blank by pitching concepts. That opening question stays in chat on purpose: their reply carries the whole request, including anything they say about HOW you should work, and a menu of options answers a narrower question than the one they need to answer. Once you know what you are building, ask one decision at a time with your built-in question tool with clickable answer options when you have one; use a short plain-chat question otherwise. If the player stays silent after a necessary question, proceed on stated assumptions where reasonable and record each as "assumed \u2014 player didn't answer" in DESIGN.md \u2192 Decisions.
|
|
@@ -1891,6 +1891,124 @@ async function listOwnSlugs(apiUrl, token, log) {
|
|
|
1891
1891
|
}
|
|
1892
1892
|
}
|
|
1893
1893
|
|
|
1894
|
+
// src/commands/rename.ts
|
|
1895
|
+
import fs12 from "fs/promises";
|
|
1896
|
+
import path12 from "path";
|
|
1897
|
+
async function runRename(opts) {
|
|
1898
|
+
const log = createLogger({ quiet: opts.quiet });
|
|
1899
|
+
log.plain(c.bold("genex rename"));
|
|
1900
|
+
log.plain("");
|
|
1901
|
+
const name = opts.name?.trim();
|
|
1902
|
+
if (!name) {
|
|
1903
|
+
log.error("Rename it to what? Usage: genex rename <name>");
|
|
1904
|
+
process.exitCode = 1;
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1907
|
+
const meta = await readProject();
|
|
1908
|
+
if (!meta) {
|
|
1909
|
+
log.error("No genex project here. Run `genex init` in this directory first.");
|
|
1910
|
+
process.exitCode = 1;
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
const token = await readUserToken(opts.envPath);
|
|
1914
|
+
if (!token) {
|
|
1915
|
+
log.error("Not signed in. Run `genex auth` first.");
|
|
1916
|
+
process.exitCode = 1;
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
const apiUrl = getApiUrl(opts.apiUrl ?? meta.apiUrl);
|
|
1920
|
+
const from = meta.slug;
|
|
1921
|
+
if (meta.status === "published" && !opts.yes) {
|
|
1922
|
+
log.warn(`This retires ${c.cyan(`https://${from}.genex.technology/`)} \u2014 links already shared to it will stop working.`);
|
|
1923
|
+
log.dim(" Plays, likes, and comments move with the game. Re-run with --yes to go ahead.");
|
|
1924
|
+
process.exitCode = 1;
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
log.step(`Renaming ${c.cyan(from)}\u2026`);
|
|
1928
|
+
let res;
|
|
1929
|
+
try {
|
|
1930
|
+
res = await apiFetch(`${apiUrl}/api/projects/${meta.id}/rename`, {
|
|
1931
|
+
method: "POST",
|
|
1932
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
1933
|
+
body: JSON.stringify({ name })
|
|
1934
|
+
});
|
|
1935
|
+
} catch (err) {
|
|
1936
|
+
log.error(`Couldn't reach the API at ${apiUrl}.`);
|
|
1937
|
+
log.dim(` ${String(err)}`);
|
|
1938
|
+
process.exitCode = 1;
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
if (res.status === 409) {
|
|
1942
|
+
log.error("That name is taken \u2014 every game needs its own address. Try another.");
|
|
1943
|
+
process.exitCode = 1;
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
if (res.status === 404) {
|
|
1947
|
+
log.error("That project isn't yours (or no longer exists).");
|
|
1948
|
+
process.exitCode = 1;
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
if (!res.ok) {
|
|
1952
|
+
log.error(`Rename failed (HTTP ${res.status}). Your game is untouched at ${c.cyan(from)}.`);
|
|
1953
|
+
process.exitCode = 1;
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
const data = await res.json().catch(() => null);
|
|
1957
|
+
const project = data?.project;
|
|
1958
|
+
if (!project) {
|
|
1959
|
+
log.error("Unexpected API response \u2014 rename may not have applied. Check `genex list`.");
|
|
1960
|
+
process.exitCode = 1;
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
await writeProject({
|
|
1964
|
+
...meta,
|
|
1965
|
+
slug: project.slug,
|
|
1966
|
+
cloneUrl: project.cloneUrl ?? meta.cloneUrl,
|
|
1967
|
+
playUrl: project.playUrl ?? meta.playUrl
|
|
1968
|
+
});
|
|
1969
|
+
await rewriteSlugEnv(from, project.slug, log);
|
|
1970
|
+
await rewriteBakedSlug(from, project.slug, log);
|
|
1971
|
+
log.plain("");
|
|
1972
|
+
log.success(`Renamed to ${c.cyan(project.slug)}.`);
|
|
1973
|
+
if (project.playUrl) log.dim(` play: ${project.playUrl}`);
|
|
1974
|
+
if (meta.status === "published") {
|
|
1975
|
+
const dashboard = meta.dashboardOrigins?.[0];
|
|
1976
|
+
if (dashboard) log.dim(` world page: ${dashboard}/world/${project.slug}`);
|
|
1977
|
+
}
|
|
1978
|
+
log.plain("");
|
|
1979
|
+
log.info("Run `genex preview` (or `publish`) to rebuild \u2014 the new slug is baked into the bundle.");
|
|
1980
|
+
}
|
|
1981
|
+
async function rewriteSlugEnv(from, to, log, cwd = process.cwd()) {
|
|
1982
|
+
const file = path12.join(cwd, ".env");
|
|
1983
|
+
let content;
|
|
1984
|
+
try {
|
|
1985
|
+
content = await fs12.readFile(file, "utf8");
|
|
1986
|
+
} catch {
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1989
|
+
const re = /^(\s*VITE_GENEX_SLUG=)(.*)$/m;
|
|
1990
|
+
if (!re.test(content)) return;
|
|
1991
|
+
await fs12.writeFile(file, content.replace(re, `$1${to}`));
|
|
1992
|
+
log.dim(` .env: VITE_GENEX_SLUG=${to} (was ${from})`);
|
|
1993
|
+
}
|
|
1994
|
+
async function rewriteBakedSlug(from, to, log, cwd = process.cwd()) {
|
|
1995
|
+
const file = path12.join(cwd, "src", "genex.config.ts");
|
|
1996
|
+
let content;
|
|
1997
|
+
try {
|
|
1998
|
+
content = await fs12.readFile(file, "utf8");
|
|
1999
|
+
} catch {
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
const quoted = new RegExp(`(["'])${from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\1`, "g");
|
|
2003
|
+
const hits = content.match(quoted)?.length ?? 0;
|
|
2004
|
+
if (hits === 0) {
|
|
2005
|
+
log.warn(` src/genex.config.ts has no "${from}" literal \u2014 check its slug by hand.`);
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
await fs12.writeFile(file, content.replace(quoted, `"${to}"`));
|
|
2009
|
+
log.dim(` src/genex.config.ts: baked slug -> ${to}`);
|
|
2010
|
+
}
|
|
2011
|
+
|
|
1894
2012
|
// src/commands/list.ts
|
|
1895
2013
|
async function runList(opts) {
|
|
1896
2014
|
const log = createLogger({ quiet: opts.quiet });
|
|
@@ -2017,9 +2135,9 @@ function relTime(iso) {
|
|
|
2017
2135
|
// src/lib/deploy.ts
|
|
2018
2136
|
import { spawn as spawn3 } from "child_process";
|
|
2019
2137
|
import crypto3 from "crypto";
|
|
2020
|
-
import
|
|
2138
|
+
import fs15 from "fs/promises";
|
|
2021
2139
|
import os5 from "os";
|
|
2022
|
-
import
|
|
2140
|
+
import path14 from "path";
|
|
2023
2141
|
|
|
2024
2142
|
// ../../packages/mobile-scan/src/image-dims.ts
|
|
2025
2143
|
function u32be(b, o) {
|
|
@@ -2189,7 +2307,8 @@ function scanBundle(files) {
|
|
|
2189
2307
|
ktx2Loader: text.includes("KTX2Loader") || text.includes(".ktx2"),
|
|
2190
2308
|
touchHints: /joystick/i.test(text) || text.includes("touchstart") && text.includes("touch-action"),
|
|
2191
2309
|
adaptiveQuality: text.includes("__GENEX_QUALITY__"),
|
|
2192
|
-
modelRungs: text.includes("__GENEX_MODEL_RUNGS__")
|
|
2310
|
+
modelRungs: text.includes("__GENEX_MODEL_RUNGS__"),
|
|
2311
|
+
voice: text.includes("__GENEX_VOICE__")
|
|
2193
2312
|
};
|
|
2194
2313
|
return {
|
|
2195
2314
|
images,
|
|
@@ -2282,12 +2401,12 @@ function tierFor(estVramMb) {
|
|
|
2282
2401
|
}
|
|
2283
2402
|
|
|
2284
2403
|
// src/commands/ui.ts
|
|
2285
|
-
import
|
|
2286
|
-
import
|
|
2404
|
+
import fs14 from "fs/promises";
|
|
2405
|
+
import path13 from "path";
|
|
2287
2406
|
import { PNG as PNG2 } from "pngjs";
|
|
2288
2407
|
|
|
2289
2408
|
// src/lib/png-tools.ts
|
|
2290
|
-
import
|
|
2409
|
+
import fs13 from "fs/promises";
|
|
2291
2410
|
import { PNG } from "pngjs";
|
|
2292
2411
|
var ALPHA_TRANSPARENT_MAX = 16;
|
|
2293
2412
|
var isHttpUrl = (s) => /^https?:\/\//i.test(s);
|
|
@@ -2298,12 +2417,12 @@ async function loadPng(input) {
|
|
|
2298
2417
|
if (!res.ok) throw new Error(`Couldn't fetch ${input} (HTTP ${res.status}).`);
|
|
2299
2418
|
buf = Buffer.from(await res.arrayBuffer());
|
|
2300
2419
|
} else {
|
|
2301
|
-
buf = await
|
|
2420
|
+
buf = await fs13.readFile(input);
|
|
2302
2421
|
}
|
|
2303
2422
|
return PNG.sync.read(buf);
|
|
2304
2423
|
}
|
|
2305
2424
|
async function writePng(file, png) {
|
|
2306
|
-
await
|
|
2425
|
+
await fs13.writeFile(file, PNG.sync.write(png));
|
|
2307
2426
|
}
|
|
2308
2427
|
function cropPng(image, box) {
|
|
2309
2428
|
const out = new PNG({ width: box.w, height: box.h });
|
|
@@ -2603,7 +2722,7 @@ async function uiExtract(opts, log) {
|
|
|
2603
2722
|
const dilatePx = opts.dilate ?? 0;
|
|
2604
2723
|
const sheet = await loadPng(input);
|
|
2605
2724
|
const { width: W, height: H, data } = sheet;
|
|
2606
|
-
await
|
|
2725
|
+
await fs14.mkdir(outDir, { recursive: true });
|
|
2607
2726
|
log.plain(c.bold("genex ui extract"));
|
|
2608
2727
|
log.dim(` ${input} (${W}x${H}) \u2192 ${outDir}, ${names.length} names`);
|
|
2609
2728
|
let hasTransparency = false;
|
|
@@ -2832,7 +2951,7 @@ async function uiExtract(opts, log) {
|
|
|
2832
2951
|
rimPixels: speckle.sampled
|
|
2833
2952
|
});
|
|
2834
2953
|
}
|
|
2835
|
-
const outPath =
|
|
2954
|
+
const outPath = path13.join(outDir, `${name}.png`);
|
|
2836
2955
|
await writePng(outPath, out);
|
|
2837
2956
|
const sidecar = {
|
|
2838
2957
|
name,
|
|
@@ -2851,7 +2970,7 @@ async function uiExtract(opts, log) {
|
|
|
2851
2970
|
defringed
|
|
2852
2971
|
};
|
|
2853
2972
|
const { name: _n, out: _o, ...sidecarBody } = sidecar;
|
|
2854
|
-
await
|
|
2973
|
+
await fs14.writeFile(
|
|
2855
2974
|
outPath.replace(/\.png$/i, "") + ".bbox.json",
|
|
2856
2975
|
JSON.stringify(sidecarBody, null, 2)
|
|
2857
2976
|
);
|
|
@@ -2860,8 +2979,8 @@ async function uiExtract(opts, log) {
|
|
|
2860
2979
|
`${name}.png ${cropW}x${cropH} (ar ${sidecar.aspectRatio}) at sheet ${padX0},${padY0}`
|
|
2861
2980
|
);
|
|
2862
2981
|
}
|
|
2863
|
-
const debugPath =
|
|
2864
|
-
await
|
|
2982
|
+
const debugPath = path13.join(outDir, "extract-debug.json");
|
|
2983
|
+
await fs14.writeFile(
|
|
2865
2984
|
debugPath,
|
|
2866
2985
|
JSON.stringify(
|
|
2867
2986
|
{
|
|
@@ -3323,7 +3442,7 @@ async function uiMasks(opts, log) {
|
|
|
3323
3442
|
const registrationTolerance = opts.registrationTolerance ?? 0.04;
|
|
3324
3443
|
const edgeFlushMax = opts.edgeFlushMax ?? 0.04;
|
|
3325
3444
|
const loosened = registrationTolerance > 0.04 || edgeFlushMax > 0.04 || minCoverage < 0.01 || maxCoverage > 0.85;
|
|
3326
|
-
await
|
|
3445
|
+
await fs14.mkdir(outDir, { recursive: true });
|
|
3327
3446
|
log.plain(c.bold("genex ui masks"));
|
|
3328
3447
|
log.dim(` ${input} (${image.width}x${image.height}), ${pairs.length} pair(s) \u2192 ${outDir}`);
|
|
3329
3448
|
const sheetComponents = detectSheetComponents(image, opts.minPixels ?? 2e3);
|
|
@@ -3376,11 +3495,11 @@ async function uiMasks(opts, log) {
|
|
|
3376
3495
|
});
|
|
3377
3496
|
}
|
|
3378
3497
|
const overlay = makeOverlay(clean2, converted.png);
|
|
3379
|
-
const framePath =
|
|
3380
|
-
const maskPath =
|
|
3381
|
-
const annotatedPath =
|
|
3382
|
-
const overlayPath =
|
|
3383
|
-
const metaPath =
|
|
3498
|
+
const framePath = path13.join(outDir, `${pair.name}-frame.png`);
|
|
3499
|
+
const maskPath = path13.join(outDir, `${pair.name}-mask.png`);
|
|
3500
|
+
const annotatedPath = path13.join(outDir, `${pair.name}-annotated-source.png`);
|
|
3501
|
+
const overlayPath = path13.join(outDir, `${pair.name}-overlay.png`);
|
|
3502
|
+
const metaPath = path13.join(outDir, `${pair.name}.annotated-progress.json`);
|
|
3384
3503
|
await writePng(framePath, clean2);
|
|
3385
3504
|
await writePng(maskPath, converted.png);
|
|
3386
3505
|
await writePng(annotatedPath, annotated);
|
|
@@ -3427,7 +3546,7 @@ async function uiMasks(opts, log) {
|
|
|
3427
3546
|
},
|
|
3428
3547
|
overlay: overlayPath
|
|
3429
3548
|
};
|
|
3430
|
-
await
|
|
3549
|
+
await fs14.writeFile(metaPath, `${JSON.stringify(meta, null, 2)}
|
|
3431
3550
|
`);
|
|
3432
3551
|
results.push(meta);
|
|
3433
3552
|
const fb = converted.bbox;
|
|
@@ -3443,8 +3562,8 @@ async function uiMasks(opts, log) {
|
|
|
3443
3562
|
);
|
|
3444
3563
|
}
|
|
3445
3564
|
}
|
|
3446
|
-
const indexPath =
|
|
3447
|
-
await
|
|
3565
|
+
const indexPath = path13.join(outDir, "annotated-progress.json");
|
|
3566
|
+
await fs14.writeFile(indexPath, `${JSON.stringify({ input, loosened, pairs: results }, null, 2)}
|
|
3448
3567
|
`);
|
|
3449
3568
|
log.plain("");
|
|
3450
3569
|
log.success(`Wrote ${pairs.length} frame+mask pair(s) \u2192 ${outDir} (index: ${indexPath}).`);
|
|
@@ -3570,7 +3689,7 @@ async function uiTextColor(opts, log) {
|
|
|
3570
3689
|
};
|
|
3571
3690
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
3572
3691
|
`);
|
|
3573
|
-
if (opts.out) await
|
|
3692
|
+
if (opts.out) await fs14.writeFile(opts.out, `${JSON.stringify(result, null, 2)}
|
|
3574
3693
|
`);
|
|
3575
3694
|
if (opts.cropPath) log.dim(` crop \u2192 ${opts.cropPath}`);
|
|
3576
3695
|
}
|
|
@@ -3602,7 +3721,7 @@ async function uiTrim(opts, log) {
|
|
|
3602
3721
|
const sidecar = computeBBoxes(trimmed);
|
|
3603
3722
|
const speckleAllowed = !!(speckle && speckle.ratio > SPECKLE_MAX_RATIO);
|
|
3604
3723
|
const sidecarPath = outPath.replace(/\.png$/i, "") + ".bbox.json";
|
|
3605
|
-
await
|
|
3724
|
+
await fs14.writeFile(
|
|
3606
3725
|
sidecarPath,
|
|
3607
3726
|
JSON.stringify(speckleAllowed ? { ...sidecar, speckleAllowed: true } : sidecar, null, 2)
|
|
3608
3727
|
);
|
|
@@ -3709,7 +3828,7 @@ async function uiPlate(opts, log) {
|
|
|
3709
3828
|
fail("No interior found \u2014 the image is fully transparent (or erode ate everything). Check --in / lower --erode.");
|
|
3710
3829
|
}
|
|
3711
3830
|
await writePng(outPath, out);
|
|
3712
|
-
const name =
|
|
3831
|
+
const name = path13.basename(outPath);
|
|
3713
3832
|
log.plain(c.bold("genex ui plate"));
|
|
3714
3833
|
log.success(`${outPath} ${W}x${H}, interior ${(count / (W * H) * 100).toFixed(1)}% (erode ${erode}px)`);
|
|
3715
3834
|
log.dim(" Wire it as the plate's silhouette (same box as the frame <img>, plate UNDER the art):");
|
|
@@ -3874,13 +3993,13 @@ async function walkFiles(dir) {
|
|
|
3874
3993
|
const out = [];
|
|
3875
3994
|
let entries;
|
|
3876
3995
|
try {
|
|
3877
|
-
entries = await
|
|
3996
|
+
entries = await fs14.readdir(dir, { withFileTypes: true });
|
|
3878
3997
|
} catch {
|
|
3879
3998
|
return out;
|
|
3880
3999
|
}
|
|
3881
4000
|
for (const entry of entries) {
|
|
3882
4001
|
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
3883
|
-
const p =
|
|
4002
|
+
const p = path13.join(dir, entry.name);
|
|
3884
4003
|
if (entry.isDirectory()) out.push(...await walkFiles(p));
|
|
3885
4004
|
else out.push(p);
|
|
3886
4005
|
}
|
|
@@ -3889,7 +4008,7 @@ async function walkFiles(dir) {
|
|
|
3889
4008
|
async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
3890
4009
|
const viewportFindings = [];
|
|
3891
4010
|
try {
|
|
3892
|
-
const indexHtml = await
|
|
4011
|
+
const indexHtml = await fs14.readFile(path13.join(cwd, "index.html"), "utf8");
|
|
3893
4012
|
if (!/<meta[^>]+name=["']viewport["']/i.test(indexHtml)) {
|
|
3894
4013
|
viewportFindings.push({
|
|
3895
4014
|
kind: "viewport-meta",
|
|
@@ -3903,28 +4022,28 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
3903
4022
|
}
|
|
3904
4023
|
} catch {
|
|
3905
4024
|
}
|
|
3906
|
-
const absAssets =
|
|
4025
|
+
const absAssets = path13.resolve(cwd, assetDir);
|
|
3907
4026
|
try {
|
|
3908
|
-
if (!(await
|
|
4027
|
+
if (!(await fs14.stat(absAssets)).isDirectory()) {
|
|
3909
4028
|
return viewportFindings.length > 0 ? viewportFindings : null;
|
|
3910
4029
|
}
|
|
3911
4030
|
} catch {
|
|
3912
4031
|
return viewportFindings.length > 0 ? viewportFindings : null;
|
|
3913
4032
|
}
|
|
3914
|
-
const srcFiles = (await walkFiles(
|
|
3915
|
-
(p) => AUDIT_SRC_EXTS.has(
|
|
4033
|
+
const srcFiles = (await walkFiles(path13.resolve(cwd, srcDir))).filter(
|
|
4034
|
+
(p) => AUDIT_SRC_EXTS.has(path13.extname(p).toLowerCase())
|
|
3916
4035
|
);
|
|
3917
4036
|
try {
|
|
3918
|
-
for (const name of await
|
|
3919
|
-
const ext =
|
|
3920
|
-
if (ext === ".html" || ext === ".css") srcFiles.push(
|
|
4037
|
+
for (const name of await fs14.readdir(cwd)) {
|
|
4038
|
+
const ext = path13.extname(name).toLowerCase();
|
|
4039
|
+
if (ext === ".html" || ext === ".css") srcFiles.push(path13.join(cwd, name));
|
|
3921
4040
|
}
|
|
3922
4041
|
} catch {
|
|
3923
4042
|
}
|
|
3924
4043
|
const sources = [];
|
|
3925
4044
|
for (const p of srcFiles) {
|
|
3926
4045
|
try {
|
|
3927
|
-
sources.push({ rel:
|
|
4046
|
+
sources.push({ rel: path13.relative(cwd, p), text: await fs14.readFile(p, "utf8") });
|
|
3928
4047
|
} catch {
|
|
3929
4048
|
}
|
|
3930
4049
|
}
|
|
@@ -3934,11 +4053,11 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
3934
4053
|
const metaByName = /* @__PURE__ */ new Map();
|
|
3935
4054
|
const bboxByPng = /* @__PURE__ */ new Map();
|
|
3936
4055
|
for (const p of assetFiles) {
|
|
3937
|
-
const base =
|
|
4056
|
+
const base = path13.basename(p);
|
|
3938
4057
|
const metaMatch = /^(.+)\.annotated-progress\.json$/.exec(base);
|
|
3939
4058
|
if (metaMatch) {
|
|
3940
4059
|
try {
|
|
3941
|
-
const meta = JSON.parse(await
|
|
4060
|
+
const meta = JSON.parse(await fs14.readFile(p, "utf8"));
|
|
3942
4061
|
metaByName.set(metaMatch[1], {
|
|
3943
4062
|
cleanCrop: meta.clean?.crop ?? null,
|
|
3944
4063
|
loosened: meta.loosened === true
|
|
@@ -3949,7 +4068,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
3949
4068
|
}
|
|
3950
4069
|
if (base.endsWith(".bbox.json")) {
|
|
3951
4070
|
try {
|
|
3952
|
-
const sidecar = JSON.parse(await
|
|
4071
|
+
const sidecar = JSON.parse(await fs14.readFile(p, "utf8"));
|
|
3953
4072
|
if (sidecar.sheetBBox) bboxByPng.set(base.replace(/\.bbox\.json$/, ".png"), sidecar.sheetBBox);
|
|
3954
4073
|
} catch {
|
|
3955
4074
|
}
|
|
@@ -3958,7 +4077,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
3958
4077
|
for (const [name, meta] of metaByName) {
|
|
3959
4078
|
if (!meta.cleanCrop || !referenced(`${name}-mask.png`)) continue;
|
|
3960
4079
|
for (const p of assetFiles) {
|
|
3961
|
-
const base =
|
|
4080
|
+
const base = path13.basename(p);
|
|
3962
4081
|
if (!base.toLowerCase().endsWith(".png")) continue;
|
|
3963
4082
|
if (base !== `${name}.png` && !base.startsWith(`${name}-`)) continue;
|
|
3964
4083
|
if (/-mask\.png$|-frame\.png$|-overlay\.png$|-annotated-source\.png$/.test(base)) continue;
|
|
@@ -3982,7 +4101,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
3982
4101
|
}
|
|
3983
4102
|
const pngByBase = /* @__PURE__ */ new Map();
|
|
3984
4103
|
for (const p of assetFiles) {
|
|
3985
|
-
const base =
|
|
4104
|
+
const base = path13.basename(p);
|
|
3986
4105
|
if (base.toLowerCase().endsWith(".png")) pngByBase.set(base, p);
|
|
3987
4106
|
}
|
|
3988
4107
|
for (const [maskBase, maskPath] of pngByBase) {
|
|
@@ -3995,8 +4114,8 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
3995
4114
|
let frame;
|
|
3996
4115
|
let mask;
|
|
3997
4116
|
try {
|
|
3998
|
-
frame = PNG2.sync.read(await
|
|
3999
|
-
mask = PNG2.sync.read(await
|
|
4117
|
+
frame = PNG2.sync.read(await fs14.readFile(framePath));
|
|
4118
|
+
mask = PNG2.sync.read(await fs14.readFile(maskPath));
|
|
4000
4119
|
} catch {
|
|
4001
4120
|
continue;
|
|
4002
4121
|
}
|
|
@@ -4015,7 +4134,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4015
4134
|
if (!referenced(base)) continue;
|
|
4016
4135
|
let png;
|
|
4017
4136
|
try {
|
|
4018
|
-
png = PNG2.sync.read(await
|
|
4137
|
+
png = PNG2.sync.read(await fs14.readFile(p));
|
|
4019
4138
|
} catch {
|
|
4020
4139
|
continue;
|
|
4021
4140
|
}
|
|
@@ -4035,7 +4154,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4035
4154
|
}
|
|
4036
4155
|
const maskReported = /* @__PURE__ */ new Set();
|
|
4037
4156
|
for (const p of assetFiles) {
|
|
4038
|
-
const m = /^(.+)\.annotated-progress\.json$/.exec(
|
|
4157
|
+
const m = /^(.+)\.annotated-progress\.json$/.exec(path13.basename(p));
|
|
4039
4158
|
if (!m) continue;
|
|
4040
4159
|
const maskBase = `${m[1]}-mask.png`;
|
|
4041
4160
|
if (!referenced(maskBase)) {
|
|
@@ -4047,14 +4166,14 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4047
4166
|
}
|
|
4048
4167
|
}
|
|
4049
4168
|
for (const p of assetFiles) {
|
|
4050
|
-
const base =
|
|
4169
|
+
const base = path13.basename(p);
|
|
4051
4170
|
if (!base.toLowerCase().endsWith(".png")) continue;
|
|
4052
4171
|
if (/-annotated-source\.png$|-overlay\.png$/.test(base)) continue;
|
|
4053
4172
|
if (maskReported.has(base)) continue;
|
|
4054
4173
|
if (!referenced(base)) {
|
|
4055
4174
|
findings.push({
|
|
4056
4175
|
kind: "unwired-sprite",
|
|
4057
|
-
message: `${
|
|
4176
|
+
message: `${path13.relative(cwd, p)} is on disk but never referenced in ${srcDir}/index.html/CSS \u2014 wire it, or record the one-line reason it was cut. (Computed-string references are invisible here \u2014 check.)`
|
|
4058
4177
|
});
|
|
4059
4178
|
}
|
|
4060
4179
|
}
|
|
@@ -4188,7 +4307,7 @@ async function printUiAuditPreflight(log) {
|
|
|
4188
4307
|
}
|
|
4189
4308
|
async function printPipelineStatePreflight(log, cwd = process.cwd()) {
|
|
4190
4309
|
try {
|
|
4191
|
-
const design = await
|
|
4310
|
+
const design = await fs15.readFile(path14.join(cwd, "DESIGN.md"), "utf8").catch(() => "");
|
|
4192
4311
|
const warnings = [];
|
|
4193
4312
|
if (design.length > 0 && !/##\s*build plan & status/i.test(design)) {
|
|
4194
4313
|
warnings.push(
|
|
@@ -4196,7 +4315,7 @@ async function printPipelineStatePreflight(log, cwd = process.cwd()) {
|
|
|
4196
4315
|
);
|
|
4197
4316
|
}
|
|
4198
4317
|
if (!/player character:/i.test(design)) {
|
|
4199
|
-
const hasCharacter = await
|
|
4318
|
+
const hasCharacter = await fs15.access(path14.join(cwd, "public", "assets", "meshy-character.json")).then(() => true, () => false);
|
|
4200
4319
|
if (!hasCharacter && await loadsPlayerBody(cwd)) {
|
|
4201
4320
|
warnings.push(
|
|
4202
4321
|
`Player character is the stock avatar \u2014 no generated character is wired. The game's own generated character is the player's body wherever a human body appears on screen, first-person included (genex-ai-character). Generate it, or record "Player character: VRM \u2014 <reason>" in DESIGN.md (no human body in this game / out of credits / player declined).`
|
|
@@ -4210,12 +4329,12 @@ async function printPipelineStatePreflight(log, cwd = process.cwd()) {
|
|
|
4210
4329
|
async function loadsPlayerBody(cwd) {
|
|
4211
4330
|
const BODY_LOADERS = /\bloadPlayerCharacter\s*\(|\bloadVrm(?:Clone)?\s*\(/;
|
|
4212
4331
|
try {
|
|
4213
|
-
const entries = await
|
|
4332
|
+
const entries = await fs15.readdir(path14.join(cwd, "src"), { recursive: true });
|
|
4214
4333
|
for (const rel of entries) {
|
|
4215
4334
|
if (rel.includes("node_modules")) continue;
|
|
4216
|
-
if (rel.split(
|
|
4335
|
+
if (rel.split(path14.sep)[0] === "controllers" || rel.startsWith("controllers/")) continue;
|
|
4217
4336
|
if (!/\.(ts|tsx|js|jsx)$/.test(rel)) continue;
|
|
4218
|
-
const text = await
|
|
4337
|
+
const text = await fs15.readFile(path14.join(cwd, "src", rel), "utf8").catch(() => "");
|
|
4219
4338
|
if (BODY_LOADERS.test(text)) return true;
|
|
4220
4339
|
}
|
|
4221
4340
|
} catch {
|
|
@@ -4239,9 +4358,9 @@ async function deployGame(ctx, opts, log) {
|
|
|
4239
4358
|
}
|
|
4240
4359
|
log.success("Built.");
|
|
4241
4360
|
}
|
|
4242
|
-
const distDir =
|
|
4361
|
+
const distDir = path14.join(cwd, "dist");
|
|
4243
4362
|
const siteDir = await isDir2(distDir) ? distDir : cwd;
|
|
4244
|
-
const rel =
|
|
4363
|
+
const rel = path14.relative(cwd, siteDir) || ".";
|
|
4245
4364
|
if (siteDir === cwd) await writeGitignore(cwd, log);
|
|
4246
4365
|
const files = await collectFiles(siteDir);
|
|
4247
4366
|
if (files.length === 0) {
|
|
@@ -4308,7 +4427,7 @@ async function deployGame(ctx, opts, log) {
|
|
|
4308
4427
|
}
|
|
4309
4428
|
async function hasBuildScript(cwd) {
|
|
4310
4429
|
try {
|
|
4311
|
-
const pkg = JSON.parse(await
|
|
4430
|
+
const pkg = JSON.parse(await fs15.readFile(path14.join(cwd, "package.json"), "utf8"));
|
|
4312
4431
|
return Boolean(pkg.scripts?.build);
|
|
4313
4432
|
} catch {
|
|
4314
4433
|
return false;
|
|
@@ -4317,12 +4436,12 @@ async function hasBuildScript(cwd) {
|
|
|
4317
4436
|
async function collectFiles(root) {
|
|
4318
4437
|
const out = [];
|
|
4319
4438
|
const walk2 = async (dir, prefix) => {
|
|
4320
|
-
for (const e of await
|
|
4439
|
+
for (const e of await fs15.readdir(dir, { withFileTypes: true })) {
|
|
4321
4440
|
const relPath = prefix ? `${prefix}/${e.name}` : e.name;
|
|
4322
4441
|
if (e.isDirectory()) {
|
|
4323
|
-
if (!EXCLUDE_DIRS.has(e.name)) await walk2(
|
|
4442
|
+
if (!EXCLUDE_DIRS.has(e.name)) await walk2(path14.join(dir, e.name), relPath);
|
|
4324
4443
|
} else if (e.isFile() && !isSecretEnvFile(e.name)) {
|
|
4325
|
-
out.push({ relPath, bytes: await
|
|
4444
|
+
out.push({ relPath, bytes: await fs15.readFile(path14.join(dir, e.name)) });
|
|
4326
4445
|
}
|
|
4327
4446
|
}
|
|
4328
4447
|
};
|
|
@@ -4560,7 +4679,7 @@ async function pushWorktree(cwd, pushUrl, managed, log, branch = "main") {
|
|
|
4560
4679
|
log.error("Couldn't save your game's source \u2014 please try again.");
|
|
4561
4680
|
return false;
|
|
4562
4681
|
};
|
|
4563
|
-
const gitDir = await
|
|
4682
|
+
const gitDir = await fs15.mkdtemp(path14.join(os5.tmpdir(), "genex-source-"));
|
|
4564
4683
|
const base = { GIT_DIR: gitDir };
|
|
4565
4684
|
if (urlHasEmbeddedCredentials(pushUrl)) {
|
|
4566
4685
|
base.GIT_CONFIG_COUNT = "1";
|
|
@@ -4575,12 +4694,12 @@ async function pushWorktree(cwd, pushUrl, managed, log, branch = "main") {
|
|
|
4575
4694
|
};
|
|
4576
4695
|
try {
|
|
4577
4696
|
if ((await run("git", ["init", "-q"], base)).code !== 0) return failed();
|
|
4578
|
-
await
|
|
4579
|
-
|
|
4697
|
+
await fs15.writeFile(
|
|
4698
|
+
path14.join(gitDir, "info", "exclude"),
|
|
4580
4699
|
// .env* are secrets — never publish them; `!` keeps the non-secret template.
|
|
4581
4700
|
["node_modules/", "dist/", ".git/", ".genex/", ".env", ".env.*", "!.env.example", ""].join("\n")
|
|
4582
4701
|
);
|
|
4583
|
-
const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE:
|
|
4702
|
+
const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE: path14.join(gitDir, "index-source") };
|
|
4584
4703
|
let lfs = (await run("git", ["lfs", "version"], base)).code !== 0 ? false : true;
|
|
4585
4704
|
if (!lfs) {
|
|
4586
4705
|
log.step("Installing git-lfs (keeps large binary assets out of the source push)\u2026");
|
|
@@ -4631,7 +4750,7 @@ async function pushWorktree(cwd, pushUrl, managed, log, branch = "main") {
|
|
|
4631
4750
|
} catch {
|
|
4632
4751
|
return failed();
|
|
4633
4752
|
} finally {
|
|
4634
|
-
await
|
|
4753
|
+
await fs15.rm(gitDir, { recursive: true, force: true }).catch(() => {
|
|
4635
4754
|
});
|
|
4636
4755
|
}
|
|
4637
4756
|
}
|
|
@@ -4667,7 +4786,7 @@ async function fetchPushUrl(ctx, log) {
|
|
|
4667
4786
|
}
|
|
4668
4787
|
async function isDir2(p) {
|
|
4669
4788
|
try {
|
|
4670
|
-
return (await
|
|
4789
|
+
return (await fs15.stat(p)).isDirectory();
|
|
4671
4790
|
} catch {
|
|
4672
4791
|
return false;
|
|
4673
4792
|
}
|
|
@@ -4802,17 +4921,17 @@ async function promoteBuild(apiUrl, projectId, token, log) {
|
|
|
4802
4921
|
}
|
|
4803
4922
|
|
|
4804
4923
|
// src/lib/detect-features.ts
|
|
4805
|
-
import
|
|
4806
|
-
import
|
|
4924
|
+
import fs17 from "fs/promises";
|
|
4925
|
+
import path16 from "path";
|
|
4807
4926
|
|
|
4808
4927
|
// src/lib/generation-ledger.ts
|
|
4809
|
-
import
|
|
4810
|
-
import
|
|
4811
|
-
var ledgerPath = (cwd) =>
|
|
4928
|
+
import fs16 from "fs/promises";
|
|
4929
|
+
import path15 from "path";
|
|
4930
|
+
var ledgerPath = (cwd) => path15.join(cwd, ".genex", "generations.ndjson");
|
|
4812
4931
|
async function append(cwd, event) {
|
|
4813
4932
|
try {
|
|
4814
|
-
await
|
|
4815
|
-
await
|
|
4933
|
+
await fs16.access(path15.join(cwd, ".genex"));
|
|
4934
|
+
await fs16.appendFile(ledgerPath(cwd), `${JSON.stringify(event)}
|
|
4816
4935
|
`, "utf8");
|
|
4817
4936
|
} catch {
|
|
4818
4937
|
}
|
|
@@ -4820,7 +4939,7 @@ async function append(cwd, event) {
|
|
|
4820
4939
|
async function readLedger(cwd = process.cwd()) {
|
|
4821
4940
|
let raw;
|
|
4822
4941
|
try {
|
|
4823
|
-
raw = await
|
|
4942
|
+
raw = await fs16.readFile(ledgerPath(cwd), "utf8");
|
|
4824
4943
|
} catch {
|
|
4825
4944
|
return [];
|
|
4826
4945
|
}
|
|
@@ -4874,7 +4993,7 @@ async function countFailed(kind, cwd = process.cwd()) {
|
|
|
4874
4993
|
// src/lib/detect-features.ts
|
|
4875
4994
|
async function detectEmbedSdkVersion(cwd = process.cwd()) {
|
|
4876
4995
|
try {
|
|
4877
|
-
const raw = await
|
|
4996
|
+
const raw = await fs17.readFile(path16.join(cwd, "package.json"), "utf8");
|
|
4878
4997
|
const pkg = JSON.parse(raw);
|
|
4879
4998
|
const version = pkg.dependencies?.["@genex-ai/embed-sdk"] ?? pkg.devDependencies?.["@genex-ai/embed-sdk"];
|
|
4880
4999
|
return typeof version === "string" && version ? version : null;
|
|
@@ -4884,7 +5003,7 @@ async function detectEmbedSdkVersion(cwd = process.cwd()) {
|
|
|
4884
5003
|
}
|
|
4885
5004
|
async function detectMultiplayer(cwd = process.cwd()) {
|
|
4886
5005
|
try {
|
|
4887
|
-
const raw = await
|
|
5006
|
+
const raw = await fs17.readFile(path16.join(cwd, "package.json"), "utf8");
|
|
4888
5007
|
const pkg = JSON.parse(raw);
|
|
4889
5008
|
return Boolean(
|
|
4890
5009
|
pkg.dependencies?.["@genex-ai/multiplayer"] ?? pkg.devDependencies?.["@genex-ai/multiplayer"]
|
|
@@ -4896,7 +5015,7 @@ async function detectMultiplayer(cwd = process.cwd()) {
|
|
|
4896
5015
|
async function detectMatchmaking(log, cwd = process.cwd()) {
|
|
4897
5016
|
let pkg;
|
|
4898
5017
|
try {
|
|
4899
|
-
pkg = JSON.parse(await
|
|
5018
|
+
pkg = JSON.parse(await fs17.readFile(path16.join(cwd, "package.json"), "utf8"));
|
|
4900
5019
|
} catch (err) {
|
|
4901
5020
|
log.dim(` (skipping matchmaking \u2014 couldn't read package.json: ${String(err)})`);
|
|
4902
5021
|
return null;
|
|
@@ -4914,15 +5033,15 @@ async function detectMatchmaking(log, cwd = process.cwd()) {
|
|
|
4914
5033
|
var TOUCH_KIT_MARKERS = /controllers\/touch\/|controllers\/character\/touch-joystick|TouchJoystick|VirtualButton|DragZone|RotateOverlay/;
|
|
4915
5034
|
async function detectMobileControls(cwd = process.cwd()) {
|
|
4916
5035
|
try {
|
|
4917
|
-
const raw = await
|
|
5036
|
+
const raw = await fs17.readFile(path16.join(cwd, "package.json"), "utf8");
|
|
4918
5037
|
const pkg = JSON.parse(raw);
|
|
4919
5038
|
if (pkg.genex?.mobileControls === true) return true;
|
|
4920
5039
|
} catch {
|
|
4921
5040
|
}
|
|
4922
|
-
const srcDir =
|
|
5041
|
+
const srcDir = path16.join(cwd, "src");
|
|
4923
5042
|
let entries;
|
|
4924
5043
|
try {
|
|
4925
|
-
entries = await
|
|
5044
|
+
entries = await fs17.readdir(srcDir, { recursive: true });
|
|
4926
5045
|
} catch {
|
|
4927
5046
|
return false;
|
|
4928
5047
|
}
|
|
@@ -4930,7 +5049,7 @@ async function detectMobileControls(cwd = process.cwd()) {
|
|
|
4930
5049
|
if (rel.includes("node_modules")) continue;
|
|
4931
5050
|
if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(rel)) continue;
|
|
4932
5051
|
try {
|
|
4933
|
-
const content = await
|
|
5052
|
+
const content = await fs17.readFile(path16.join(srcDir, rel), "utf8");
|
|
4934
5053
|
if (TOUCH_KIT_MARKERS.test(content)) return true;
|
|
4935
5054
|
} catch {
|
|
4936
5055
|
}
|
|
@@ -4939,10 +5058,10 @@ async function detectMobileControls(cwd = process.cwd()) {
|
|
|
4939
5058
|
}
|
|
4940
5059
|
var GAME_STATE_CALLS = /savePlayerState|saveWorldState|submitScore/;
|
|
4941
5060
|
async function detectGameStateUsage(cwd = process.cwd()) {
|
|
4942
|
-
const srcDir =
|
|
5061
|
+
const srcDir = path16.join(cwd, "src");
|
|
4943
5062
|
let entries;
|
|
4944
5063
|
try {
|
|
4945
|
-
entries = await
|
|
5064
|
+
entries = await fs17.readdir(srcDir, { recursive: true });
|
|
4946
5065
|
} catch {
|
|
4947
5066
|
return false;
|
|
4948
5067
|
}
|
|
@@ -4950,7 +5069,7 @@ async function detectGameStateUsage(cwd = process.cwd()) {
|
|
|
4950
5069
|
if (rel.includes("node_modules")) continue;
|
|
4951
5070
|
if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(rel)) continue;
|
|
4952
5071
|
try {
|
|
4953
|
-
const content = await
|
|
5072
|
+
const content = await fs17.readFile(path16.join(srcDir, rel), "utf8");
|
|
4954
5073
|
if (GAME_STATE_CALLS.test(content)) return true;
|
|
4955
5074
|
} catch {
|
|
4956
5075
|
}
|
|
@@ -4999,19 +5118,19 @@ async function detectSurfaceScan(cwd = process.cwd()) {
|
|
|
4999
5118
|
depthRange: [],
|
|
5000
5119
|
mediaElementAudio: []
|
|
5001
5120
|
};
|
|
5002
|
-
const srcDir =
|
|
5121
|
+
const srcDir = path16.join(cwd, "src");
|
|
5003
5122
|
let entries;
|
|
5004
5123
|
try {
|
|
5005
|
-
entries = await
|
|
5124
|
+
entries = await fs17.readdir(srcDir, { recursive: true });
|
|
5006
5125
|
} catch {
|
|
5007
5126
|
return found;
|
|
5008
5127
|
}
|
|
5009
5128
|
for (const nativeRel of entries) {
|
|
5010
5129
|
if (nativeRel.includes("node_modules")) continue;
|
|
5011
5130
|
if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(nativeRel)) continue;
|
|
5012
|
-
const raw = await
|
|
5131
|
+
const raw = await fs17.readFile(path16.join(srcDir, nativeRel), "utf8").catch(() => "");
|
|
5013
5132
|
if (!raw) continue;
|
|
5014
|
-
const rel = nativeRel.split(
|
|
5133
|
+
const rel = nativeRel.split(path16.sep).join("/");
|
|
5015
5134
|
const content = raw.replace(/\/\*[\s\S]*?\*\//g, (m2) => m2.replace(/[^\n]/g, " ")).replace(/(^|[^:])\/\/[^\n]*/gm, (m2, p1) => p1 + " ".repeat(m2.length - p1.length));
|
|
5016
5135
|
const repeatRe = /\.repeat\.set\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)/g;
|
|
5017
5136
|
let m;
|
|
@@ -5069,25 +5188,25 @@ async function detectGenerationAudit(cwd = process.cwd()) {
|
|
|
5069
5188
|
let haystack = "";
|
|
5070
5189
|
const read = async (file) => {
|
|
5071
5190
|
try {
|
|
5072
|
-
haystack += await
|
|
5191
|
+
haystack += await fs17.readFile(file, "utf8");
|
|
5073
5192
|
} catch {
|
|
5074
5193
|
}
|
|
5075
5194
|
};
|
|
5076
5195
|
try {
|
|
5077
|
-
for (const entry of await
|
|
5196
|
+
for (const entry of await fs17.readdir(cwd, { withFileTypes: true })) {
|
|
5078
5197
|
if (entry.isFile() && /\.(ts|tsx|js|jsx|css|html|json)$/.test(entry.name)) {
|
|
5079
|
-
await read(
|
|
5198
|
+
await read(path16.join(cwd, entry.name));
|
|
5080
5199
|
}
|
|
5081
5200
|
}
|
|
5082
5201
|
} catch {
|
|
5083
5202
|
}
|
|
5084
5203
|
for (const sub of ["src", "public"]) {
|
|
5085
5204
|
try {
|
|
5086
|
-
const entries = await
|
|
5205
|
+
const entries = await fs17.readdir(path16.join(cwd, sub), { recursive: true });
|
|
5087
5206
|
for (const rel of entries) {
|
|
5088
5207
|
if (rel.includes("node_modules")) continue;
|
|
5089
5208
|
if (!/\.(ts|tsx|js|jsx|css|html|json|txt)$/.test(rel)) continue;
|
|
5090
|
-
await read(
|
|
5209
|
+
await read(path16.join(cwd, sub, rel));
|
|
5091
5210
|
}
|
|
5092
5211
|
} catch {
|
|
5093
5212
|
}
|
|
@@ -5219,7 +5338,7 @@ async function borrowEvidence(meta, cwd) {
|
|
|
5219
5338
|
} catch {
|
|
5220
5339
|
return true;
|
|
5221
5340
|
}
|
|
5222
|
-
const gitConfig = await
|
|
5341
|
+
const gitConfig = await fs17.readFile(path16.join(cwd, ".git", "config"), "utf8").catch(() => "");
|
|
5223
5342
|
for (const m of gitConfig.matchAll(/url\s*=\s*(\S+)/g)) {
|
|
5224
5343
|
try {
|
|
5225
5344
|
const u = new URL(m[1]);
|
|
@@ -5227,7 +5346,7 @@ async function borrowEvidence(meta, cwd) {
|
|
|
5227
5346
|
} catch {
|
|
5228
5347
|
}
|
|
5229
5348
|
}
|
|
5230
|
-
const readme = await
|
|
5349
|
+
const readme = await fs17.readFile(path16.join(cwd, "README.md"), "utf8").catch(() => "");
|
|
5231
5350
|
return readme.includes(host) || /\b(upstream|originally by|ported from|borrowed from)\b/i.test(readme);
|
|
5232
5351
|
}
|
|
5233
5352
|
|
|
@@ -5477,8 +5596,8 @@ async function runPromote(opts) {
|
|
|
5477
5596
|
}
|
|
5478
5597
|
|
|
5479
5598
|
// src/commands/generate.ts
|
|
5480
|
-
import
|
|
5481
|
-
import
|
|
5599
|
+
import fs18 from "fs/promises";
|
|
5600
|
+
import path17 from "path";
|
|
5482
5601
|
import { PNG as PNG4 } from "pngjs";
|
|
5483
5602
|
|
|
5484
5603
|
// src/lib/glass.ts
|
|
@@ -5648,7 +5767,7 @@ var isRemoteRef = (value) => /^(https?:\/\/|data:)/i.test(value);
|
|
|
5648
5767
|
async function inlineLocalImage(filePath, flag) {
|
|
5649
5768
|
let bytes;
|
|
5650
5769
|
try {
|
|
5651
|
-
bytes = await
|
|
5770
|
+
bytes = await fs18.readFile(filePath);
|
|
5652
5771
|
} catch {
|
|
5653
5772
|
return { ok: false, error: `Couldn't read the ${flag} file at ${filePath}.` };
|
|
5654
5773
|
}
|
|
@@ -5658,7 +5777,7 @@ async function inlineLocalImage(filePath, flag) {
|
|
|
5658
5777
|
error: `${flag} file is ${(bytes.length / 1048576).toFixed(1)} MB \u2014 over the ~4 MB inline limit. Downscale/compress it first, or pass an asset URL instead.`
|
|
5659
5778
|
};
|
|
5660
5779
|
}
|
|
5661
|
-
const mime = IMAGE_MIME_BY_EXT[
|
|
5780
|
+
const mime = IMAGE_MIME_BY_EXT[path17.extname(filePath).toLowerCase()] ?? "image/png";
|
|
5662
5781
|
return { ok: true, dataUri: `data:${mime};base64,${bytes.toString("base64")}` };
|
|
5663
5782
|
}
|
|
5664
5783
|
var SKYBOX_ENVIRONMENT_SUFFIX = ". The image contains ONLY sky: cloud, atmosphere, light, weather and distant haze at the horizon. Every structure, object, plant and ground surface is outside the frame.";
|
|
@@ -5854,7 +5973,7 @@ async function runGenerate(kind, opts) {
|
|
|
5854
5973
|
return;
|
|
5855
5974
|
}
|
|
5856
5975
|
try {
|
|
5857
|
-
const bytes = await
|
|
5976
|
+
const bytes = await fs18.readFile(opts.inpaintUrl);
|
|
5858
5977
|
opts = { ...opts, inpaintUrl: `data:image/png;base64,${bytes.toString("base64")}` };
|
|
5859
5978
|
} catch {
|
|
5860
5979
|
log.error(`Couldn't read the --inpaint mask at ${opts.inpaintUrl}.`);
|
|
@@ -5978,7 +6097,7 @@ async function reportGlassTerminal(view, outDir, log, json) {
|
|
|
5978
6097
|
return;
|
|
5979
6098
|
}
|
|
5980
6099
|
await recordTerminal(view.id, "completed", files.map((f) => f.url));
|
|
5981
|
-
await
|
|
6100
|
+
await fs18.mkdir(outDir, { recursive: true });
|
|
5982
6101
|
const solved = [];
|
|
5983
6102
|
for (let i = 0; i < files.length; i++) {
|
|
5984
6103
|
const f = files[i];
|
|
@@ -6010,8 +6129,8 @@ async function reportGlassTerminal(view, outDir, log, json) {
|
|
|
6010
6129
|
});
|
|
6011
6130
|
continue;
|
|
6012
6131
|
}
|
|
6013
|
-
const outPath =
|
|
6014
|
-
await
|
|
6132
|
+
const outPath = path17.join(outDir, `glass-${i + 1}.png`);
|
|
6133
|
+
await fs18.writeFile(outPath, PNG4.sync.write(r.png));
|
|
6015
6134
|
solved.push({
|
|
6016
6135
|
path: outPath,
|
|
6017
6136
|
url: f.url,
|
|
@@ -6605,8 +6724,8 @@ async function toRow(e, v, cwd) {
|
|
|
6605
6724
|
}
|
|
6606
6725
|
|
|
6607
6726
|
// src/commands/controller.ts
|
|
6608
|
-
import
|
|
6609
|
-
import
|
|
6727
|
+
import fs20 from "fs/promises";
|
|
6728
|
+
import path19 from "path";
|
|
6610
6729
|
|
|
6611
6730
|
// ../../packages/meshy-animation-catalog/src/index.ts
|
|
6612
6731
|
import { createHash } from "crypto";
|
|
@@ -15736,9 +15855,9 @@ function searchMeshyAnimations(query, options = {}) {
|
|
|
15736
15855
|
}
|
|
15737
15856
|
|
|
15738
15857
|
// src/lib/anims.ts
|
|
15739
|
-
import
|
|
15740
|
-
import
|
|
15741
|
-
var ANIMS_DEST =
|
|
15858
|
+
import fs19 from "fs/promises";
|
|
15859
|
+
import path18 from "path";
|
|
15860
|
+
var ANIMS_DEST = path18.join("public", "assets", "anims");
|
|
15742
15861
|
var HIDDEN_TAG = "reference";
|
|
15743
15862
|
async function runAnims(opts) {
|
|
15744
15863
|
const log = createLogger({ quiet: opts.quiet });
|
|
@@ -15754,7 +15873,7 @@ async function runAnims(opts) {
|
|
|
15754
15873
|
printCatalog(log, manifest, selectors);
|
|
15755
15874
|
return;
|
|
15756
15875
|
}
|
|
15757
|
-
const controllerMarker =
|
|
15876
|
+
const controllerMarker = path18.join(root, "src", "controllers", "character");
|
|
15758
15877
|
if (!await exists2(controllerMarker)) {
|
|
15759
15878
|
log.error(
|
|
15760
15879
|
`No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
|
|
@@ -15763,11 +15882,11 @@ async function runAnims(opts) {
|
|
|
15763
15882
|
process.exitCode = 1;
|
|
15764
15883
|
return;
|
|
15765
15884
|
}
|
|
15766
|
-
const destDir =
|
|
15767
|
-
const gameManifestPath =
|
|
15885
|
+
const destDir = path18.join(root, ANIMS_DEST);
|
|
15886
|
+
const gameManifestPath = path18.join(destDir, "manifest.json");
|
|
15768
15887
|
if (opts.reset) {
|
|
15769
|
-
await
|
|
15770
|
-
log.step(`Cleared ${c.cyan(ANIMS_DEST +
|
|
15888
|
+
await fs19.rm(destDir, { recursive: true, force: true });
|
|
15889
|
+
log.step(`Cleared ${c.cyan(ANIMS_DEST + path18.sep)} (--reset)`);
|
|
15771
15890
|
}
|
|
15772
15891
|
if (selectors.length === 0) {
|
|
15773
15892
|
const installed = await readGameManifest(gameManifestPath);
|
|
@@ -15805,35 +15924,35 @@ async function runAnims(opts) {
|
|
|
15805
15924
|
}
|
|
15806
15925
|
}
|
|
15807
15926
|
const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
15808
|
-
const cacheDir =
|
|
15927
|
+
const cacheDir = path18.join(
|
|
15809
15928
|
opts.cacheDir ?? getAnimsCacheDir(),
|
|
15810
15929
|
`${manifest.library}-v${manifest.version}`
|
|
15811
15930
|
);
|
|
15812
|
-
await
|
|
15813
|
-
await
|
|
15931
|
+
await fs19.mkdir(cacheDir, { recursive: true });
|
|
15932
|
+
await fs19.mkdir(destDir, { recursive: true });
|
|
15814
15933
|
const base = getAnimsBase(opts.animsBase);
|
|
15815
15934
|
let installedCount = 0;
|
|
15816
15935
|
let presentCount = 0;
|
|
15817
15936
|
let addedBytes = 0;
|
|
15818
15937
|
const failures = [];
|
|
15819
15938
|
for (const entry of wanted) {
|
|
15820
|
-
const dest =
|
|
15939
|
+
const dest = path18.join(destDir, entry.file);
|
|
15821
15940
|
if (await hasSize(dest, entry.bytes)) {
|
|
15822
15941
|
presentCount++;
|
|
15823
15942
|
continue;
|
|
15824
15943
|
}
|
|
15825
15944
|
try {
|
|
15826
|
-
const cached =
|
|
15945
|
+
const cached = path18.join(cacheDir, entry.file);
|
|
15827
15946
|
if (!await hasSize(cached, entry.bytes)) {
|
|
15828
15947
|
const res = await fetch(base + entry.file);
|
|
15829
15948
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
15830
15949
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
15831
|
-
await
|
|
15950
|
+
await fs19.writeFile(cached, buf);
|
|
15832
15951
|
}
|
|
15833
|
-
await
|
|
15952
|
+
await fs19.copyFile(cached, dest);
|
|
15834
15953
|
installedCount++;
|
|
15835
15954
|
addedBytes += entry.bytes;
|
|
15836
|
-
log.dim(` ${
|
|
15955
|
+
log.dim(` ${path18.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
|
|
15837
15956
|
} catch (err) {
|
|
15838
15957
|
failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
|
|
15839
15958
|
}
|
|
@@ -15849,13 +15968,13 @@ async function runAnims(opts) {
|
|
|
15849
15968
|
version: manifest.version,
|
|
15850
15969
|
clips: [...union].sort((a, b) => a.localeCompare(b))
|
|
15851
15970
|
};
|
|
15852
|
-
await
|
|
15971
|
+
await fs19.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
|
|
15853
15972
|
log.plain("");
|
|
15854
15973
|
const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
|
|
15855
15974
|
if (presentCount > 0) parts.push(`${presentCount} already present`);
|
|
15856
15975
|
if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
|
|
15857
15976
|
log.success(
|
|
15858
|
-
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST +
|
|
15977
|
+
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path18.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
|
|
15859
15978
|
);
|
|
15860
15979
|
for (const [selector, entries] of resolved) {
|
|
15861
15980
|
const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
|
|
@@ -15887,8 +16006,8 @@ async function loadManifest(baseOverride) {
|
|
|
15887
16006
|
}
|
|
15888
16007
|
} catch {
|
|
15889
16008
|
}
|
|
15890
|
-
const snapshotPath =
|
|
15891
|
-
const manifest = JSON.parse(await
|
|
16009
|
+
const snapshotPath = path18.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
|
|
16010
|
+
const manifest = JSON.parse(await fs19.readFile(snapshotPath, "utf8"));
|
|
15892
16011
|
return { manifest, source: "snapshot" };
|
|
15893
16012
|
}
|
|
15894
16013
|
function resolveSelectors(manifest, selectors) {
|
|
@@ -16006,21 +16125,21 @@ function printCatalog(log, manifest, selectors) {
|
|
|
16006
16125
|
}
|
|
16007
16126
|
async function readGameManifest(file) {
|
|
16008
16127
|
try {
|
|
16009
|
-
return JSON.parse(await
|
|
16128
|
+
return JSON.parse(await fs19.readFile(file, "utf8"));
|
|
16010
16129
|
} catch {
|
|
16011
16130
|
return null;
|
|
16012
16131
|
}
|
|
16013
16132
|
}
|
|
16014
16133
|
async function hasSize(file, bytes) {
|
|
16015
16134
|
try {
|
|
16016
|
-
return (await
|
|
16135
|
+
return (await fs19.stat(file)).size === bytes;
|
|
16017
16136
|
} catch {
|
|
16018
16137
|
return false;
|
|
16019
16138
|
}
|
|
16020
16139
|
}
|
|
16021
16140
|
async function exists2(p) {
|
|
16022
16141
|
try {
|
|
16023
|
-
await
|
|
16142
|
+
await fs19.access(p);
|
|
16024
16143
|
return true;
|
|
16025
16144
|
} catch {
|
|
16026
16145
|
return false;
|
|
@@ -16037,7 +16156,9 @@ var CONTROLLER_KINDS = [
|
|
|
16037
16156
|
"drone",
|
|
16038
16157
|
"touch",
|
|
16039
16158
|
"networked-physics",
|
|
16040
|
-
"quality"
|
|
16159
|
+
"quality",
|
|
16160
|
+
"chat",
|
|
16161
|
+
"voice"
|
|
16041
16162
|
];
|
|
16042
16163
|
var SHARED = [
|
|
16043
16164
|
"shared/math.ts",
|
|
@@ -16160,6 +16281,26 @@ var CONTROLLER_FILE_SETS = {
|
|
|
16160
16281
|
`const gov = new QualityGovernor(tier, { setDprScale: (m) => renderer.setPixelRatio(Math.min(window.devicePixelRatio, tier.dprCap * m)), setShadowQuality: (l) => sun.shadow.mapSize.setScalar(l === 'full' ? tier.shadowMapSize : tier.shadowMapSize / 2) }, renderer);`
|
|
16161
16282
|
]
|
|
16162
16283
|
},
|
|
16284
|
+
voice: {
|
|
16285
|
+
code: ["voice/voice-proximity.ts", NOTICE],
|
|
16286
|
+
assets: [],
|
|
16287
|
+
skill: "genex-threejs-multiplayer",
|
|
16288
|
+
sketch: [
|
|
16289
|
+
`const voice = new ProximityVoice({ room, listener: camera.children.find(c => c.type === "AudioListener") });`,
|
|
16290
|
+
`await voice.join(); // MUST be inside a click/tap handler \u2014 browsers gate the mic prompt on a gesture`,
|
|
16291
|
+
`// per frame: voice.update((id) => remoteBodies.get(id)?.position ?? null)`
|
|
16292
|
+
]
|
|
16293
|
+
},
|
|
16294
|
+
chat: {
|
|
16295
|
+
code: ["chat/chat-overlay.ts", NOTICE],
|
|
16296
|
+
assets: [],
|
|
16297
|
+
skill: "genex-threejs-multiplayer",
|
|
16298
|
+
sketch: [
|
|
16299
|
+
`const room = await connect({ gameId: "<slug>", auth: getColyseusAuth }); // @genex-ai/multiplayer`,
|
|
16300
|
+
`const chat = new ChatOverlay({ room, onTypingChange: (t) => { keyboard.enabled = !t; } });`,
|
|
16301
|
+
`// that onTypingChange is REQUIRED \u2014 without it "w" walks the player while someone types`
|
|
16302
|
+
]
|
|
16303
|
+
},
|
|
16163
16304
|
touch: {
|
|
16164
16305
|
code: [...TOUCH_KIT, NOTICE],
|
|
16165
16306
|
assets: [],
|
|
@@ -16189,8 +16330,8 @@ var CONTROLLER_FILE_SETS = {
|
|
|
16189
16330
|
]
|
|
16190
16331
|
}
|
|
16191
16332
|
};
|
|
16192
|
-
var CODE_DEST =
|
|
16193
|
-
var ASSETS_DEST =
|
|
16333
|
+
var CODE_DEST = path19.join("src", "controllers");
|
|
16334
|
+
var ASSETS_DEST = path19.join("public", "assets");
|
|
16194
16335
|
async function runController(opts) {
|
|
16195
16336
|
const log = createLogger({ quiet: opts.quiet });
|
|
16196
16337
|
if (opts.kind?.trim() === "anims") {
|
|
@@ -16207,31 +16348,31 @@ async function runController(opts) {
|
|
|
16207
16348
|
process.exitCode = 1;
|
|
16208
16349
|
return;
|
|
16209
16350
|
}
|
|
16210
|
-
const srcDir =
|
|
16351
|
+
const srcDir = path19.join(getTemplatesDir(), "controllers");
|
|
16211
16352
|
const root = opts.cwd ?? process.cwd();
|
|
16212
16353
|
const set = CONTROLLER_FILE_SETS[kind];
|
|
16213
16354
|
log.plain(c.bold(`genex controller ${kind}`));
|
|
16214
16355
|
log.plain("");
|
|
16215
|
-
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST +
|
|
16356
|
+
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path19.sep)}`);
|
|
16216
16357
|
const plan = [
|
|
16217
|
-
...set.code.map((rel) => ({ from: rel, rel:
|
|
16358
|
+
...set.code.map((rel) => ({ from: rel, rel: path19.join(CODE_DEST, rel) })),
|
|
16218
16359
|
...set.assets.map((rel) => ({
|
|
16219
16360
|
from: rel,
|
|
16220
|
-
rel:
|
|
16361
|
+
rel: path19.join(ASSETS_DEST, path19.basename(rel))
|
|
16221
16362
|
}))
|
|
16222
16363
|
];
|
|
16223
16364
|
let copied = 0;
|
|
16224
16365
|
let skipped = 0;
|
|
16225
16366
|
try {
|
|
16226
16367
|
for (const file of plan) {
|
|
16227
|
-
const dest =
|
|
16368
|
+
const dest = path19.join(root, file.rel);
|
|
16228
16369
|
if (!opts.force && await exists3(dest)) {
|
|
16229
16370
|
skipped++;
|
|
16230
16371
|
log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
|
|
16231
16372
|
continue;
|
|
16232
16373
|
}
|
|
16233
|
-
await
|
|
16234
|
-
await
|
|
16374
|
+
await fs20.mkdir(path19.dirname(dest), { recursive: true });
|
|
16375
|
+
await fs20.copyFile(path19.join(srcDir, file.from), dest);
|
|
16235
16376
|
copied++;
|
|
16236
16377
|
log.dim(` ${file.rel}`);
|
|
16237
16378
|
}
|
|
@@ -16284,7 +16425,7 @@ async function runController(opts) {
|
|
|
16284
16425
|
for (const line of set.sketch) {
|
|
16285
16426
|
log.dim(` ${line}`);
|
|
16286
16427
|
}
|
|
16287
|
-
if (kind === "character" && !await exists3(
|
|
16428
|
+
if (kind === "character" && !await exists3(path19.join(root, ASSETS_DEST, "meshy-character.json"))) {
|
|
16288
16429
|
log.plain("");
|
|
16289
16430
|
log.plain(
|
|
16290
16431
|
` ${stepOffset + 3}. This game has no generated character yet \u2014 ${c.cyan(
|
|
@@ -16316,9 +16457,9 @@ async function installMeshyCharacterManifest(args) {
|
|
|
16316
16457
|
throw new Error("The API returned an invalid Meshy character manifest.");
|
|
16317
16458
|
}
|
|
16318
16459
|
assertCompleteMeshyControllerPack(manifest);
|
|
16319
|
-
const destination =
|
|
16320
|
-
await
|
|
16321
|
-
await
|
|
16460
|
+
const destination = path19.join(args.root, ASSETS_DEST, "meshy-character.json");
|
|
16461
|
+
await fs20.mkdir(path19.dirname(destination), { recursive: true });
|
|
16462
|
+
await fs20.writeFile(
|
|
16322
16463
|
destination,
|
|
16323
16464
|
`${JSON.stringify(manifest, null, 2)}
|
|
16324
16465
|
`
|
|
@@ -16456,14 +16597,14 @@ function assertCompleteMeshyControllerPack(manifest) {
|
|
|
16456
16597
|
}
|
|
16457
16598
|
async function installFallbackAvatar(args) {
|
|
16458
16599
|
const { root, srcDir, log } = args;
|
|
16459
|
-
const dest =
|
|
16460
|
-
await
|
|
16461
|
-
await
|
|
16600
|
+
const dest = path19.join(root, ASSETS_DEST, "avatar.vrm");
|
|
16601
|
+
await fs20.mkdir(path19.dirname(dest), { recursive: true });
|
|
16602
|
+
await fs20.copyFile(path19.join(srcDir, "assets", "default-avatar.vrm"), dest);
|
|
16462
16603
|
log.dim(" public/assets/avatar.vrm (fallback avatar \u2014 bundled CC0 default)");
|
|
16463
16604
|
}
|
|
16464
16605
|
async function exists3(p) {
|
|
16465
16606
|
try {
|
|
16466
|
-
await
|
|
16607
|
+
await fs20.access(p);
|
|
16467
16608
|
return true;
|
|
16468
16609
|
} catch {
|
|
16469
16610
|
return false;
|
|
@@ -16957,22 +17098,22 @@ async function context2(opts) {
|
|
|
16957
17098
|
const project = await readProject();
|
|
16958
17099
|
return { token, apiUrl: getApiUrl(opts.apiUrl ?? project?.apiUrl) };
|
|
16959
17100
|
}
|
|
16960
|
-
async function readVideo(
|
|
17101
|
+
async function readVideo(path21, log) {
|
|
16961
17102
|
let bytes;
|
|
16962
17103
|
try {
|
|
16963
|
-
bytes = await readFile(
|
|
17104
|
+
bytes = await readFile(path21);
|
|
16964
17105
|
} catch {
|
|
16965
|
-
log.error(`Can't read ${
|
|
17106
|
+
log.error(`Can't read ${path21}.`);
|
|
16966
17107
|
return null;
|
|
16967
17108
|
}
|
|
16968
17109
|
if (bytes.byteLength > MAX_VIDEO_BYTES) {
|
|
16969
|
-
log.error(`${basename(
|
|
17110
|
+
log.error(`${basename(path21)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
|
|
16970
17111
|
return null;
|
|
16971
17112
|
}
|
|
16972
17113
|
return bytes;
|
|
16973
17114
|
}
|
|
16974
|
-
async function uploadVideo(apiUrl, token, characterId,
|
|
16975
|
-
const contentType = /\.mov$/i.test(
|
|
17115
|
+
async function uploadVideo(apiUrl, token, characterId, path21, bytes, log) {
|
|
17116
|
+
const contentType = /\.mov$/i.test(path21) ? "video/quicktime" : "video/mp4";
|
|
16976
17117
|
const minted = await apiFetch(
|
|
16977
17118
|
`${apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/video-url`,
|
|
16978
17119
|
{
|
|
@@ -16987,7 +17128,7 @@ async function uploadVideo(apiUrl, token, characterId, path20, bytes, log) {
|
|
|
16987
17128
|
return null;
|
|
16988
17129
|
}
|
|
16989
17130
|
const { uploadUrl, videoUrl } = await minted.json();
|
|
16990
|
-
log.dim(` uploading ${basename(
|
|
17131
|
+
log.dim(` uploading ${basename(path21)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
|
|
16991
17132
|
const put = await fetch(uploadUrl, {
|
|
16992
17133
|
method: "PUT",
|
|
16993
17134
|
headers: { "Content-Type": contentType, "Content-Length": String(bytes.byteLength) },
|
|
@@ -17303,8 +17444,8 @@ function rank(items, query) {
|
|
|
17303
17444
|
}
|
|
17304
17445
|
|
|
17305
17446
|
// src/commands/motion.ts
|
|
17306
|
-
import
|
|
17307
|
-
import
|
|
17447
|
+
import fs21 from "fs/promises";
|
|
17448
|
+
import path20 from "path";
|
|
17308
17449
|
|
|
17309
17450
|
// src/lib/motion/npz.ts
|
|
17310
17451
|
import zlib from "zlib";
|
|
@@ -18555,7 +18696,7 @@ async function motionGen(opts, log) {
|
|
|
18555
18696
|
}
|
|
18556
18697
|
if (opts.constraintsPath !== void 0) {
|
|
18557
18698
|
try {
|
|
18558
|
-
const raw = await
|
|
18699
|
+
const raw = await fs21.readFile(opts.constraintsPath, "utf8");
|
|
18559
18700
|
generationOptions.constraints = JSON.parse(raw);
|
|
18560
18701
|
} catch {
|
|
18561
18702
|
log.error(`Couldn't read the --constraints JSON at ${opts.constraintsPath}.`);
|
|
@@ -18579,10 +18720,10 @@ async function motionGen(opts, log) {
|
|
|
18579
18720
|
async function expandTakes(selectors) {
|
|
18580
18721
|
const out = [];
|
|
18581
18722
|
for (const sel of selectors) {
|
|
18582
|
-
const st = await
|
|
18723
|
+
const st = await fs21.stat(sel).catch(() => null);
|
|
18583
18724
|
if (st?.isDirectory()) {
|
|
18584
|
-
const names = await
|
|
18585
|
-
for (const n of names.sort()) if (n.endsWith(".npz")) out.push(
|
|
18725
|
+
const names = await fs21.readdir(sel);
|
|
18726
|
+
for (const n of names.sort()) if (n.endsWith(".npz")) out.push(path20.join(sel, n));
|
|
18586
18727
|
} else if (st?.isFile()) {
|
|
18587
18728
|
out.push(sel);
|
|
18588
18729
|
} else {
|
|
@@ -18617,7 +18758,7 @@ async function motionVerify(opts, log) {
|
|
|
18617
18758
|
let gates = DEFAULT_GATES;
|
|
18618
18759
|
if (opts.gatesPath) {
|
|
18619
18760
|
try {
|
|
18620
|
-
gates = mergeGates(DEFAULT_GATES, JSON.parse(await
|
|
18761
|
+
gates = mergeGates(DEFAULT_GATES, JSON.parse(await fs21.readFile(opts.gatesPath, "utf8")));
|
|
18621
18762
|
} catch {
|
|
18622
18763
|
log.error(`Couldn't read the --gates JSON at ${opts.gatesPath}.`);
|
|
18623
18764
|
process.exitCode = 1;
|
|
@@ -18639,9 +18780,9 @@ async function motionVerify(opts, log) {
|
|
|
18639
18780
|
}
|
|
18640
18781
|
const reports = [];
|
|
18641
18782
|
for (const file of files) {
|
|
18642
|
-
const stem =
|
|
18783
|
+
const stem = path20.basename(file).replace(/\.npz$/, "");
|
|
18643
18784
|
try {
|
|
18644
|
-
reports.push(analyzeTake(stem, await
|
|
18785
|
+
reports.push(analyzeTake(stem, await fs21.readFile(file), gates));
|
|
18645
18786
|
} catch (err) {
|
|
18646
18787
|
reports.push({
|
|
18647
18788
|
take: stem,
|
|
@@ -18679,7 +18820,7 @@ async function motionCompile(opts, log) {
|
|
|
18679
18820
|
let cfg = DEFAULT_MOTION_CONFIG;
|
|
18680
18821
|
if (opts.configPath) {
|
|
18681
18822
|
try {
|
|
18682
|
-
const patch = JSON.parse(await
|
|
18823
|
+
const patch = JSON.parse(await fs21.readFile(opts.configPath, "utf8"));
|
|
18683
18824
|
cfg = { ...DEFAULT_MOTION_CONFIG, ...patch, idleLoop: { ...DEFAULT_MOTION_CONFIG.idleLoop, ...patch.idleLoop } };
|
|
18684
18825
|
} catch {
|
|
18685
18826
|
log.error(`Couldn't read the --config JSON at ${opts.configPath}.`);
|
|
@@ -18697,16 +18838,16 @@ async function motionCompile(opts, log) {
|
|
|
18697
18838
|
}
|
|
18698
18839
|
const inputs = [];
|
|
18699
18840
|
for (const file of files) {
|
|
18700
|
-
const stem =
|
|
18841
|
+
const stem = path20.basename(file).replace(/\.npz$/, "");
|
|
18701
18842
|
try {
|
|
18702
|
-
inputs.push({ stem, take: loadTake(await
|
|
18843
|
+
inputs.push({ stem, take: loadTake(await fs21.readFile(file)) });
|
|
18703
18844
|
} catch (err) {
|
|
18704
18845
|
log.error(`${stem}: ${err instanceof Error ? err.message : String(err)}`);
|
|
18705
18846
|
process.exitCode = 1;
|
|
18706
18847
|
return;
|
|
18707
18848
|
}
|
|
18708
18849
|
}
|
|
18709
|
-
const setName = opts.set ??
|
|
18850
|
+
const setName = opts.set ?? path20.basename(opts.out).replace(/\.json$/, "");
|
|
18710
18851
|
let result;
|
|
18711
18852
|
try {
|
|
18712
18853
|
result = compileSet(inputs, setName, cfg);
|
|
@@ -18721,9 +18862,9 @@ async function motionCompile(opts, log) {
|
|
|
18721
18862
|
process.exitCode = 1;
|
|
18722
18863
|
return;
|
|
18723
18864
|
}
|
|
18724
|
-
await
|
|
18865
|
+
await fs21.mkdir(path20.dirname(path20.resolve(opts.out)), { recursive: true });
|
|
18725
18866
|
const json = JSON.stringify(result.data);
|
|
18726
|
-
await
|
|
18867
|
+
await fs21.writeFile(opts.out, json);
|
|
18727
18868
|
if (opts.json) {
|
|
18728
18869
|
writeJson({ out: opts.out, set: setName, tracks: Object.keys(result.data.gaits), bytes: json.length });
|
|
18729
18870
|
return;
|
|
@@ -18741,9 +18882,9 @@ var MOTION_RUNTIME_FILES = [
|
|
|
18741
18882
|
var MOTION_PRESETS = {
|
|
18742
18883
|
rifle: ["sets/rifle.json", "sets/jumps.json"]
|
|
18743
18884
|
};
|
|
18744
|
-
var MOTION_DEST =
|
|
18885
|
+
var MOTION_DEST = path20.join("src", "motion");
|
|
18745
18886
|
async function motionInstall(opts, log) {
|
|
18746
|
-
const srcDir =
|
|
18887
|
+
const srcDir = path20.join(getTemplatesDir(), "motion");
|
|
18747
18888
|
const root = opts.cwd ?? process.cwd();
|
|
18748
18889
|
const preset = opts.set;
|
|
18749
18890
|
if (preset !== void 0 && !MOTION_PRESETS[preset]) {
|
|
@@ -18754,21 +18895,21 @@ async function motionInstall(opts, log) {
|
|
|
18754
18895
|
const files = [...MOTION_RUNTIME_FILES, ...preset ? MOTION_PRESETS[preset] : []];
|
|
18755
18896
|
log.plain(c.bold(`genex motion install${preset ? ` --set ${preset}` : ""}`));
|
|
18756
18897
|
log.plain("");
|
|
18757
|
-
log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST +
|
|
18898
|
+
log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path20.sep)}`);
|
|
18758
18899
|
let copied = 0, skipped = 0;
|
|
18759
18900
|
try {
|
|
18760
18901
|
for (const rel of files) {
|
|
18761
|
-
const dest =
|
|
18762
|
-
const exists4 = await
|
|
18902
|
+
const dest = path20.join(root, MOTION_DEST, rel);
|
|
18903
|
+
const exists4 = await fs21.access(dest).then(() => true, () => false);
|
|
18763
18904
|
if (!opts.force && exists4) {
|
|
18764
18905
|
skipped++;
|
|
18765
|
-
log.dim(` skipped ${
|
|
18906
|
+
log.dim(` skipped ${path20.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
|
|
18766
18907
|
continue;
|
|
18767
18908
|
}
|
|
18768
|
-
await
|
|
18769
|
-
await
|
|
18909
|
+
await fs21.mkdir(path20.dirname(dest), { recursive: true });
|
|
18910
|
+
await fs21.copyFile(path20.join(srcDir, rel), dest);
|
|
18770
18911
|
copied++;
|
|
18771
|
-
log.dim(` ${
|
|
18912
|
+
log.dim(` ${path20.join(MOTION_DEST, rel)}`);
|
|
18772
18913
|
}
|
|
18773
18914
|
} catch (err) {
|
|
18774
18915
|
log.error(`Copy failed: ${String(err)}`);
|
|
@@ -18809,7 +18950,7 @@ async function motionConstraints(opts, log) {
|
|
|
18809
18950
|
}
|
|
18810
18951
|
const doc = directionConstraint(dir, speed, duration);
|
|
18811
18952
|
const out = opts.out ?? "constraints.json";
|
|
18812
|
-
await
|
|
18953
|
+
await fs21.writeFile(out, JSON.stringify(doc));
|
|
18813
18954
|
if (opts.json) {
|
|
18814
18955
|
writeJson({ out, dir: dirName, speed, duration, waypoints: doc[0].frame_indices.length });
|
|
18815
18956
|
return;
|
|
@@ -18859,6 +19000,11 @@ ${c.bold("Usage")}
|
|
|
18859
19000
|
genex link <slug> [options] Re-link THIS folder to an existing game of yours
|
|
18860
19001
|
(lost folder / new machine); preview/publish then
|
|
18861
19002
|
update the same live game. Never creates a project.
|
|
19003
|
+
genex rename <name> [options] Rename THIS game \u2014 its play address, its source repo,
|
|
19004
|
+
and its gallery name. Keeps the game (plays, likes,
|
|
19005
|
+
comments); the OLD address stops working, so a
|
|
19006
|
+
published game needs --yes. Rebuild after with
|
|
19007
|
+
'genex preview'/'publish'.
|
|
18862
19008
|
genex list [options] List your games \u2014 slug, status, and page link for
|
|
18863
19009
|
each. Signs you in if needed; --json for raw data.
|
|
18864
19010
|
Find the slug to reconnect with 'genex link' here.
|
|
@@ -18916,13 +19062,18 @@ ${c.bold("Usage")}
|
|
|
18916
19062
|
enqueued (done/running/queued/failed) \u2014 a single
|
|
18917
19063
|
API refresh, never blocks, never bills.
|
|
18918
19064
|
genex controller <type> [--force] Install a bundled controller
|
|
18919
|
-
(character|car|drone|touch|networked-physics|
|
|
18920
|
-
into src/controllers (+ assets
|
|
19065
|
+
(character|car|drone|touch|networked-physics|
|
|
19066
|
+
quality|chat|voice) into src/controllers (+ assets
|
|
19067
|
+
into public/assets).
|
|
18921
19068
|
"touch" = the standalone mobile touch kit (joystick,
|
|
18922
19069
|
buttons, drag zone, rotate overlay) for games
|
|
18923
19070
|
without a physics controller. "quality" = the
|
|
18924
19071
|
adaptive-quality kit (device tier, runtime governor,
|
|
18925
19072
|
per-tier asset rungs) every game wires at boot.
|
|
19073
|
+
"chat" = the room text-chat overlay for multiplayer
|
|
19074
|
+
games (transport is room.chat in the SDK).
|
|
19075
|
+
"voice" = proximity voice for multiplayer games
|
|
19076
|
+
(mesh WebRTC via room.voice; party-sized, max 6).
|
|
18926
19077
|
Character's player body is the game's own generated
|
|
18927
19078
|
character, with a VRM avatar as the fallback.
|
|
18928
19079
|
genex controller character --character <id>
|
|
@@ -19025,6 +19176,12 @@ ${c.bold("Options for `link`")}
|
|
|
19025
19176
|
off to 'genex auth' (default: 100). The code itself stays
|
|
19026
19177
|
valid for 15 minutes either way.
|
|
19027
19178
|
|
|
19179
|
+
${c.bold("Options for `rename`")}
|
|
19180
|
+
<name> The new name (positional). Slugified the same way init does it.
|
|
19181
|
+
--yes Confirm retiring the current address (required once published).
|
|
19182
|
+
--env <path> Token env file (default: ~/.genex/env).
|
|
19183
|
+
--api-url <url> Override the API base URL.
|
|
19184
|
+
|
|
19028
19185
|
${c.bold("Options for `preview` / `publish`")}
|
|
19029
19186
|
--no-build Skip the build step; deploy whatever is already built.
|
|
19030
19187
|
--no-push (publish) Skip build + push; only flip the gallery flag.
|
|
@@ -19132,6 +19289,8 @@ ${c.bold("Examples")}
|
|
|
19132
19289
|
genex controller anims sword pistol
|
|
19133
19290
|
genex controller touch
|
|
19134
19291
|
genex controller networked-physics
|
|
19292
|
+
genex controller chat
|
|
19293
|
+
genex controller voice
|
|
19135
19294
|
`;
|
|
19136
19295
|
function parseArgs(argv) {
|
|
19137
19296
|
const parsed = {
|
|
@@ -19283,6 +19442,9 @@ function parseArgs(argv) {
|
|
|
19283
19442
|
case "--force":
|
|
19284
19443
|
parsed.options.force = true;
|
|
19285
19444
|
break;
|
|
19445
|
+
case "--yes":
|
|
19446
|
+
parsed.options.yes = true;
|
|
19447
|
+
break;
|
|
19286
19448
|
case "--list":
|
|
19287
19449
|
parsed.options.list = true;
|
|
19288
19450
|
break;
|
|
@@ -19677,6 +19839,9 @@ async function main() {
|
|
|
19677
19839
|
case "link":
|
|
19678
19840
|
await runLink(parsed.options);
|
|
19679
19841
|
break;
|
|
19842
|
+
case "rename":
|
|
19843
|
+
await runRename(parsed.options);
|
|
19844
|
+
break;
|
|
19680
19845
|
case "list":
|
|
19681
19846
|
await runList(parsed.options);
|
|
19682
19847
|
break;
|