@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 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 fs10.readFile(path11.join(cwd, "package.json"), "utf8");
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 fs10.readFile(path11.join(cwd, "package.json"), "utf8");
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 fs10.readFile(path11.join(cwd, "package.json"), "utf8"));
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 = path11.join(cwd, "src");
2033
+ const srcDir = path12.join(cwd, "src");
1976
2034
  let entries;
1977
2035
  try {
1978
- entries = await fs10.readdir(srcDir, { recursive: true });
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 fs10.readFile(path11.join(srcDir, rel), "utf8");
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 fs10.readFile(path11.join(cwd, "index.html"), "utf8").catch(() => "");
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 = path11.join(cwd, "src");
1999
- const entries = await fs10.readdir(srcDir, { recursive: true });
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 fs10.readFile(path11.join(srcDir, rel), "utf8").catch(() => "");
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 = path11.join(cwd, "src");
2106
+ const srcDir = path12.join(cwd, "src");
2049
2107
  let entries;
2050
2108
  try {
2051
- entries = await fs10.readdir(srcDir, { recursive: true });
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 fs10.readFile(path11.join(srcDir, rel), "utf8").catch(() => "");
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 fs12 from "fs/promises";
2756
- import path13 from "path";
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 fs11 from "fs/promises";
11887
- import path12 from "path";
11888
- var ANIMS_DEST = path12.join("public", "assets", "anims");
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 = path12.join(root, "src", "controllers", "character");
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 = path12.join(root, ANIMS_DEST);
11914
- const gameManifestPath = path12.join(destDir, "manifest.json");
12049
+ const destDir = path13.join(root, ANIMS_DEST);
12050
+ const gameManifestPath = path13.join(destDir, "manifest.json");
11915
12051
  if (opts.reset) {
11916
- await fs11.rm(destDir, { recursive: true, force: true });
11917
- log.step(`Cleared ${c.cyan(ANIMS_DEST + path12.sep)} (--reset)`);
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 = path12.join(
12091
+ const cacheDir = path13.join(
11956
12092
  opts.cacheDir ?? getAnimsCacheDir(),
11957
12093
  `${manifest.library}-v${manifest.version}`
11958
12094
  );
11959
- await fs11.mkdir(cacheDir, { recursive: true });
11960
- await fs11.mkdir(destDir, { recursive: true });
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 = path12.join(destDir, entry.file);
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 = path12.join(cacheDir, entry.file);
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 fs11.writeFile(cached, buf);
12114
+ await fs12.writeFile(cached, buf);
11979
12115
  }
11980
- await fs11.copyFile(cached, dest);
12116
+ await fs12.copyFile(cached, dest);
11981
12117
  installedCount++;
11982
12118
  addedBytes += entry.bytes;
11983
- log.dim(` ${path12.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
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 fs11.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
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 + path12.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
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 = path12.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
12038
- const manifest = JSON.parse(await fs11.readFile(snapshotPath, "utf8"));
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 fs11.readFile(file, "utf8"));
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 fs11.stat(file)).size === bytes;
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 fs11.access(p);
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 = path13.join("src", "controllers");
12304
- var ASSETS_DEST = path13.join("public", "assets");
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 = path13.join(getTemplatesDir(), "controllers");
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 + path13.sep)}`);
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: path13.join(CODE_DEST, 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: path13.join(ASSETS_DEST, path13.basename(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 = path13.join(root, file.rel);
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 fs12.mkdir(path13.dirname(dest), { recursive: true });
12345
- await fs12.copyFile(path13.join(srcDir, file.from), dest);
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 = path13.join(args.root, ASSETS_DEST, "meshy-character.json");
12431
- await fs12.mkdir(path13.dirname(destination), { recursive: true });
12432
- await fs12.writeFile(
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 = path13.join(root, ASSETS_DEST, "avatar.vrm");
12554
- await fs12.mkdir(path13.dirname(dest), { recursive: true });
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 fs12.writeFile(dest, buf);
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 fs12.copyFile(path13.join(srcDir, "assets", "default-avatar.vrm"), dest);
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 fs12.access(p);
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 fs14 from "fs/promises";
12870
- import path14 from "path";
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 fs13 from "fs/promises";
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 fs13.readFile(input);
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 fs13.writeFile(file, PNG2.sync.write(png));
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 fs14.mkdir(outDir, { recursive: true });
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 = path14.join(outDir, `${name}.png`);
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 fs14.writeFile(
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 = path14.join(outDir, "extract-debug.json");
13283
- await fs14.writeFile(
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 fs14.mkdir(outDir, { recursive: true });
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 = path14.join(outDir, `${pair.name}-frame.png`);
13561
- const maskPath = path14.join(outDir, `${pair.name}-mask.png`);
13562
- const annotatedPath = path14.join(outDir, `${pair.name}-annotated-source.png`);
13563
- const overlayPath = path14.join(outDir, `${pair.name}-overlay.png`);
13564
- const metaPath = path14.join(outDir, `${pair.name}.annotated-progress.json`);
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 fs14.writeFile(metaPath, `${JSON.stringify(meta, null, 2)}
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 = path14.join(outDir, "annotated-progress.json");
13613
- await fs14.writeFile(indexPath, `${JSON.stringify({ input, pairs: results }, null, 2)}
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 fs14.writeFile(opts.out, `${JSON.stringify(result, null, 2)}
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 fs14.writeFile(sidecarPath, JSON.stringify(sidecar, null, 2));
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.143",
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`