@genex-ai/cli-demo 0.62.0-dev.143 → 0.62.0-dev.144
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 +213 -77
- package/package.json +1 -1
- package/templates/skills/genex-ai-menu/SKILL.md +13 -0
- package/templates/skills/genex-threejs-game-content/SKILL.md +189 -0
- package/templates/skills/genex-threejs-game-content/references/content-tables.md +269 -0
- package/templates/skills/genex-threejs-open-world/SKILL.md +149 -0
- package/templates/skills/genex-threejs-open-world/references/terrain-streaming.md +215 -0
- package/templates/skills/genex-threejs-skill-router/SKILL.md +20 -0
- package/templates/skills/genex-threejs-skill-router/references/routing-map.md +38 -4
package/dist/index.js
CHANGED
|
@@ -1929,11 +1929,69 @@ async function runMakeRemixable(opts) {
|
|
|
1929
1929
|
}
|
|
1930
1930
|
|
|
1931
1931
|
// src/lib/detect-features.ts
|
|
1932
|
+
import fs11 from "fs/promises";
|
|
1933
|
+
import path12 from "path";
|
|
1934
|
+
|
|
1935
|
+
// src/lib/generation-ledger.ts
|
|
1932
1936
|
import fs10 from "fs/promises";
|
|
1933
1937
|
import path11 from "path";
|
|
1938
|
+
var ledgerPath = (cwd) => path11.join(cwd, ".genex", "generations.ndjson");
|
|
1939
|
+
async function append(cwd, event) {
|
|
1940
|
+
try {
|
|
1941
|
+
await fs10.access(path11.join(cwd, ".genex"));
|
|
1942
|
+
await fs10.appendFile(ledgerPath(cwd), `${JSON.stringify(event)}
|
|
1943
|
+
`, "utf8");
|
|
1944
|
+
} catch {
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
async function readLedger(cwd = process.cwd()) {
|
|
1948
|
+
let raw;
|
|
1949
|
+
try {
|
|
1950
|
+
raw = await fs10.readFile(ledgerPath(cwd), "utf8");
|
|
1951
|
+
} catch {
|
|
1952
|
+
return [];
|
|
1953
|
+
}
|
|
1954
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1955
|
+
for (const line of raw.split("\n")) {
|
|
1956
|
+
if (!line.trim()) continue;
|
|
1957
|
+
try {
|
|
1958
|
+
const e = JSON.parse(line);
|
|
1959
|
+
if (e.t === "q" && typeof e.id === "string" && typeof e.kind === "string") {
|
|
1960
|
+
if (!entries.has(e.id)) {
|
|
1961
|
+
entries.set(e.id, {
|
|
1962
|
+
id: e.id,
|
|
1963
|
+
kind: e.kind,
|
|
1964
|
+
prompt: typeof e.prompt === "string" ? e.prompt : "",
|
|
1965
|
+
queuedAt: typeof e.at === "string" ? e.at : "",
|
|
1966
|
+
status: "queued"
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
} else if (e.t === "done" && typeof e.id === "string") {
|
|
1970
|
+
const entry = entries.get(e.id);
|
|
1971
|
+
if (entry && (e.status === "completed" || e.status === "failed")) {
|
|
1972
|
+
entry.status = e.status;
|
|
1973
|
+
if (Array.isArray(e.urls)) entry.urls = e.urls.filter((u) => typeof u === "string");
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
} catch {
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
return [...entries.values()];
|
|
1980
|
+
}
|
|
1981
|
+
async function recordQueued(id, kind, prompt, cwd = process.cwd()) {
|
|
1982
|
+
await append(cwd, { t: "q", id, kind, prompt: prompt.slice(0, 120), at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1983
|
+
}
|
|
1984
|
+
async function recordTerminal(id, status, urls, cwd = process.cwd()) {
|
|
1985
|
+
await append(cwd, { t: "done", id, status, ...urls?.length ? { urls } : {} });
|
|
1986
|
+
}
|
|
1987
|
+
async function countFailed(kind, cwd = process.cwd()) {
|
|
1988
|
+
return (await readLedger(cwd)).filter((e) => e.kind === kind && e.status === "failed").length;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
// src/lib/detect-features.ts
|
|
1934
1992
|
async function detectEmbedSdkVersion(cwd = process.cwd()) {
|
|
1935
1993
|
try {
|
|
1936
|
-
const raw = await
|
|
1994
|
+
const raw = await fs11.readFile(path12.join(cwd, "package.json"), "utf8");
|
|
1937
1995
|
const pkg = JSON.parse(raw);
|
|
1938
1996
|
const version = pkg.dependencies?.["@genex-ai/embed-sdk"] ?? pkg.devDependencies?.["@genex-ai/embed-sdk"];
|
|
1939
1997
|
return typeof version === "string" && version ? version : null;
|
|
@@ -1943,7 +2001,7 @@ async function detectEmbedSdkVersion(cwd = process.cwd()) {
|
|
|
1943
2001
|
}
|
|
1944
2002
|
async function detectMultiplayer(cwd = process.cwd()) {
|
|
1945
2003
|
try {
|
|
1946
|
-
const raw = await
|
|
2004
|
+
const raw = await fs11.readFile(path12.join(cwd, "package.json"), "utf8");
|
|
1947
2005
|
const pkg = JSON.parse(raw);
|
|
1948
2006
|
return Boolean(
|
|
1949
2007
|
pkg.dependencies?.["@genex-ai/multiplayer"] ?? pkg.devDependencies?.["@genex-ai/multiplayer"]
|
|
@@ -1955,7 +2013,7 @@ async function detectMultiplayer(cwd = process.cwd()) {
|
|
|
1955
2013
|
async function detectMatchmaking(log, cwd = process.cwd()) {
|
|
1956
2014
|
let pkg;
|
|
1957
2015
|
try {
|
|
1958
|
-
pkg = JSON.parse(await
|
|
2016
|
+
pkg = JSON.parse(await fs11.readFile(path12.join(cwd, "package.json"), "utf8"));
|
|
1959
2017
|
} catch (err) {
|
|
1960
2018
|
log.dim(` (skipping matchmaking \u2014 couldn't read package.json: ${String(err)})`);
|
|
1961
2019
|
return null;
|
|
@@ -1972,10 +2030,10 @@ async function detectMatchmaking(log, cwd = process.cwd()) {
|
|
|
1972
2030
|
}
|
|
1973
2031
|
var GAME_STATE_CALLS = /savePlayerState|saveWorldState|submitScore/;
|
|
1974
2032
|
async function detectGameStateUsage(cwd = process.cwd()) {
|
|
1975
|
-
const srcDir =
|
|
2033
|
+
const srcDir = path12.join(cwd, "src");
|
|
1976
2034
|
let entries;
|
|
1977
2035
|
try {
|
|
1978
|
-
entries = await
|
|
2036
|
+
entries = await fs11.readdir(srcDir, { recursive: true });
|
|
1979
2037
|
} catch {
|
|
1980
2038
|
return false;
|
|
1981
2039
|
}
|
|
@@ -1983,7 +2041,7 @@ async function detectGameStateUsage(cwd = process.cwd()) {
|
|
|
1983
2041
|
if (rel.includes("node_modules")) continue;
|
|
1984
2042
|
if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(rel)) continue;
|
|
1985
2043
|
try {
|
|
1986
|
-
const content = await
|
|
2044
|
+
const content = await fs11.readFile(path12.join(srcDir, rel), "utf8");
|
|
1987
2045
|
if (GAME_STATE_CALLS.test(content)) return true;
|
|
1988
2046
|
} catch {
|
|
1989
2047
|
}
|
|
@@ -1993,14 +2051,14 @@ async function detectGameStateUsage(cwd = process.cwd()) {
|
|
|
1993
2051
|
var UI_PHASE_MARKERS = /data-phase|setPhase/;
|
|
1994
2052
|
async function detectUiPhases(cwd = process.cwd()) {
|
|
1995
2053
|
try {
|
|
1996
|
-
const html = await
|
|
2054
|
+
const html = await fs11.readFile(path12.join(cwd, "index.html"), "utf8").catch(() => "");
|
|
1997
2055
|
if (UI_PHASE_MARKERS.test(html)) return true;
|
|
1998
|
-
const srcDir =
|
|
1999
|
-
const entries = await
|
|
2056
|
+
const srcDir = path12.join(cwd, "src");
|
|
2057
|
+
const entries = await fs11.readdir(srcDir, { recursive: true });
|
|
2000
2058
|
for (const rel of entries) {
|
|
2001
2059
|
if (rel.includes("node_modules")) continue;
|
|
2002
2060
|
if (!/\.(ts|js|mts|mjs|tsx|jsx|html|css)$/.test(rel)) continue;
|
|
2003
|
-
const content = await
|
|
2061
|
+
const content = await fs11.readFile(path12.join(srcDir, rel), "utf8").catch(() => "");
|
|
2004
2062
|
if (UI_PHASE_MARKERS.test(content)) return true;
|
|
2005
2063
|
}
|
|
2006
2064
|
return false;
|
|
@@ -2045,17 +2103,17 @@ var lineOf = (content, index) => content.slice(0, index).split("\n").length;
|
|
|
2045
2103
|
var DEPTH_RATIO_LIMIT = 1e6;
|
|
2046
2104
|
async function detectSurfaceScan(cwd = process.cwd()) {
|
|
2047
2105
|
const found = { guessedRepeat: [], squarePoints: [], depthRange: [] };
|
|
2048
|
-
const srcDir =
|
|
2106
|
+
const srcDir = path12.join(cwd, "src");
|
|
2049
2107
|
let entries;
|
|
2050
2108
|
try {
|
|
2051
|
-
entries = await
|
|
2109
|
+
entries = await fs11.readdir(srcDir, { recursive: true });
|
|
2052
2110
|
} catch {
|
|
2053
2111
|
return found;
|
|
2054
2112
|
}
|
|
2055
2113
|
for (const rel of entries) {
|
|
2056
2114
|
if (rel.includes("node_modules")) continue;
|
|
2057
2115
|
if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(rel)) continue;
|
|
2058
|
-
const raw = await
|
|
2116
|
+
const raw = await fs11.readFile(path12.join(srcDir, rel), "utf8").catch(() => "");
|
|
2059
2117
|
if (!raw) continue;
|
|
2060
2118
|
const content = raw.replace(/\/\*[\s\S]*?\*\//g, (m2) => m2.replace(/[^\n]/g, " ")).replace(/(^|[^:])\/\/[^\n]*/gm, (m2, p1) => p1 + " ".repeat(m2.length - p1.length));
|
|
2061
2119
|
const repeatRe = /\.repeat\.set\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)/g;
|
|
@@ -2093,6 +2151,54 @@ async function detectSurfaceScan(cwd = process.cwd()) {
|
|
|
2093
2151
|
}
|
|
2094
2152
|
return found;
|
|
2095
2153
|
}
|
|
2154
|
+
var WIRED_BY_URL = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "texture", "video"]);
|
|
2155
|
+
var UNPICKED_AFTER_MS = 15 * 60 * 1e3;
|
|
2156
|
+
async function detectGenerationAudit(cwd = process.cwd()) {
|
|
2157
|
+
const ledger = await readLedger(cwd);
|
|
2158
|
+
const audited = ledger.filter((e) => WIRED_BY_URL.has(e.kind));
|
|
2159
|
+
if (audited.length === 0) return { unwired: [], unpicked: [] };
|
|
2160
|
+
let haystack = "";
|
|
2161
|
+
const read = async (file) => {
|
|
2162
|
+
try {
|
|
2163
|
+
haystack += await fs11.readFile(file, "utf8");
|
|
2164
|
+
} catch {
|
|
2165
|
+
}
|
|
2166
|
+
};
|
|
2167
|
+
try {
|
|
2168
|
+
for (const entry of await fs11.readdir(cwd, { withFileTypes: true })) {
|
|
2169
|
+
if (entry.isFile() && /\.(ts|tsx|js|jsx|css|html|json)$/.test(entry.name)) {
|
|
2170
|
+
await read(path12.join(cwd, entry.name));
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
} catch {
|
|
2174
|
+
}
|
|
2175
|
+
for (const sub of ["src", "public"]) {
|
|
2176
|
+
try {
|
|
2177
|
+
const entries = await fs11.readdir(path12.join(cwd, sub), { recursive: true });
|
|
2178
|
+
for (const rel of entries) {
|
|
2179
|
+
if (rel.includes("node_modules")) continue;
|
|
2180
|
+
if (!/\.(ts|tsx|js|jsx|css|html|json|txt)$/.test(rel)) continue;
|
|
2181
|
+
await read(path12.join(cwd, sub, rel));
|
|
2182
|
+
}
|
|
2183
|
+
} catch {
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
const ref = ({ id, kind, prompt }) => ({
|
|
2187
|
+
id,
|
|
2188
|
+
kind,
|
|
2189
|
+
prompt
|
|
2190
|
+
});
|
|
2191
|
+
const now = Date.now();
|
|
2192
|
+
return {
|
|
2193
|
+
unwired: audited.filter((e) => e.status === "completed" && !haystack.includes(e.id)).map(ref),
|
|
2194
|
+
// An id already referenced in the game was obviously picked up — a stale
|
|
2195
|
+
// "queued" record (e.g. `genex wait` ran from another directory) must not
|
|
2196
|
+
// nag about an asset that's wired in and working.
|
|
2197
|
+
unpicked: audited.filter(
|
|
2198
|
+
(e) => e.status === "queued" && !haystack.includes(e.id) && now - Date.parse(e.queuedAt) > UNPICKED_AFTER_MS
|
|
2199
|
+
).map(ref)
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2096
2202
|
async function detectFeatures(log, cwd = process.cwd()) {
|
|
2097
2203
|
return {
|
|
2098
2204
|
embedSdkVersion: await detectEmbedSdkVersion(cwd),
|
|
@@ -2100,7 +2206,8 @@ async function detectFeatures(log, cwd = process.cwd()) {
|
|
|
2100
2206
|
matchmaking: await detectMatchmaking(log, cwd),
|
|
2101
2207
|
gameStateUsed: await detectGameStateUsage(cwd),
|
|
2102
2208
|
uiPhases: await detectUiPhases(cwd),
|
|
2103
|
-
surfaces: await detectSurfaceScan(cwd)
|
|
2209
|
+
surfaces: await detectSurfaceScan(cwd),
|
|
2210
|
+
generations: await detectGenerationAudit(cwd)
|
|
2104
2211
|
};
|
|
2105
2212
|
}
|
|
2106
2213
|
function advisoryNudges(log, d) {
|
|
@@ -2125,6 +2232,20 @@ function advisoryNudges(log, d) {
|
|
|
2125
2232
|
);
|
|
2126
2233
|
}
|
|
2127
2234
|
surfaceNudges(log, d.surfaces);
|
|
2235
|
+
generationNudges(log, d.generations);
|
|
2236
|
+
}
|
|
2237
|
+
function generationNudges(log, g) {
|
|
2238
|
+
const describe = (refs) => refs.map((r) => `${r.kind} ${r.id} ("${r.prompt.slice(0, 48)}")`).join("; ");
|
|
2239
|
+
if (g.unpicked.length) {
|
|
2240
|
+
log.warn(
|
|
2241
|
+
`${g.unpicked.length} generation(s) enqueued 15+ minutes ago were never picked up: ${describe(g.unpicked)}. Run npx genex wait <id> on each \u2014 landed assets get wired in before the last push of a session, never parked for "later".`
|
|
2242
|
+
);
|
|
2243
|
+
}
|
|
2244
|
+
if (g.unwired.length) {
|
|
2245
|
+
log.warn(
|
|
2246
|
+
`${g.unwired.length} finished generation(s) are wired nowhere in the game: ${describe(g.unwired)}. Each was paid for \u2014 wire it in (its URL embeds the id) or tell the user in one plain line why it stays unused.`
|
|
2247
|
+
);
|
|
2248
|
+
}
|
|
2128
2249
|
}
|
|
2129
2250
|
function surfaceNudges(log, s) {
|
|
2130
2251
|
if (s.guessedRepeat.length) {
|
|
@@ -2518,6 +2639,7 @@ async function runGenerate(kind, opts) {
|
|
|
2518
2639
|
process.exitCode = 1;
|
|
2519
2640
|
return;
|
|
2520
2641
|
}
|
|
2642
|
+
await recordQueued(id, kind, prompt);
|
|
2521
2643
|
if (opts.noWait) {
|
|
2522
2644
|
log.success(`Queued (${id}).`);
|
|
2523
2645
|
log.plain(` Pick it up any time with: ${c.cyan(`npx genex wait ${id}`)}`);
|
|
@@ -2544,6 +2666,13 @@ async function awaitAndReport(apiUrl, token, id, kind, log, open = false) {
|
|
|
2544
2666
|
async function reportTerminal(kind, view, log, open = false) {
|
|
2545
2667
|
if (view.status !== "completed") {
|
|
2546
2668
|
log.error(`Generation ${view.status}${view.error ? `: ${view.error}` : ""}.`);
|
|
2669
|
+
if (view.status === "failed") {
|
|
2670
|
+
await recordTerminal(view.id, "failed");
|
|
2671
|
+
if (kind === "video") {
|
|
2672
|
+
const failures = await countFailed("video");
|
|
2673
|
+
(failures >= 2 ? log.plain : log.dim)(videoFailureAdvice(failures));
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2547
2676
|
process.exitCode = 1;
|
|
2548
2677
|
return;
|
|
2549
2678
|
}
|
|
@@ -2553,6 +2682,7 @@ async function reportTerminal(kind, view, log, open = false) {
|
|
|
2553
2682
|
process.exitCode = 1;
|
|
2554
2683
|
return;
|
|
2555
2684
|
}
|
|
2685
|
+
await recordTerminal(view.id, "completed", files.map((f) => f.url));
|
|
2556
2686
|
log.plain("");
|
|
2557
2687
|
log.success("Done \u2014 live on R2 (nothing downloaded or committed):");
|
|
2558
2688
|
for (const f of files) {
|
|
@@ -2572,6 +2702,12 @@ async function reportTerminal(kind, view, log, open = false) {
|
|
|
2572
2702
|
printHint(kind, view, files, log);
|
|
2573
2703
|
await firstPreviewNudge(log);
|
|
2574
2704
|
}
|
|
2705
|
+
function videoFailureAdvice(failures) {
|
|
2706
|
+
if (failures >= 2) {
|
|
2707
|
+
return ` ${c.bold(`\u270B ${failures} failed videos in this project \u2014 don't queue another attempt of the same clip.`)} One retry per clip is the budget. Use its key-art still as the backdrop instead (the genex-ai-menu skill's built-in fallback; a slow CSS pan gives it life), tell the user in one plain line, and spend the minutes in the game.`;
|
|
2708
|
+
}
|
|
2709
|
+
return " Video renders fail server-side sometimes. One retry is fair \u2014 shorter clip (4\u20136 s), simpler motion. After a second failure, switch to the key-art still (genex-ai-menu's fallback) instead of a third attempt.";
|
|
2710
|
+
}
|
|
2575
2711
|
var WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
2576
2712
|
var KIND_WAIT_TIMEOUT_MS = {
|
|
2577
2713
|
video: 15 * 60 * 1e3,
|
|
@@ -2752,8 +2888,8 @@ async function runWait(opts) {
|
|
|
2752
2888
|
}
|
|
2753
2889
|
|
|
2754
2890
|
// src/commands/controller.ts
|
|
2755
|
-
import
|
|
2756
|
-
import
|
|
2891
|
+
import fs13 from "fs/promises";
|
|
2892
|
+
import path14 from "path";
|
|
2757
2893
|
|
|
2758
2894
|
// ../../packages/meshy-animation-catalog/src/index.ts
|
|
2759
2895
|
import { createHash } from "crypto";
|
|
@@ -11883,9 +12019,9 @@ function searchMeshyAnimations(query, options = {}) {
|
|
|
11883
12019
|
}
|
|
11884
12020
|
|
|
11885
12021
|
// src/lib/anims.ts
|
|
11886
|
-
import
|
|
11887
|
-
import
|
|
11888
|
-
var ANIMS_DEST =
|
|
12022
|
+
import fs12 from "fs/promises";
|
|
12023
|
+
import path13 from "path";
|
|
12024
|
+
var ANIMS_DEST = path13.join("public", "assets", "anims");
|
|
11889
12025
|
var HIDDEN_TAG = "reference";
|
|
11890
12026
|
async function runAnims(opts) {
|
|
11891
12027
|
const log = createLogger({ quiet: opts.quiet });
|
|
@@ -11901,7 +12037,7 @@ async function runAnims(opts) {
|
|
|
11901
12037
|
printCatalog(log, manifest, selectors);
|
|
11902
12038
|
return;
|
|
11903
12039
|
}
|
|
11904
|
-
const controllerMarker =
|
|
12040
|
+
const controllerMarker = path13.join(root, "src", "controllers", "character");
|
|
11905
12041
|
if (!await exists2(controllerMarker)) {
|
|
11906
12042
|
log.error(
|
|
11907
12043
|
`No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
|
|
@@ -11910,11 +12046,11 @@ async function runAnims(opts) {
|
|
|
11910
12046
|
process.exitCode = 1;
|
|
11911
12047
|
return;
|
|
11912
12048
|
}
|
|
11913
|
-
const destDir =
|
|
11914
|
-
const gameManifestPath =
|
|
12049
|
+
const destDir = path13.join(root, ANIMS_DEST);
|
|
12050
|
+
const gameManifestPath = path13.join(destDir, "manifest.json");
|
|
11915
12051
|
if (opts.reset) {
|
|
11916
|
-
await
|
|
11917
|
-
log.step(`Cleared ${c.cyan(ANIMS_DEST +
|
|
12052
|
+
await fs12.rm(destDir, { recursive: true, force: true });
|
|
12053
|
+
log.step(`Cleared ${c.cyan(ANIMS_DEST + path13.sep)} (--reset)`);
|
|
11918
12054
|
}
|
|
11919
12055
|
if (selectors.length === 0) {
|
|
11920
12056
|
const installed = await readGameManifest(gameManifestPath);
|
|
@@ -11952,35 +12088,35 @@ async function runAnims(opts) {
|
|
|
11952
12088
|
}
|
|
11953
12089
|
}
|
|
11954
12090
|
const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
11955
|
-
const cacheDir =
|
|
12091
|
+
const cacheDir = path13.join(
|
|
11956
12092
|
opts.cacheDir ?? getAnimsCacheDir(),
|
|
11957
12093
|
`${manifest.library}-v${manifest.version}`
|
|
11958
12094
|
);
|
|
11959
|
-
await
|
|
11960
|
-
await
|
|
12095
|
+
await fs12.mkdir(cacheDir, { recursive: true });
|
|
12096
|
+
await fs12.mkdir(destDir, { recursive: true });
|
|
11961
12097
|
const base = getAnimsBase(opts.animsBase);
|
|
11962
12098
|
let installedCount = 0;
|
|
11963
12099
|
let presentCount = 0;
|
|
11964
12100
|
let addedBytes = 0;
|
|
11965
12101
|
const failures = [];
|
|
11966
12102
|
for (const entry of wanted) {
|
|
11967
|
-
const dest =
|
|
12103
|
+
const dest = path13.join(destDir, entry.file);
|
|
11968
12104
|
if (await hasSize(dest, entry.bytes)) {
|
|
11969
12105
|
presentCount++;
|
|
11970
12106
|
continue;
|
|
11971
12107
|
}
|
|
11972
12108
|
try {
|
|
11973
|
-
const cached =
|
|
12109
|
+
const cached = path13.join(cacheDir, entry.file);
|
|
11974
12110
|
if (!await hasSize(cached, entry.bytes)) {
|
|
11975
12111
|
const res = await fetch(base + entry.file);
|
|
11976
12112
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
11977
12113
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
11978
|
-
await
|
|
12114
|
+
await fs12.writeFile(cached, buf);
|
|
11979
12115
|
}
|
|
11980
|
-
await
|
|
12116
|
+
await fs12.copyFile(cached, dest);
|
|
11981
12117
|
installedCount++;
|
|
11982
12118
|
addedBytes += entry.bytes;
|
|
11983
|
-
log.dim(` ${
|
|
12119
|
+
log.dim(` ${path13.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
|
|
11984
12120
|
} catch (err) {
|
|
11985
12121
|
failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
|
|
11986
12122
|
}
|
|
@@ -11996,13 +12132,13 @@ async function runAnims(opts) {
|
|
|
11996
12132
|
version: manifest.version,
|
|
11997
12133
|
clips: [...union].sort((a, b) => a.localeCompare(b))
|
|
11998
12134
|
};
|
|
11999
|
-
await
|
|
12135
|
+
await fs12.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
|
|
12000
12136
|
log.plain("");
|
|
12001
12137
|
const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
|
|
12002
12138
|
if (presentCount > 0) parts.push(`${presentCount} already present`);
|
|
12003
12139
|
if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
|
|
12004
12140
|
log.success(
|
|
12005
|
-
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST +
|
|
12141
|
+
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path13.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
|
|
12006
12142
|
);
|
|
12007
12143
|
for (const [selector, entries] of resolved) {
|
|
12008
12144
|
const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
|
|
@@ -12034,8 +12170,8 @@ async function loadManifest(baseOverride) {
|
|
|
12034
12170
|
}
|
|
12035
12171
|
} catch {
|
|
12036
12172
|
}
|
|
12037
|
-
const snapshotPath =
|
|
12038
|
-
const manifest = JSON.parse(await
|
|
12173
|
+
const snapshotPath = path13.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
|
|
12174
|
+
const manifest = JSON.parse(await fs12.readFile(snapshotPath, "utf8"));
|
|
12039
12175
|
return { manifest, source: "snapshot" };
|
|
12040
12176
|
}
|
|
12041
12177
|
function resolveSelectors(manifest, selectors) {
|
|
@@ -12153,21 +12289,21 @@ function printCatalog(log, manifest, selectors) {
|
|
|
12153
12289
|
}
|
|
12154
12290
|
async function readGameManifest(file) {
|
|
12155
12291
|
try {
|
|
12156
|
-
return JSON.parse(await
|
|
12292
|
+
return JSON.parse(await fs12.readFile(file, "utf8"));
|
|
12157
12293
|
} catch {
|
|
12158
12294
|
return null;
|
|
12159
12295
|
}
|
|
12160
12296
|
}
|
|
12161
12297
|
async function hasSize(file, bytes) {
|
|
12162
12298
|
try {
|
|
12163
|
-
return (await
|
|
12299
|
+
return (await fs12.stat(file)).size === bytes;
|
|
12164
12300
|
} catch {
|
|
12165
12301
|
return false;
|
|
12166
12302
|
}
|
|
12167
12303
|
}
|
|
12168
12304
|
async function exists2(p) {
|
|
12169
12305
|
try {
|
|
12170
|
-
await
|
|
12306
|
+
await fs12.access(p);
|
|
12171
12307
|
return true;
|
|
12172
12308
|
} catch {
|
|
12173
12309
|
return false;
|
|
@@ -12300,8 +12436,8 @@ var CONTROLLER_FILE_SETS = {
|
|
|
12300
12436
|
]
|
|
12301
12437
|
}
|
|
12302
12438
|
};
|
|
12303
|
-
var CODE_DEST =
|
|
12304
|
-
var ASSETS_DEST =
|
|
12439
|
+
var CODE_DEST = path14.join("src", "controllers");
|
|
12440
|
+
var ASSETS_DEST = path14.join("public", "assets");
|
|
12305
12441
|
async function runController(opts) {
|
|
12306
12442
|
const log = createLogger({ quiet: opts.quiet });
|
|
12307
12443
|
if (opts.kind?.trim() === "anims") {
|
|
@@ -12318,31 +12454,31 @@ async function runController(opts) {
|
|
|
12318
12454
|
process.exitCode = 1;
|
|
12319
12455
|
return;
|
|
12320
12456
|
}
|
|
12321
|
-
const srcDir =
|
|
12457
|
+
const srcDir = path14.join(getTemplatesDir(), "controllers");
|
|
12322
12458
|
const root = opts.cwd ?? process.cwd();
|
|
12323
12459
|
const set = CONTROLLER_FILE_SETS[kind];
|
|
12324
12460
|
log.plain(c.bold(`genex controller ${kind}`));
|
|
12325
12461
|
log.plain("");
|
|
12326
|
-
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST +
|
|
12462
|
+
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path14.sep)}`);
|
|
12327
12463
|
const plan = [
|
|
12328
|
-
...set.code.map((rel) => ({ from: rel, rel:
|
|
12464
|
+
...set.code.map((rel) => ({ from: rel, rel: path14.join(CODE_DEST, rel) })),
|
|
12329
12465
|
...set.assets.map((rel) => ({
|
|
12330
12466
|
from: rel,
|
|
12331
|
-
rel:
|
|
12467
|
+
rel: path14.join(ASSETS_DEST, path14.basename(rel))
|
|
12332
12468
|
}))
|
|
12333
12469
|
];
|
|
12334
12470
|
let copied = 0;
|
|
12335
12471
|
let skipped = 0;
|
|
12336
12472
|
try {
|
|
12337
12473
|
for (const file of plan) {
|
|
12338
|
-
const dest =
|
|
12474
|
+
const dest = path14.join(root, file.rel);
|
|
12339
12475
|
if (!opts.force && await exists3(dest)) {
|
|
12340
12476
|
skipped++;
|
|
12341
12477
|
log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
|
|
12342
12478
|
continue;
|
|
12343
12479
|
}
|
|
12344
|
-
await
|
|
12345
|
-
await
|
|
12480
|
+
await fs13.mkdir(path14.dirname(dest), { recursive: true });
|
|
12481
|
+
await fs13.copyFile(path14.join(srcDir, file.from), dest);
|
|
12346
12482
|
copied++;
|
|
12347
12483
|
log.dim(` ${file.rel}`);
|
|
12348
12484
|
}
|
|
@@ -12427,9 +12563,9 @@ async function installMeshyCharacterManifest(args) {
|
|
|
12427
12563
|
throw new Error("The API returned an invalid Meshy character manifest.");
|
|
12428
12564
|
}
|
|
12429
12565
|
assertCompleteMeshyControllerPack(manifest);
|
|
12430
|
-
const destination =
|
|
12431
|
-
await
|
|
12432
|
-
await
|
|
12566
|
+
const destination = path14.join(args.root, ASSETS_DEST, "meshy-character.json");
|
|
12567
|
+
await fs13.mkdir(path14.dirname(destination), { recursive: true });
|
|
12568
|
+
await fs13.writeFile(
|
|
12433
12569
|
destination,
|
|
12434
12570
|
`${JSON.stringify(manifest, null, 2)}
|
|
12435
12571
|
`
|
|
@@ -12550,8 +12686,8 @@ function assertCompleteMeshyControllerPack(manifest) {
|
|
|
12550
12686
|
}
|
|
12551
12687
|
async function installOwnerAvatar(args) {
|
|
12552
12688
|
const { root, srcDir, apiUrl, token, log } = args;
|
|
12553
|
-
const dest =
|
|
12554
|
-
await
|
|
12689
|
+
const dest = path14.join(root, ASSETS_DEST, "avatar.vrm");
|
|
12690
|
+
await fs13.mkdir(path14.dirname(dest), { recursive: true });
|
|
12555
12691
|
if (token) {
|
|
12556
12692
|
try {
|
|
12557
12693
|
const res = await apiFetch(`${apiUrl}/api/avatars/me`, {
|
|
@@ -12563,7 +12699,7 @@ async function installOwnerAvatar(args) {
|
|
|
12563
12699
|
const vrmRes = await fetch(me.vrmUrl);
|
|
12564
12700
|
if (vrmRes.ok) {
|
|
12565
12701
|
const buf = Buffer.from(await vrmRes.arrayBuffer());
|
|
12566
|
-
await
|
|
12702
|
+
await fs13.writeFile(dest, buf);
|
|
12567
12703
|
log.dim(` public/assets/avatar.vrm (your avatar \u2014 ${(buf.length / 1e6).toFixed(1)} MB)`);
|
|
12568
12704
|
return;
|
|
12569
12705
|
}
|
|
@@ -12574,14 +12710,14 @@ async function installOwnerAvatar(args) {
|
|
|
12574
12710
|
log.dim(" avatar fetch failed (offline?); using the bundled default.");
|
|
12575
12711
|
}
|
|
12576
12712
|
}
|
|
12577
|
-
await
|
|
12713
|
+
await fs13.copyFile(path14.join(srcDir, "assets", "default-avatar.vrm"), dest);
|
|
12578
12714
|
log.dim(
|
|
12579
12715
|
token ? " public/assets/avatar.vrm (bundled default)" : " public/assets/avatar.vrm (bundled default \u2014 sign in and re-run for your own)"
|
|
12580
12716
|
);
|
|
12581
12717
|
}
|
|
12582
12718
|
async function exists3(p) {
|
|
12583
12719
|
try {
|
|
12584
|
-
await
|
|
12720
|
+
await fs13.access(p);
|
|
12585
12721
|
return true;
|
|
12586
12722
|
} catch {
|
|
12587
12723
|
return false;
|
|
@@ -12866,12 +13002,12 @@ function rank(items, query) {
|
|
|
12866
13002
|
}
|
|
12867
13003
|
|
|
12868
13004
|
// src/commands/ui.ts
|
|
12869
|
-
import
|
|
12870
|
-
import
|
|
13005
|
+
import fs15 from "fs/promises";
|
|
13006
|
+
import path15 from "path";
|
|
12871
13007
|
import { PNG as PNG3 } from "pngjs";
|
|
12872
13008
|
|
|
12873
13009
|
// src/lib/png-tools.ts
|
|
12874
|
-
import
|
|
13010
|
+
import fs14 from "fs/promises";
|
|
12875
13011
|
import { PNG as PNG2 } from "pngjs";
|
|
12876
13012
|
var ALPHA_TRANSPARENT_MAX = 16;
|
|
12877
13013
|
var isHttpUrl = (s) => /^https?:\/\//i.test(s);
|
|
@@ -12882,12 +13018,12 @@ async function loadPng(input) {
|
|
|
12882
13018
|
if (!res.ok) throw new Error(`Couldn't fetch ${input} (HTTP ${res.status}).`);
|
|
12883
13019
|
buf = Buffer.from(await res.arrayBuffer());
|
|
12884
13020
|
} else {
|
|
12885
|
-
buf = await
|
|
13021
|
+
buf = await fs14.readFile(input);
|
|
12886
13022
|
}
|
|
12887
13023
|
return PNG2.sync.read(buf);
|
|
12888
13024
|
}
|
|
12889
13025
|
async function writePng(file, png) {
|
|
12890
|
-
await
|
|
13026
|
+
await fs14.writeFile(file, PNG2.sync.write(png));
|
|
12891
13027
|
}
|
|
12892
13028
|
function cropPng(image, box) {
|
|
12893
13029
|
const out = new PNG2({ width: box.w, height: box.h });
|
|
@@ -13093,7 +13229,7 @@ async function uiExtract(opts, log) {
|
|
|
13093
13229
|
const dilatePx = opts.dilate ?? 0;
|
|
13094
13230
|
const sheet = await loadPng(input);
|
|
13095
13231
|
const { width: W, height: H, data } = sheet;
|
|
13096
|
-
await
|
|
13232
|
+
await fs15.mkdir(outDir, { recursive: true });
|
|
13097
13233
|
log.plain(c.bold("genex ui extract"));
|
|
13098
13234
|
log.dim(` ${input} (${W}x${H}) \u2192 ${outDir}, ${names.length} names`);
|
|
13099
13235
|
let hasTransparency = false;
|
|
@@ -13252,7 +13388,7 @@ async function uiExtract(opts, log) {
|
|
|
13252
13388
|
}
|
|
13253
13389
|
}
|
|
13254
13390
|
}
|
|
13255
|
-
const outPath =
|
|
13391
|
+
const outPath = path15.join(outDir, `${name}.png`);
|
|
13256
13392
|
await writePng(outPath, out);
|
|
13257
13393
|
const sidecar = {
|
|
13258
13394
|
name,
|
|
@@ -13270,7 +13406,7 @@ async function uiExtract(opts, log) {
|
|
|
13270
13406
|
componentPixels: comp.pixels
|
|
13271
13407
|
};
|
|
13272
13408
|
const { name: _n, out: _o, ...sidecarBody } = sidecar;
|
|
13273
|
-
await
|
|
13409
|
+
await fs15.writeFile(
|
|
13274
13410
|
outPath.replace(/\.png$/i, "") + ".bbox.json",
|
|
13275
13411
|
JSON.stringify(sidecarBody, null, 2)
|
|
13276
13412
|
);
|
|
@@ -13279,8 +13415,8 @@ async function uiExtract(opts, log) {
|
|
|
13279
13415
|
`${name}.png ${cropW}x${cropH} (ar ${sidecar.aspectRatio}) at sheet ${padX0},${padY0}`
|
|
13280
13416
|
);
|
|
13281
13417
|
}
|
|
13282
|
-
const debugPath =
|
|
13283
|
-
await
|
|
13418
|
+
const debugPath = path15.join(outDir, "extract-debug.json");
|
|
13419
|
+
await fs15.writeFile(
|
|
13284
13420
|
debugPath,
|
|
13285
13421
|
JSON.stringify(
|
|
13286
13422
|
{
|
|
@@ -13514,7 +13650,7 @@ async function uiMasks(opts, log) {
|
|
|
13514
13650
|
const registrationTolerance = opts.registrationTolerance ?? 0.04;
|
|
13515
13651
|
const edgeFlushMax = opts.edgeFlushMax ?? 0.04;
|
|
13516
13652
|
const image = await loadPng(input);
|
|
13517
|
-
await
|
|
13653
|
+
await fs15.mkdir(outDir, { recursive: true });
|
|
13518
13654
|
log.plain(c.bold("genex ui masks"));
|
|
13519
13655
|
log.dim(` ${input} (${image.width}x${image.height}), ${pairs.length} pair(s) \u2192 ${outDir}`);
|
|
13520
13656
|
const results = [];
|
|
@@ -13557,11 +13693,11 @@ async function uiMasks(opts, log) {
|
|
|
13557
13693
|
});
|
|
13558
13694
|
}
|
|
13559
13695
|
const overlay = makeOverlay(clean2, converted.png);
|
|
13560
|
-
const framePath =
|
|
13561
|
-
const maskPath =
|
|
13562
|
-
const annotatedPath =
|
|
13563
|
-
const overlayPath =
|
|
13564
|
-
const metaPath =
|
|
13696
|
+
const framePath = path15.join(outDir, `${pair.name}-frame.png`);
|
|
13697
|
+
const maskPath = path15.join(outDir, `${pair.name}-mask.png`);
|
|
13698
|
+
const annotatedPath = path15.join(outDir, `${pair.name}-annotated-source.png`);
|
|
13699
|
+
const overlayPath = path15.join(outDir, `${pair.name}-overlay.png`);
|
|
13700
|
+
const metaPath = path15.join(outDir, `${pair.name}.annotated-progress.json`);
|
|
13565
13701
|
await writePng(framePath, clean2);
|
|
13566
13702
|
await writePng(maskPath, converted.png);
|
|
13567
13703
|
await writePng(annotatedPath, annotated);
|
|
@@ -13600,7 +13736,7 @@ async function uiMasks(opts, log) {
|
|
|
13600
13736
|
},
|
|
13601
13737
|
overlay: overlayPath
|
|
13602
13738
|
};
|
|
13603
|
-
await
|
|
13739
|
+
await fs15.writeFile(metaPath, `${JSON.stringify(meta, null, 2)}
|
|
13604
13740
|
`);
|
|
13605
13741
|
results.push(meta);
|
|
13606
13742
|
const fb = converted.bbox;
|
|
@@ -13609,8 +13745,8 @@ async function uiMasks(opts, log) {
|
|
|
13609
13745
|
`${pair.name}: coverage ${meta.mask.coverage},${fbText} ${components.length} component(s), registration \u0394 ${meta.registration.bboxDelta}`
|
|
13610
13746
|
);
|
|
13611
13747
|
}
|
|
13612
|
-
const indexPath =
|
|
13613
|
-
await
|
|
13748
|
+
const indexPath = path15.join(outDir, "annotated-progress.json");
|
|
13749
|
+
await fs15.writeFile(indexPath, `${JSON.stringify({ input, pairs: results }, null, 2)}
|
|
13614
13750
|
`);
|
|
13615
13751
|
log.plain("");
|
|
13616
13752
|
log.success(`Wrote ${pairs.length} frame+mask pair(s) \u2192 ${outDir} (index: ${indexPath}).`);
|
|
@@ -13731,7 +13867,7 @@ async function uiTextColor(opts, log) {
|
|
|
13731
13867
|
};
|
|
13732
13868
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
13733
13869
|
`);
|
|
13734
|
-
if (opts.out) await
|
|
13870
|
+
if (opts.out) await fs15.writeFile(opts.out, `${JSON.stringify(result, null, 2)}
|
|
13735
13871
|
`);
|
|
13736
13872
|
if (opts.cropPath) log.dim(` crop \u2192 ${opts.cropPath}`);
|
|
13737
13873
|
}
|
|
@@ -13754,7 +13890,7 @@ async function uiTrim(opts, log) {
|
|
|
13754
13890
|
await writePng(outPath, trimmed);
|
|
13755
13891
|
const sidecar = computeBBoxes(trimmed);
|
|
13756
13892
|
const sidecarPath = outPath.replace(/\.png$/i, "") + ".bbox.json";
|
|
13757
|
-
await
|
|
13893
|
+
await fs15.writeFile(sidecarPath, JSON.stringify(sidecar, null, 2));
|
|
13758
13894
|
log.success(
|
|
13759
13895
|
`Trimmed ${png.width}x${png.height} \u2192 ${trimmed.width}x${trimmed.height} (${outPath}).`
|
|
13760
13896
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genex-ai/cli-demo",
|
|
3
|
-
"version": "0.62.0-dev.
|
|
3
|
+
"version": "0.62.0-dev.144",
|
|
4
4
|
"description": "Set up your project's agent workspace (.claude/.codex/.cursor in the game folder), authorize, create a game project, generate AI assets, and publish (genex CLI).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -126,6 +126,19 @@ after the UI plan gate, enqueue the video with `--no-wait`, ship the CSS menu
|
|
|
126
126
|
(R2) — permanent, public, CORS-open; you load them straight from the printed
|
|
127
127
|
URLs, nothing is downloaded or committed.
|
|
128
128
|
|
|
129
|
+
**Two failed video attempts = ship the still. Hard stop.** Video is the one
|
|
130
|
+
generation that fails server-side with real frequency (render timeouts), and
|
|
131
|
+
every attempt costs minutes of waiting. One retry is fair — shorten the clip
|
|
132
|
+
(4–6 s) and simplify the motion prompt. After a SECOND failure, stop
|
|
133
|
+
generating: keep the key-art still as the menu background and give it life
|
|
134
|
+
for free with a slow CSS pan/zoom (`transform: scale(1.06)` over ~20 s,
|
|
135
|
+
alternating), tell the user in one plain line ("the animated menu backdrop
|
|
136
|
+
kept failing, so your menu uses the key art — looks great, costs nothing"),
|
|
137
|
+
and spend those minutes in the game. A third attempt is how half an hour
|
|
138
|
+
disappears into chrome — the CLI counts failures and reminds you at the
|
|
139
|
+
second one. The still-image menu is a real menu: this rule is the built-in
|
|
140
|
+
fallback, not a downgrade to apologize for.
|
|
141
|
+
|
|
129
142
|
## Wire it as a phase screen
|
|
130
143
|
|
|
131
144
|
The menu is one `data-phase` screen in the `$genex-threejs-game-ui`
|