@genex-ai/cli-demo 1.35.4-dev.768 → 1.36.0-dev.770

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
@@ -2,6 +2,7 @@
2
2
  import {
3
3
  ASSETS_MODE_ENV,
4
4
  RENDER_MODES,
5
+ SCRATCH_DIR,
5
6
  SHEET_FORMATS,
6
7
  acceptUrl,
7
8
  acquireCliSeat,
@@ -15,6 +16,7 @@ import {
15
16
  excludeFile,
16
17
  fetchCloneGrant,
17
18
  fetchSignedInEmail,
19
+ hostedBrowserCovers,
18
20
  inHostedSession,
19
21
  isRenderMode,
20
22
  isStale,
@@ -42,7 +44,7 @@ import {
42
44
  writeSecretFile,
43
45
  writeUserToken,
44
46
  writeWorkspace
45
- } from "./chunk-2SCXGPZY.js";
47
+ } from "./chunk-Y6WO5FJZ.js";
46
48
  import {
47
49
  CLI_CHANNEL,
48
50
  DEFAULT_API_URL,
@@ -418,7 +420,7 @@ async function authorizeLoopback(authBaseUrl, options) {
418
420
  cleanup();
419
421
  resolve(token);
420
422
  };
421
- const fail4 = (err) => {
423
+ const fail5 = (err) => {
422
424
  if (settled) return;
423
425
  settled = true;
424
426
  cleanup();
@@ -435,7 +437,7 @@ async function authorizeLoopback(authBaseUrl, options) {
435
437
  }
436
438
  });
437
439
  });
438
- server.on("error", (err) => fail4(err));
440
+ server.on("error", (err) => fail5(err));
439
441
  server.listen(0, "127.0.0.1", () => {
440
442
  const { port } = server.address();
441
443
  const redirectUri = `http://127.0.0.1:${port}/callback`;
@@ -466,7 +468,7 @@ async function authorizeLoopback(authBaseUrl, options) {
466
468
  );
467
469
  }
468
470
  timer = setTimeout(() => {
469
- fail4(
471
+ fail5(
470
472
  new Error(
471
473
  "Timed out waiting for authorization. Re-run the command to try again."
472
474
  )
@@ -689,9 +691,9 @@ async function runAuth(opts) {
689
691
  }
690
692
 
691
693
  // src/commands/remix.ts
692
- import fs18 from "fs/promises";
693
- import path18 from "path";
694
- import crypto4 from "crypto";
694
+ import fs19 from "fs/promises";
695
+ import path19 from "path";
696
+ import crypto5 from "crypto";
695
697
  import { spawn as spawn4 } from "child_process";
696
698
 
697
699
  // src/lib/game-config.ts
@@ -889,7 +891,7 @@ Important note: put soul into your creations, with many details and love. Aim to
889
891
  8. Never park ready work behind a question, and never stall on an unanswered one \u2014 decide, state the decision in chat, record it, keep building.
890
892
  9. Before any publish and before ending a session: run \`npx genex wait\` on every generation you enqueued and wire in what landed \u2014 never park landed assets. Fonts the brief names are LOADED for real, Escape pauses, the loader shows something of the game rather than a black screen (the renderer draws before any \`await waitForPlayer()\` \u2014 identity takes seconds on a hosted page), and the player wears the game's own generated character (or DESIGN.md records why it doesn't).
891
893
  10. The player's body is the game's own generated character (\`npx genex character "<look>"\` \u2192 \`npx genex controller character --character <id>\`), enqueued with your first art actions, not after them. It applies wherever a human body appears on screen \u2014 first-person included, the moment remotes, a look-down body, a shadow, or a menu portrait shows one. Games whose player is not a person (car, ship, RTS cursor, board) generate that object with \`npx genex model\` instead, or build it procedurally \u2014 held to law 22's bar like everything else the player looks at. Characters: Meshy/Mixamo/VRM rigs rest facing +Z. Set yaw explicitly when placing a rig; never mirror a SkinnedMesh with negative scale. In any two-character scene, verify in a capture that they face each other, not the camera.
892
- 11. Verify by looking: one smoke check per milestone, after that milestone's preview push, in local test mode (\`?genex_local_test=1\`) with a real gameplay screenshot. A claim without a capture is not verification. Local-test evidence proves visuals and controls ONLY \u2014 label it that way when you show the player, and never work around the draft sign-in gate any other way.
894
+ 11. Verify by looking: one smoke check per milestone, after that milestone's preview push, in local test mode (\`?genex_local_test=1\`) with a real gameplay screenshot. A claim without a capture is not verification. Local-test evidence proves visuals and controls ONLY \u2014 label it that way when you show the player, and never work around the draft sign-in gate any other way. These captures are also where the game's cover comes from: when one catches the game's signature moment, keep that frame as its cover (law 23).
893
895
  12. Treat every \`genex\` warning line \u2014 preflight, \`ui audit\`, \`wait\` nudges \u2014 as work, not noise.
894
896
  13. Every finished Build-plan milestone ends with \`npx genex preview\` and the player's page link (\`<dashboard>/draft/<slug>\`, with \`<dashboard>\` from \`.genex/project.json\`) \u2014 never a localhost link, a file path, or the bare play origin presented as their game. That page shows the build you just previewed; \`preview\` never disturbs the build players are on. When the player is happy with it, \`npx genex promote\` makes that exact build live for everyone \u2014 no rebuild. After every round of player feedback, end with a preview push. After the first release, the game is two versions and the player only ever hears these two words for them: the **draft** (their working copy, updated by \`preview\`) and the **public version** (what everyone plays). Command names are yours, not theirs \u2014 never say "promote" to the player; ask directly: "Want me to update the public version?", one line, once per round, and keep building while you wait \u2014 updating the public version is the ONE action that waits for an explicit yes, and more change requests instead of a yes mean "not yet". The player's "publish it" / "publish the update" / "update it" / "yes" after the first release ALL mean \`genex promote\` \u2014 never \`genex publish\` again, which would ship an untried rebuild. \`promote\` ships the last previewed build, so if anything was edited since the last \`preview\`, preview again before promoting.
895
897
  14. Parallel work: this line is your standing authorization and request to use sub-agents / parallel agent work whenever your platform provides them. While drafting the Build plan, decide per module what runs in parallel and what stays serial for THIS game \u2014 dependencies decide, there is no fixed list \u2014 and record each call in the Modules table with a one-line reason. Independent modules default to parallel; building everything serially needs a stated reason. You keep integration, previews, and the player conversation; each sub-agent owns only its module's files. When the player asks for parallel work, repeated refinement passes, a critic reviewing your output, or names any way of working your platform provides, that IS your instruction \u2014 wherever it reaches you, including inside their answer to a question you asked \u2014 so adopt it as the working mode for the rest of the build, starting with the work in front of you, and never file it as a later milestone. While delegated work runs, keep building or talk to the player; never idle in a foreground wait for something your platform will tell you about.
@@ -901,7 +903,7 @@ Important note: put soul into your creations, with many details and love. Aim to
901
903
  20. When the player names a specific thing to integrate \u2014 a library, a controller, an asset, a part of an existing game, or a tagged reference the platform placed in \`.genex/refs/<slug>/\` \u2014 integrate the real thing: read its source first and build from it, never a lighter look-alike written from scratch because it was faster. A tagged reference owns the subsystem it was tagged for, outranking the default lane and the vendored controllers for that subsystem. If you genuinely can't use it, say so in one plain line and ask how to proceed.
902
904
  21. A turn ends in exactly one of two ways: on a question the player must answer before the next step can be chosen, or on a handoff \u2014 the draft link, what changed, what to try, and what is still open. Never close a turn on a promise. "I'll keep building", "while I keep working", "adding that now" are things you say and then DO before you stop; if you are stopping, say that you have stopped and what remains. While the Build plan's \`Now:\` line has open work and no answer is needed from the player, the turn is not over: preview, hand off, and start the next milestone in the same turn, until the requested outcome is reached or the platform's own budget ends the session. If a stop was forced on you anyway, the first line of your next turn is the promise you left and where it stands.
903
905
  22. **What the player sees must read as the thing it is**, in this game's own style: a building as that building, a person as a person, a prop as that prop, a surface as its material \u2014 and a coloured box, a capsule, or a flat grey block standing in for one of them is never its finished version. Generation is the default route to that bar wherever code will not honestly reach it: the player's character, whatever the request names, whatever the player walks up to, enters, or interacts with, and the music and sound underneath \u2014 reach for the belt on your own judgment, without stopping to ask; a status line saying what you queued is enough, and \`--no-wait\` keeps you building while it lands. Procedural code is a first-class engine for what is structural, repeated, distant, or parametric \u2014 terrain, sky, fences, paving, modular kits, filler \u2014 and for anything else only when the result meets the same bar and you have looked at it in a capture before its row says \`landed\`. Start with one reusable implementation per distinct required gameplay or visual role. Background populations default to reusable procedural bodies and motion or compatible existing rigged assets. Catalog clips need a compatible skeleton and may be paid. Reserve paid custom bodies for the player and characters examined or interacted with closely, while honoring explicit user requests. Add variants only for an unmet requirement; unspent allowance is not unfinished work. Allowance questions use timeoutPolicy no-consent: silence does not raise the allowance. Record role, reuse and completion criteria in the existing Assets table. Decide per object, not per category, and write the route on each Assets row in \`DESIGN.md\` \u2014 the paid flow for generated pieces, the procedural flow with its local path for code-built ones. An Assets table with no generation in it is a decision, not a default: record it as \`Generation: none \u2014 <why code alone reaches the bar here>\` or generate.
904
- 23. **The screenshots you take to look at your own work go in \`.genex/scratch/\`.** Captures, render comparisons, before/after strips, traces and metrics dumps are how you SEE what you built \u2014 they are not part of what you built, and \`genex preview\` pushes the whole folder to the game's source, every time, forever. \`.genex/scratch/\` already exists for this and is already ignored, so there is nothing to set up: write the capture as \`.genex/scratch/arena-before.png\` and read it back from there. Never put them in a folder of your own at the top level: \`progress/\`, \`reports/\`, \`shots/\` and their kind are pushed like source and become permanent weight in the game and in every remix of it \u2014 one real game reached 11 GB and 10,332 committed screenshots exactly this way. If you find such a folder already there, add it to \`.gitignore\`; never delete the player's files (law 18).
906
+ 23. **The screenshots you take to look at your own work go in \`.genex/scratch/\`.** Captures, render comparisons, before/after strips, traces and metrics dumps are how you SEE what you built \u2014 they are not part of what you built, and \`genex preview\` pushes the whole folder to the game's source, every time, forever. \`.genex/scratch/\` already exists for this and is already ignored, so there is nothing to set up: write the capture as \`.genex/scratch/arena-before.png\` and read it back from there. Never put them in a folder of your own at the top level: \`progress/\`, \`reports/\`, \`shots/\` and their kind are pushed like source and become permanent weight in the game and in every remix of it \u2014 one real game reached 11 GB and 10,332 committed screenshots exactly this way. If you find such a folder already there, add it to \`.gitignore\`; never delete the player's files (law 18). One file in \`.genex/scratch/\` IS read, by the platform: \`.genex/scratch/cover.png\` is the game's cover \u2014 the frame you would put on its poster: real gameplay at its signature moment, well lit, no menus, popups or busy HUD, landscape 16:9, and never any text (the page sets the title beside it). The next \`genex preview\` makes it the cover; save a better moment over it whenever one comes along, and if your browser tool can set the cover itself, use that instead.
905
907
  24. **Benchmark before declaring an in-game LLM ceiling.** If the game calls a language model while the player plays \u2014 an NPC answering in its own words, a quest written for this save, a judge reading what the player typed \u2014 the PLAYER pays for it from their Genex credits, billed as used, and the game declares no price, only a per-call CEILING: \`maxCredits\` on \`generate()\`, \`perCallMaxCredits\` on a standing budget. That ceiling bounds what one call may cost AND funds how long its answer may be, so never pick it by judgement or from a vendor's rate card. Run the real prompt on your own credits \u2014 \`npx genex llm bench "<the prompt>" --schema <file> --samples 3 --max-credits <n> --user-approved\` \u2014 and declare the figures it prints, recorded in \`DESIGN.md\` with its model and date; re-benchmark whenever the prompt, the schema or the model changes, because a prompt edit changes the ceiling. Check the lane first with \`npx genex llm models\`: where it is off the routes 404, so build the feature behind a graceful unavailable state and say so in the handoff. A repeated call is a standing budget the player approves once (\`requestSpendGrant()\`, then \`generate({ grantId })\`), never a popup per turn, and its disclosed call rate and credits per period are computed from the game's own loop and the bench's measured average. The coin spellings (\`estimateCoins\`, \`perCallMaxCoins\`) are deprecated \u2014 write the credit names. Model output is data the game validates against its own expectation \u2014 never executed, and never authority over coin, credits, items, entitlements or rewards. Load \`$genex-llm-in-games\` before writing any of it.
906
908
  ${CONTRACT_END}
907
909
  `;
@@ -1161,6 +1163,7 @@ var REFUSED_IN_TOOLS = /* @__PURE__ */ new Set([
1161
1163
  "publish",
1162
1164
  "rollback",
1163
1165
  "rename",
1166
+ "cover",
1164
1167
  "make-remixable",
1165
1168
  "shop",
1166
1169
  "domain",
@@ -5180,18 +5183,18 @@ async function deployGame(ctx, opts, log) {
5180
5183
  return false;
5181
5184
  }
5182
5185
  const totalBytes = files.reduce((n, f) => n + f.bytes.length, 0);
5183
- const mb = (n) => (n / 1048576).toFixed(n < 1048576 ? 2 : 1);
5186
+ const mb2 = (n) => (n / 1048576).toFixed(n < 1048576 ? 2 : 1);
5184
5187
  if (files.length > grant.limits.maxFiles) {
5185
5188
  log.error(`Too many files (${files.length} > ${grant.limits.maxFiles}). Trim the build and retry.`);
5186
5189
  return false;
5187
5190
  }
5188
5191
  const oversized = files.find((f) => f.bytes.length > grant.limits.maxFileBytes);
5189
5192
  if (oversized) {
5190
- log.error(`${oversized.relPath} is ${mb(oversized.bytes.length)} MB \u2014 over the ${mb(grant.limits.maxFileBytes)} MB per-file limit.`);
5193
+ log.error(`${oversized.relPath} is ${mb2(oversized.bytes.length)} MB \u2014 over the ${mb2(grant.limits.maxFileBytes)} MB per-file limit.`);
5191
5194
  return false;
5192
5195
  }
5193
5196
  if (totalBytes > grant.limits.maxTotalBytes) {
5194
- log.error(`Build is ${mb(totalBytes)} MB \u2014 over the ${mb(grant.limits.maxTotalBytes)} MB limit.`);
5197
+ log.error(`Build is ${mb2(totalBytes)} MB \u2014 over the ${mb2(grant.limits.maxTotalBytes)} MB limit.`);
5195
5198
  return false;
5196
5199
  }
5197
5200
  log.step(`Uploading ${files.length} files\u2026`);
@@ -5765,15 +5768,275 @@ async function createDraftProject(opts) {
5765
5768
  function randomSuffix() {
5766
5769
  return crypto3.randomBytes(3).toString("hex");
5767
5770
  }
5768
- async function fetchProjectStatus(apiUrl, token, slug) {
5771
+ async function fetchOwnerProject(apiUrl, token, slug) {
5769
5772
  try {
5770
5773
  const res = await apiFetch(`${apiUrl}/api/projects/by-slug/${encodeURIComponent(slug)}`, {
5771
5774
  headers: { Authorization: `Bearer ${token}` }
5772
5775
  });
5773
5776
  if (!res.ok) return null;
5774
5777
  const data = await res.json().catch(() => null);
5775
- if (!data?.project) return null;
5776
- return data.project.status ?? "draft";
5778
+ const project = data?.project;
5779
+ if (!project) return null;
5780
+ return {
5781
+ status: project.status ?? "draft",
5782
+ coverUrl: project.thumbnailUrl ?? null,
5783
+ coverSource: "coverSource" in project ? project.coverSource ?? null : void 0
5784
+ };
5785
+ } catch {
5786
+ return null;
5787
+ }
5788
+ }
5789
+
5790
+ // src/lib/cover.ts
5791
+ import crypto4 from "crypto";
5792
+ import fs18 from "fs/promises";
5793
+ import path18 from "path";
5794
+ var COVER_MAX_BYTES = 8 * 1024 * 1024;
5795
+ var COVER_MIN_WIDTH = 320;
5796
+ var COVER_MIN_HEIGHT = 180;
5797
+ var SCRATCH_COVER_NAMES = ["cover.png", "cover.jpg", "cover.jpeg", "cover.webp"];
5798
+ var COVER_MEMO = ".genex/cover-upload.json";
5799
+ var STAND_IN_SOURCES = /* @__PURE__ */ new Set(["legacy", "ai", "inherited", "keyart", "session_shot"]);
5800
+ function coverIsStandIn(source) {
5801
+ if (source === void 0) return false;
5802
+ return source === null || STAND_IN_SOURCES.has(source);
5803
+ }
5804
+ var SOURCE_LABEL = {
5805
+ owner: "chosen by the owner",
5806
+ agent: "a frame set from the build (genex cover)",
5807
+ owner_play: "captured while the owner played",
5808
+ session_shot: "a screenshot from a build session (picked automatically)",
5809
+ keyart: "the game's own menu art (picked automatically)",
5810
+ inherited: "inherited from the game it was remixed from",
5811
+ ai: "AI-generated",
5812
+ legacy: "set before cover sources were recorded"
5813
+ };
5814
+ function describeCoverSource(source) {
5815
+ if (source === void 0) return "not reported by this Genex stand";
5816
+ if (source === null) return "none";
5817
+ return SOURCE_LABEL[source] ?? source;
5818
+ }
5819
+ var COVER_ESCAPE = "If the game is dark by design and its frames come back refused, leave it: never change the game's lighting for a cover.";
5820
+ function coverNudge(source, hosted, last) {
5821
+ if (!coverIsStandIn(source)) return null;
5822
+ const state = source === null ? "This game has no cover yet" : "This game's cover is only a stand-in";
5823
+ const frame = "real gameplay at its signature moment, well lit, no menus, popups or busy HUD, landscape 16:9, no text";
5824
+ if (last === "rejected") {
5825
+ const act = hosted ? "pass cover: true on the genex_browser check that shows it" : `save it over ${SCRATCH_DIR}/cover.png`;
5826
+ return `\u279C ${state}, and the last frame sent was refused. Only if the game has a brighter moment (${frame}), ${act}; if it is dark by design, leave it \u2014 never change the game's lighting for a cover; the owner can set one on the game's page.`;
5827
+ }
5828
+ if (hosted) {
5829
+ return `\u279C ${state}: once the game has a moment worth putting on its poster (${frame}), pass cover: true on the genex_browser check that shows it \u2014 that exact frame becomes the cover. ${COVER_ESCAPE}`;
5830
+ }
5831
+ return `\u279C ${state}: once the game has a moment worth putting on its poster, save that frame (${frame}) as ${SCRATCH_DIR}/cover.png \u2014 the next preview makes it the cover. ${COVER_ESCAPE}`;
5832
+ }
5833
+ function sniffImage(bytes) {
5834
+ const b = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
5835
+ if (b.length >= 24 && b.readUInt32BE(0) === 2303741511 && b.readUInt32BE(4) === 218765834) {
5836
+ return { type: "image/png", width: b.readUInt32BE(16), height: b.readUInt32BE(20) };
5837
+ }
5838
+ if (b.length >= 3 && b[0] === 255 && b[1] === 216 && b[2] === 255) {
5839
+ return { type: "image/jpeg", ...jpegSize(b) };
5840
+ }
5841
+ if (b.length >= 16 && b.toString("ascii", 0, 4) === "RIFF" && b.toString("ascii", 8, 12) === "WEBP") {
5842
+ return { type: "image/webp", ...webpSize(b) };
5843
+ }
5844
+ return null;
5845
+ }
5846
+ function jpegSize(b) {
5847
+ let i = 2;
5848
+ while (i + 9 < b.length) {
5849
+ if (b[i] !== 255) return { width: null, height: null };
5850
+ const marker = b[i + 1];
5851
+ if (marker === 255) {
5852
+ i += 1;
5853
+ continue;
5854
+ }
5855
+ if (marker === 1 || marker >= 208 && marker <= 216) {
5856
+ i += 2;
5857
+ continue;
5858
+ }
5859
+ if (marker === 218) break;
5860
+ const isFrame = marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204;
5861
+ if (isFrame) return { height: b.readUInt16BE(i + 5), width: b.readUInt16BE(i + 7) };
5862
+ i += 2 + b.readUInt16BE(i + 2);
5863
+ }
5864
+ return { width: null, height: null };
5865
+ }
5866
+ function webpSize(b) {
5867
+ const chunk = b.toString("ascii", 12, 16);
5868
+ if (chunk === "VP8 " && b.length >= 30) {
5869
+ return { width: b.readUInt16LE(26) & 16383, height: b.readUInt16LE(28) & 16383 };
5870
+ }
5871
+ if (chunk === "VP8L" && b.length >= 25) {
5872
+ const bits = b.readUInt32LE(21);
5873
+ return { width: (bits & 16383) + 1, height: (bits >>> 14 & 16383) + 1 };
5874
+ }
5875
+ if (chunk === "VP8X" && b.length >= 30) {
5876
+ return { width: b.readUIntLE(24, 3) + 1, height: b.readUIntLE(27, 3) + 1 };
5877
+ }
5878
+ return { width: null, height: null };
5879
+ }
5880
+ function isFinalAnswer(outcome2) {
5881
+ return outcome2.kind !== "failed";
5882
+ }
5883
+ function waitWords(retryAfter) {
5884
+ const secs = Number(retryAfter);
5885
+ if (!Number.isFinite(secs) || secs <= 0) return "later this hour (cover uploads are limited per hour)";
5886
+ if (secs < 90) return `in about ${Math.ceil(secs)} seconds`;
5887
+ const min = Math.ceil(secs / 60);
5888
+ return `in about ${min} minute${min === 1 ? "" : "s"}`;
5889
+ }
5890
+ function refusal(res, what) {
5891
+ const status2 = res.status;
5892
+ if (status2 === 401) return "Genex refused this machine's sign-in \u2014 run `npx genex auth`, then try again.";
5893
+ if (status2 === 403) return "Only the game's owner can set its cover, and this machine is signed in as someone else.";
5894
+ if (status2 === 404) return "Genex doesn't know this game under this account \u2014 check `.genex/project.json` (or `npx genex link <slug>`).";
5895
+ if (status2 === 413) return `That image is too big for a cover (at most ${COVER_MAX_BYTES / 1024 / 1024} MB).`;
5896
+ if (status2 === 429) {
5897
+ return `Too many cover uploads on this account for now \u2014 Genex takes the next one ${waitWords(res.headers.get("Retry-After"))}. Nothing to fix in the frame; the next preview sends it again by itself.`;
5898
+ }
5899
+ return `Couldn't ${what} (HTTP ${status2}) \u2014 try again.`;
5900
+ }
5901
+ var mb = (n) => `${(n / 1024 / 1024).toFixed(1)} MB`;
5902
+ async function uploadCover(args) {
5903
+ const { apiUrl, token, projectId, bytes, label } = args;
5904
+ const info = sniffImage(bytes);
5905
+ if (!info) return { kind: "invalid", message: `${label} is not a PNG, JPEG or WebP image.` };
5906
+ if (bytes.byteLength > COVER_MAX_BYTES) {
5907
+ return {
5908
+ kind: "invalid",
5909
+ message: `${label} is ${mb(bytes.byteLength)}; a cover can be at most ${COVER_MAX_BYTES / 1024 / 1024} MB. Save the frame as a JPEG and try again.`
5910
+ };
5911
+ }
5912
+ if (info.width !== null && info.height !== null && (info.width < COVER_MIN_WIDTH || info.height < COVER_MIN_HEIGHT)) {
5913
+ return {
5914
+ kind: "invalid",
5915
+ message: `${label} is only ${info.width}\xD7${info.height}; a cover needs at least ${COVER_MIN_WIDTH}\xD7${COVER_MIN_HEIGHT}. Capture the game at its normal window size.`
5916
+ };
5917
+ }
5918
+ const headers = { "Content-Type": "application/json", Authorization: `Bearer ${token}` };
5919
+ const base = `${apiUrl}/api/projects/${encodeURIComponent(projectId)}/cover`;
5920
+ let minted;
5921
+ try {
5922
+ minted = await apiFetch(`${base}/upload-url`, {
5923
+ method: "POST",
5924
+ headers,
5925
+ body: JSON.stringify({ bytes: bytes.byteLength, contentType: info.type })
5926
+ });
5927
+ } catch {
5928
+ return { kind: "failed", message: "Couldn't reach Genex to upload the cover." };
5929
+ }
5930
+ if (printedStructuredError(minted)) return { kind: "failed", message: null };
5931
+ if (!minted.ok) return { kind: "failed", message: refusal(minted, "start the cover upload") };
5932
+ const grant = await minted.json().catch(() => null);
5933
+ if (!grant?.uploadUrl) return { kind: "failed", message: "Genex answered the cover upload unexpectedly \u2014 nothing was uploaded." };
5934
+ if (!grant.key) {
5935
+ return { kind: "failed", message: "This Genex stand doesn't take covers from the CLI yet \u2014 nothing was uploaded." };
5936
+ }
5937
+ let put;
5938
+ try {
5939
+ put = await fetch(grant.uploadUrl, {
5940
+ method: "PUT",
5941
+ // Both are part of what the URL was signed for; send them back unchanged.
5942
+ headers: { "Content-Type": info.type, "Content-Length": String(bytes.byteLength) },
5943
+ body: new Uint8Array(bytes)
5944
+ });
5945
+ } catch {
5946
+ return { kind: "failed", message: "The cover upload didn't reach storage (network error) \u2014 try again." };
5947
+ }
5948
+ if (!put.ok) return { kind: "failed", message: `The cover upload failed (HTTP ${put.status}) \u2014 try again.` };
5949
+ let done;
5950
+ try {
5951
+ done = await apiFetch(`${base}/commit`, { method: "POST", headers, body: JSON.stringify({ key: grant.key }) });
5952
+ } catch {
5953
+ return { kind: "failed", message: "Couldn't reach Genex to finish the cover upload." };
5954
+ }
5955
+ if (printedStructuredError(done)) return { kind: "failed", message: null };
5956
+ const body = await done.json().catch(() => null);
5957
+ if (done.status === 422 && body?.error === "cover_rejected") {
5958
+ return { kind: "rejected", reason: body.reason ?? "rejected", stats: body.stats ?? null };
5959
+ }
5960
+ if (!done.ok) return { kind: "failed", message: refusal(done, "set the cover") };
5961
+ const view = { coverUrl: body?.coverUrl ?? null, coverSource: body?.coverSource ?? null };
5962
+ return body?.applied === false ? { kind: "outranked", ...view } : { kind: "applied", ...view };
5963
+ }
5964
+ var pct = (n) => `${Math.round(n * 100)}%`;
5965
+ function reportCoverOutcome(log, outcome2, opts) {
5966
+ const fail5 = (m) => opts.asError ? log.error(m) : log.warn(m);
5967
+ switch (outcome2.kind) {
5968
+ case "applied":
5969
+ log.success(`Cover set \u2014 ${opts.label} is this game's cover now.`);
5970
+ if (opts.showUrl && outcome2.coverUrl) log.dim(` ${outcome2.coverUrl}`);
5971
+ return;
5972
+ case "outranked":
5973
+ log.info("Cover unchanged \u2014 the owner picked this game's cover themselves, and that choice stands. Nothing to retry (the owner changes it on the game's page).");
5974
+ return;
5975
+ case "rejected": {
5976
+ const why = outcome2.reason === "too_dark" ? "is too dark to read as the game on a gallery card" : outcome2.reason === "flat" ? "is almost one flat colour \u2014 a loading screen, a fade or an empty sky reads as nothing on a card" : `was refused (${outcome2.reason})`;
5977
+ fail5(`Cover not used \u2014 ${opts.label} ${why}.`);
5978
+ if (outcome2.stats) {
5979
+ const s = outcome2.stats;
5980
+ log.dim(` brightness ${pct(s.mean)} \xB7 contrast ${pct(s.std)} \xB7 ${pct(s.darkShare)} near-black`);
5981
+ }
5982
+ log.dim(` ${opts.retry}`);
5983
+ return;
5984
+ }
5985
+ case "invalid":
5986
+ fail5(`Cover not used \u2014 ${outcome2.message}`);
5987
+ return;
5988
+ case "failed":
5989
+ if (outcome2.message) fail5(outcome2.message);
5990
+ return;
5991
+ }
5992
+ }
5993
+ function sha256(bytes) {
5994
+ return crypto4.createHash("sha256").update(bytes).digest("hex");
5995
+ }
5996
+ async function readCoverMemo(cwd = process.cwd()) {
5997
+ try {
5998
+ const memo = JSON.parse(await fs18.readFile(path18.join(cwd, COVER_MEMO), "utf8"));
5999
+ return typeof memo.sha256 === "string" ? memo : null;
6000
+ } catch {
6001
+ return null;
6002
+ }
6003
+ }
6004
+ async function rememberCoverUpload(bytes, file, outcome2, cwd = process.cwd()) {
6005
+ if (!isFinalAnswer(outcome2)) return;
6006
+ const memo = { sha256: sha256(bytes), file, outcome: outcome2.kind, at: (/* @__PURE__ */ new Date()).toISOString() };
6007
+ try {
6008
+ await fs18.mkdir(path18.join(cwd, ".genex"), { recursive: true });
6009
+ await fs18.writeFile(path18.join(cwd, COVER_MEMO), JSON.stringify(memo, null, 2) + "\n");
6010
+ } catch {
6011
+ }
6012
+ }
6013
+ async function findScratchCover(cwd = process.cwd()) {
6014
+ let best = null;
6015
+ for (const name of SCRATCH_COVER_NAMES) {
6016
+ const rel = `${SCRATCH_DIR}/${name}`;
6017
+ try {
6018
+ const stat = await fs18.stat(path18.join(cwd, rel));
6019
+ if (stat.isFile() && (!best || stat.mtimeMs > best.mtime)) best = { rel, mtime: stat.mtimeMs };
6020
+ } catch {
6021
+ }
6022
+ }
6023
+ return best?.rel ?? null;
6024
+ }
6025
+ async function pickUpScratchCover(args) {
6026
+ const cwd = args.cwd ?? process.cwd();
6027
+ try {
6028
+ const rel = await findScratchCover(cwd);
6029
+ if (!rel) return null;
6030
+ const bytes = await fs18.readFile(path18.join(cwd, rel));
6031
+ const memo = await readCoverMemo(cwd);
6032
+ if (memo?.sha256 === sha256(bytes)) return null;
6033
+ const outcome2 = await uploadCover({ apiUrl: args.apiUrl, token: args.token, projectId: args.projectId, bytes, label: rel });
6034
+ reportCoverOutcome(args.log, outcome2, {
6035
+ label: rel,
6036
+ retry: "Save a better frame over it and the next preview tries again \u2014 or, if the game is dark by design, leave it: never change the game's lighting for a cover."
6037
+ });
6038
+ await rememberCoverUpload(bytes, rel, outcome2, cwd);
6039
+ return outcome2;
5777
6040
  } catch {
5778
6041
  return null;
5779
6042
  }
@@ -5824,9 +6087,10 @@ async function runPreview(opts) {
5824
6087
  process.exitCode = 1;
5825
6088
  return;
5826
6089
  }
5827
- const freshStatus = await fetchProjectStatus(apiUrl, token, meta.slug);
5828
- if (freshStatus && freshStatus !== meta.status) {
5829
- meta.status = freshStatus;
6090
+ await pickUpScratchCover({ apiUrl, token, projectId: meta.id, log });
6091
+ const fresh = await fetchOwnerProject(apiUrl, token, meta.slug);
6092
+ if (fresh && fresh.status !== meta.status) {
6093
+ meta.status = fresh.status;
5830
6094
  }
5831
6095
  const onDisk = await readProject();
5832
6096
  const merged = { ...onDisk ?? meta, status: meta.status, lastDeployAt: (/* @__PURE__ */ new Date()).toISOString() };
@@ -5845,6 +6109,12 @@ async function runPreview(opts) {
5845
6109
  if (dashboard) {
5846
6110
  log.plain(` your game's page (share this link): ${c.cyan(draftPageUrl(dashboard, meta.slug))}`);
5847
6111
  }
6112
+ const hosted = inHostedSession();
6113
+ const nudge = hosted && !hostedBrowserCovers() ? null : coverNudge(fresh?.coverSource, hosted, (await readCoverMemo())?.outcome ?? null);
6114
+ if (nudge) {
6115
+ log.plain("");
6116
+ log.plain(nudge);
6117
+ }
5848
6118
  }
5849
6119
 
5850
6120
  // src/commands/remix.ts
@@ -5876,28 +6146,28 @@ function loggerFor(opts) {
5876
6146
  }
5877
6147
  async function readState(directory) {
5878
6148
  try {
5879
- const marker = path18.join(directory, ".genex", "remix-operation.json");
5880
- if ((await fs18.lstat(marker)).isSymbolicLink()) return null;
5881
- const value = JSON.parse(await fs18.readFile(marker, "utf8"));
6149
+ const marker = path19.join(directory, ".genex", "remix-operation.json");
6150
+ if ((await fs19.lstat(marker)).isSymbolicLink()) return null;
6151
+ const value = JSON.parse(await fs19.readFile(marker, "utf8"));
5882
6152
  return value.schema === 1 && value.directory === directory && typeof value.operationId === "string" && /^[a-zA-Z0-9_-]{8,128}$/.test(value.operationId) && value.source && /^[a-f0-9]{40}$/.test(value.source.sourceCommitSha) ? value : null;
5883
6153
  } catch {
5884
6154
  return null;
5885
6155
  }
5886
6156
  }
5887
6157
  async function saveState(state) {
5888
- const dir = path18.join(state.directory, ".genex");
5889
- await fs18.mkdir(dir, { recursive: true, mode: 448 });
5890
- const temp = path18.join(dir, "remix-operation.next.json");
5891
- await fs18.writeFile(temp, JSON.stringify(state, null, 2) + "\n", { mode: 384 });
5892
- await fs18.rename(temp, path18.join(dir, "remix-operation.json"));
6158
+ const dir = path19.join(state.directory, ".genex");
6159
+ await fs19.mkdir(dir, { recursive: true, mode: 448 });
6160
+ const temp = path19.join(dir, "remix-operation.next.json");
6161
+ await fs19.writeFile(temp, JSON.stringify(state, null, 2) + "\n", { mode: 384 });
6162
+ await fs19.rename(temp, path19.join(dir, "remix-operation.json"));
5893
6163
  }
5894
6164
  async function checkDestination(directory) {
5895
6165
  try {
5896
- const stat = await fs18.lstat(directory);
6166
+ const stat = await fs19.lstat(directory);
5897
6167
  if (!stat.isDirectory() || stat.isSymbolicLink()) throw new RemixError("destination_occupied", "The destination is not a fresh directory. Choose a new folder.");
5898
- const entries = await fs18.readdir(directory);
6168
+ const entries = await fs19.readdir(directory);
5899
6169
  if (!entries.length) return null;
5900
- const genex = await fs18.lstat(path18.join(directory, ".genex")).catch(() => null);
6170
+ const genex = await fs19.lstat(path19.join(directory, ".genex")).catch(() => null);
5901
6171
  const state = genex?.isDirectory() && !genex.isSymbolicLink() ? await readState(directory) : null;
5902
6172
  if (state) return state;
5903
6173
  throw new RemixError("destination_occupied", "The destination already contains files. Choose a fresh folder; remix never clears an existing project.");
@@ -5908,7 +6178,7 @@ async function checkDestination(directory) {
5908
6178
  }
5909
6179
  async function assertReferenceParents(cwd) {
5910
6180
  for (const rel of [".genex", ".genex/refs"]) {
5911
- const stat = await fs18.lstat(path18.join(cwd, rel)).catch((err) => {
6181
+ const stat = await fs19.lstat(path19.join(cwd, rel)).catch((err) => {
5912
6182
  if (err.code === "ENOENT") return null;
5913
6183
  throw err;
5914
6184
  });
@@ -5916,41 +6186,41 @@ async function assertReferenceParents(cwd) {
5916
6186
  }
5917
6187
  }
5918
6188
  async function archiveSourceInstructions(directory) {
5919
- const archive = path18.join(directory, ".genex", "source-instructions");
6189
+ const archive = path19.join(directory, ".genex", "source-instructions");
5920
6190
  const rootArtifacts = /* @__PURE__ */ new Set([".claude", ".codex", ".cursor", ".agents", ".mcp.json", ".cursorrules", "DESIGN.md"]);
5921
6191
  const instructionNames = /* @__PURE__ */ new Set(["AGENTS.md", "CLAUDE.md", "GEMINI.md"]);
5922
6192
  const visit = async (dir) => {
5923
- for (const entry of await fs18.readdir(dir, { withFileTypes: true })) {
6193
+ for (const entry of await fs19.readdir(dir, { withFileTypes: true })) {
5924
6194
  if (entry.name === ".genex" || entry.name === ".git" || entry.name === "node_modules") continue;
5925
- const from = path18.join(dir, entry.name), rel = path18.relative(directory, from);
6195
+ const from = path19.join(dir, entry.name), rel = path19.relative(directory, from);
5926
6196
  if (instructionNames.has(entry.name) || dir === directory && rootArtifacts.has(entry.name)) {
5927
- const to = path18.join(archive, rel);
5928
- await fs18.mkdir(path18.dirname(to), { recursive: true });
5929
- if (await fs18.lstat(to).catch(() => null)) throw new RemixError("destination_changed", `Source instructions at ${rel} changed during preparation; both copies were retained.`);
5930
- await fs18.rename(from, to);
6197
+ const to = path19.join(archive, rel);
6198
+ await fs19.mkdir(path19.dirname(to), { recursive: true });
6199
+ if (await fs19.lstat(to).catch(() => null)) throw new RemixError("destination_changed", `Source instructions at ${rel} changed during preparation; both copies were retained.`);
6200
+ await fs19.rename(from, to);
5931
6201
  } else if (entry.isDirectory()) await visit(from);
5932
6202
  }
5933
6203
  };
5934
6204
  await visit(directory);
5935
6205
  }
5936
6206
  async function acquireLock(directory) {
5937
- const lock = path18.join(directory, ".genex", "remix.lock");
6207
+ const lock = path19.join(directory, ".genex", "remix.lock");
5938
6208
  for (let attempt = 0; attempt < 2; attempt++) {
5939
6209
  try {
5940
- const handle = await fs18.open(lock, "wx", 384);
6210
+ const handle = await fs19.open(lock, "wx", 384);
5941
6211
  await handle.writeFile(String(process.pid));
5942
6212
  await handle.close();
5943
- return () => fs18.unlink(lock).catch(() => {
6213
+ return () => fs19.unlink(lock).catch(() => {
5944
6214
  });
5945
6215
  } catch (err) {
5946
6216
  if (err.code !== "EEXIST") throw err;
5947
- const pid = Number(await fs18.readFile(lock, "utf8").catch(() => ""));
6217
+ const pid = Number(await fs19.readFile(lock, "utf8").catch(() => ""));
5948
6218
  if (!Number.isInteger(pid) || pid <= 0) throw new RemixError("remix_in_progress", "Another remix owns this destination. Retry after it finishes.");
5949
6219
  try {
5950
6220
  process.kill(pid, 0);
5951
6221
  } catch (probe) {
5952
6222
  if (probe.code === "ESRCH") {
5953
- await fs18.unlink(lock);
6223
+ await fs19.unlink(lock);
5954
6224
  continue;
5955
6225
  }
5956
6226
  }
@@ -6036,13 +6306,13 @@ function pinRemixDependencies(pkg, version) {
6036
6306
  return warnings;
6037
6307
  }
6038
6308
  async function installRemixDependencies(cwd, pm, version, log, runner = childRun) {
6039
- const pkgPath = path18.join(cwd, "package.json");
6040
- const pkg = JSON.parse(await fs18.readFile(pkgPath, "utf8"));
6309
+ const pkgPath = path19.join(cwd, "package.json");
6310
+ const pkg = JSON.parse(await fs19.readFile(pkgPath, "utf8"));
6041
6311
  const warnings = pinRemixDependencies(pkg, version);
6042
- await fs18.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
6312
+ await fs19.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
6043
6313
  log.step(`Restoring dependencies with ${pm}; pinning ${CLI_PACKAGE}@${version} (${CLI_CHANNEL}).`);
6044
6314
  const args = pm === "npm" ? ["install", "--workspaces=false", "--no-audit", "--no-fund"] : pm === "pnpm" ? ["install", "--ignore-workspace", "--no-frozen-lockfile"] : ["install"];
6045
- if (pm === "yarn") await fs18.writeFile(path18.join(cwd, "yarn.lock"), "", { flag: "wx" }).catch((err) => {
6315
+ if (pm === "yarn") await fs19.writeFile(path19.join(cwd, "yarn.lock"), "", { flag: "wx" }).catch((err) => {
6046
6316
  if (err.code !== "EEXIST") throw err;
6047
6317
  });
6048
6318
  await runner(cwd, pm, args);
@@ -6076,27 +6346,27 @@ function rebindRemixConfig(content, slug) {
6076
6346
  async function configureIdentity(state, log) {
6077
6347
  const cwd = state.directory, meta = state.project;
6078
6348
  await writeProject(meta, cwd);
6079
- const configFile = path18.join(cwd, "src", "genex.config.ts");
6080
- const config = await fs18.readFile(configFile, "utf8").catch((err) => {
6349
+ const configFile = path19.join(cwd, "src", "genex.config.ts");
6350
+ const config = await fs19.readFile(configFile, "utf8").catch((err) => {
6081
6351
  if (err.code === "ENOENT") return null;
6082
6352
  throw err;
6083
6353
  });
6084
- if (config !== null) await fs18.writeFile(configFile, rebindRemixConfig(config, meta.slug));
6354
+ if (config !== null) await fs19.writeFile(configFile, rebindRemixConfig(config, meta.slug));
6085
6355
  await writeGameConfigFiles(meta, log, cwd);
6086
6356
  await writeGitignore(cwd, log);
6087
6357
  await installRemixProfile(cwd, state.apiUrl, log);
6088
- const manifestFile = path18.join(cwd, "package.json");
6089
- const manifest = JSON.parse(await fs18.readFile(manifestFile, "utf8"));
6358
+ const manifestFile = path19.join(cwd, "package.json");
6359
+ const manifest = JSON.parse(await fs19.readFile(manifestFile, "utf8"));
6090
6360
  manifest.genex.remixSource = { projectId: state.source.projectId, slug: state.source.slug, sourceCommitSha: state.source.sourceCommitSha, version: state.source.version, versionCertainty: state.source.versionCertainty };
6091
- await fs18.writeFile(manifestFile, JSON.stringify(manifest, null, 2) + "\n");
6361
+ await fs19.writeFile(manifestFile, JSON.stringify(manifest, null, 2) + "\n");
6092
6362
  const possible = [];
6093
6363
  const quoted = new RegExp(`(["'\`])(?:${state.source.slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}|${state.source.projectId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})\\1`);
6094
6364
  const scan = async (dir) => {
6095
- for (const entry of await fs18.readdir(dir, { withFileTypes: true })) {
6365
+ for (const entry of await fs19.readdir(dir, { withFileTypes: true })) {
6096
6366
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
6097
- const file = path18.join(dir, entry.name);
6367
+ const file = path19.join(dir, entry.name);
6098
6368
  if (entry.isDirectory()) await scan(file);
6099
- else if (entry.isFile() && /\.(?:[cm]?[jt]sx?|html)$/.test(entry.name) && (await fs18.stat(file)).size < 1024 * 1024 && quoted.test(await fs18.readFile(file, "utf8"))) possible.push(path18.relative(cwd, file));
6369
+ else if (entry.isFile() && /\.(?:[cm]?[jt]sx?|html)$/.test(entry.name) && (await fs19.stat(file)).size < 1024 * 1024 && quoted.test(await fs19.readFile(file, "utf8"))) possible.push(path19.relative(cwd, file));
6100
6370
  }
6101
6371
  };
6102
6372
  await scan(cwd);
@@ -6121,7 +6391,7 @@ async function runRemix(opts, deps = {}) {
6121
6391
  const apiUrl = getApiUrl(opts.apiUrl), dashboardUrl = new URL(getAuthUrl(opts.authUrl)).origin;
6122
6392
  if (opts.sourceOnly && !opts.destination) await assertReferenceParents(originalCwd);
6123
6393
  const selected = parseRemixSource(opts.remixSource, apiUrl, dashboardUrl);
6124
- let directory = opts.destination ? path18.resolve(originalCwd, opts.destination) : !opts.sourceOnly ? path18.resolve(originalCwd, `${selected.slug}-remix`) : void 0;
6394
+ let directory = opts.destination ? path19.resolve(originalCwd, opts.destination) : !opts.sourceOnly ? path19.resolve(originalCwd, `${selected.slug}-remix`) : void 0;
6125
6395
  if (directory) state = await checkDestination(directory);
6126
6396
  resumed = state !== null;
6127
6397
  if (state && (state.apiUrl !== apiUrl || state.sourceOnly !== Boolean(opts.sourceOnly) || ![state.requestedSlug, state.source.slug].includes(selected.slug) || opts.operationId && state.operationId !== opts.operationId || opts.name && state.name !== opts.name || selected.version && selected.version !== state.source.version)) throw new RemixError("operation_mismatch", "This folder belongs to a different remix operation. Resume with its original source/options or choose a fresh folder.");
@@ -6139,29 +6409,29 @@ async function runRemix(opts, deps = {}) {
6139
6409
  log.step(`Resolving source for ${selected.slug}\u2026`);
6140
6410
  const body = await responseJson(await request(`${apiUrl}/api/projects/${encodeURIComponent(selected.slug)}/source?${query}`, { headers, redirect: "error" }));
6141
6411
  validateSource(body, Boolean(opts.sourceOnly));
6142
- directory ??= path18.resolve(originalCwd, ".genex", "refs", `${body.source.slug}-${body.source.sourceCommitSha.slice(0, 12)}`);
6412
+ directory ??= path19.resolve(originalCwd, ".genex", "refs", `${body.source.slug}-${body.source.sourceCommitSha.slice(0, 12)}`);
6143
6413
  const existing = await checkDestination(directory);
6144
6414
  if (existing) {
6145
6415
  if (!existing.sourceOnly || !opts.sourceOnly || existing.apiUrl !== apiUrl || existing.source.sourceCommitSha !== body.source.sourceCommitSha || existing.source.projectId !== body.source.projectId) throw new RemixError("destination_occupied", "This reference destination already belongs to another operation. Choose a fresh folder.");
6146
6416
  state = existing;
6147
6417
  resumed = true;
6148
6418
  } else {
6149
- state = { schema: 1, operationId: opts.operationId ?? crypto4.randomUUID(), directory, apiUrl, dashboardUrl, requestedSlug: selected.slug, source: body.source, sourceToken: body.sourceToken, sourceOnly: Boolean(opts.sourceOnly), cliVersion: getCliVersion(), name: opts.name?.trim() || `${body.source.title.slice(0, 74)} remix`, stage: "download", warnings: [...body.source.warnings] };
6150
- await fs18.mkdir(directory, { recursive: true });
6151
- if ((await fs18.readdir(directory)).length) throw new RemixError("destination_occupied", "The destination changed during setup. Choose a fresh folder.");
6152
- await fs18.mkdir(path18.join(directory, ".genex"), { mode: 448 });
6419
+ state = { schema: 1, operationId: opts.operationId ?? crypto5.randomUUID(), directory, apiUrl, dashboardUrl, requestedSlug: selected.slug, source: body.source, sourceToken: body.sourceToken, sourceOnly: Boolean(opts.sourceOnly), cliVersion: getCliVersion(), name: opts.name?.trim() || `${body.source.title.slice(0, 74)} remix`, stage: "download", warnings: [...body.source.warnings] };
6420
+ await fs19.mkdir(directory, { recursive: true });
6421
+ if ((await fs19.readdir(directory)).length) throw new RemixError("destination_occupied", "The destination changed during setup. Choose a fresh folder.");
6422
+ await fs19.mkdir(path19.join(directory, ".genex"), { mode: 448 });
6153
6423
  await saveState(state);
6154
6424
  }
6155
6425
  }
6156
6426
  unlock = await acquireLock(state.directory);
6157
6427
  state = await readState(state.directory) ?? state;
6158
6428
  if (resumed && state.stage === "download") await renewSource(state, request, headers);
6159
- const staging = path18.join(state.directory, ".genex", "remix-stage");
6429
+ const staging = path19.join(state.directory, ".genex", "remix-stage");
6160
6430
  if (state.stage === "download") {
6161
- await fs18.rm(staging, { recursive: true, force: true });
6162
- await fs18.mkdir(staging, { recursive: true });
6163
- const archive = path18.join(staging, "source.zip"), sourceDir = path18.join(staging, "source");
6164
- await fs18.mkdir(sourceDir);
6431
+ await fs19.rm(staging, { recursive: true, force: true });
6432
+ await fs19.mkdir(staging, { recursive: true });
6433
+ const archive = path19.join(staging, "source.zip"), sourceDir = path19.join(staging, "source");
6434
+ await fs19.mkdir(sourceDir);
6165
6435
  const archiveUrl = new URL(state.source.archiveUrl, state.apiUrl);
6166
6436
  if (archiveUrl.origin !== new URL(state.apiUrl).origin || !archiveUrl.pathname.startsWith("/api/projects/")) throw new RemixError("invalid_source_response", "Source archive must be served by the selected Genex API.");
6167
6437
  log.step(`Downloading ${state.source.slug} at ${state.source.sourceCommitSha.slice(0, 12)}\u2026`);
@@ -6176,26 +6446,26 @@ async function runRemix(opts, deps = {}) {
6176
6446
  if (!state.sourceOnly) {
6177
6447
  let pkg;
6178
6448
  try {
6179
- pkg = JSON.parse(await fs18.readFile(path18.join(sourceDir, "package.json"), "utf8"));
6449
+ pkg = JSON.parse(await fs19.readFile(path19.join(sourceDir, "package.json"), "utf8"));
6180
6450
  } catch (error) {
6181
- const staticEntry = await fs18.stat(path18.join(sourceDir, "index.html")).catch(() => null);
6451
+ const staticEntry = await fs19.stat(path19.join(sourceDir, "index.html")).catch(() => null);
6182
6452
  if (error.code !== "ENOENT" || !staticEntry?.isFile()) throw new RemixError("source_unavailable", "Source has neither a readable package.json nor a static index.html. Use --source-only to inspect it.");
6183
6453
  pkg = { name: manifestName(state.directory), private: true };
6184
- await fs18.writeFile(path18.join(sourceDir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
6454
+ await fs19.writeFile(path19.join(sourceDir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
6185
6455
  state.warnings.push("This static game had no package.json; added a minimal private manifest for the local CLI. Its source and index.html remain unchanged.");
6186
6456
  }
6187
6457
  if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) throw new RemixError("source_unavailable", "Source package.json is not a project manifest.");
6188
6458
  }
6189
- state.transferEntries = await fs18.readdir(sourceDir);
6459
+ state.transferEntries = await fs19.readdir(sourceDir);
6190
6460
  state.stage = "transfer";
6191
6461
  await saveState(state);
6192
6462
  }
6193
6463
  if (state.stage === "transfer") {
6194
6464
  for (const entry of state.transferEntries ?? []) {
6195
- const from = path18.join(staging, "source", entry), to = path18.join(state.directory, entry);
6196
- if (await fs18.lstat(from).catch(() => null)) {
6197
- if (await fs18.lstat(to).catch(() => null)) throw new RemixError("destination_changed", `A file appeared at ${entry} during source preparation. It was left untouched.`);
6198
- await fs18.rename(from, to);
6465
+ const from = path19.join(staging, "source", entry), to = path19.join(state.directory, entry);
6466
+ if (await fs19.lstat(from).catch(() => null)) {
6467
+ if (await fs19.lstat(to).catch(() => null)) throw new RemixError("destination_changed", `A file appeared at ${entry} during source preparation. It was left untouched.`);
6468
+ await fs19.rename(from, to);
6199
6469
  }
6200
6470
  }
6201
6471
  if (!state.sourceOnly) {
@@ -6204,7 +6474,7 @@ async function runRemix(opts, deps = {}) {
6204
6474
  }
6205
6475
  state.stage = state.sourceOnly ? "prepared" : "configure";
6206
6476
  await saveState(state);
6207
- await fs18.rm(staging, { recursive: true, force: true });
6477
+ await fs19.rm(staging, { recursive: true, force: true });
6208
6478
  }
6209
6479
  process.chdir(state.directory);
6210
6480
  if (state.stage === "configure") {
@@ -6275,8 +6545,8 @@ async function runRemix(opts, deps = {}) {
6275
6545
  }
6276
6546
 
6277
6547
  // src/commands/init.ts
6278
- import fs19 from "fs/promises";
6279
- import path19 from "path";
6548
+ import fs20 from "fs/promises";
6549
+ import path20 from "path";
6280
6550
 
6281
6551
  // src/lib/printed.ts
6282
6552
  var printed = /* @__PURE__ */ new WeakSet();
@@ -6317,7 +6587,7 @@ var TOOLING_ENTRIES = /* @__PURE__ */ new Set([
6317
6587
  ]);
6318
6588
  async function listPreexistingEntries(dir) {
6319
6589
  try {
6320
- return (await fs19.readdir(dir)).filter((name) => !TOOLING_ENTRIES.has(name)).sort();
6590
+ return (await fs20.readdir(dir)).filter((name) => !TOOLING_ENTRIES.has(name)).sort();
6321
6591
  } catch {
6322
6592
  return [];
6323
6593
  }
@@ -6384,8 +6654,8 @@ async function runInit(opts) {
6384
6654
  let totalNew = 0;
6385
6655
  let totalUpdated = 0;
6386
6656
  for (const t of targets) {
6387
- const src = t.full ? templatesDir : path19.join(templatesDir, "skills");
6388
- const dest = t.full ? t.baseDir : path19.join(t.baseDir, "skills");
6657
+ const src = t.full ? templatesDir : path20.join(templatesDir, "skills");
6658
+ const dest = t.full ? t.baseDir : path20.join(t.baseDir, "skills");
6389
6659
  const family = remixing ? skillFamilyFilter("remix") : converting ? skillFamilyFilter("tools", { hosted: true }) : skillFamilyFilter("game");
6390
6660
  if (remixing) await pruneRemixWorkflow(t.baseDir);
6391
6661
  const { copied, updated } = await copyTemplates(src, dest, {
@@ -6393,8 +6663,8 @@ async function runInit(opts) {
6393
6663
  exclude: ["controllers", "motion", "asset-viewer", "blender-service"],
6394
6664
  filter: t.full ? family : (rel) => family(`skills/${rel}`)
6395
6665
  });
6396
- await pruneRemovedSkills(path19.join(t.baseDir, "skills"), log);
6397
- await writeSkillsMarker(path19.join(t.baseDir, "skills"));
6666
+ await pruneRemovedSkills(path20.join(t.baseDir, "skills"), log);
6667
+ await writeSkillsMarker(path20.join(t.baseDir, "skills"));
6398
6668
  const added = copied.length - updated.length;
6399
6669
  totalNew += added;
6400
6670
  totalUpdated += updated.length;
@@ -6452,7 +6722,7 @@ async function runInit(opts) {
6452
6722
  if (email) log.plain(` signed in as ${c.cyan(email)}`);
6453
6723
  };
6454
6724
  await echoIdentity();
6455
- const projectName = opts.name?.trim() || path19.basename(process.cwd());
6725
+ const projectName = opts.name?.trim() || path20.basename(process.cwd());
6456
6726
  const create = (bearer) => createDraftProject({
6457
6727
  apiUrl,
6458
6728
  token: bearer,
@@ -6504,7 +6774,7 @@ async function runInit(opts) {
6504
6774
  if (await writeToolsContract(process.cwd(), { hosted: true })) {
6505
6775
  log.dim(" wrote the Genex Tools publishing rules into AGENTS.md (managed block)");
6506
6776
  }
6507
- await ensureEnvVar(path19.join(process.cwd(), ".env"), "VITE_GENEX_SLUG", meta.slug, log);
6777
+ await ensureEnvVar(path20.join(process.cwd(), ".env"), "VITE_GENEX_SLUG", meta.slug, log);
6508
6778
  await reportCliEvent(apiUrl, token, "tools_converted");
6509
6779
  }
6510
6780
  if (!remixing) warnPreexisting(log, preexisting);
@@ -6531,9 +6801,9 @@ async function runInit(opts) {
6531
6801
  }
6532
6802
 
6533
6803
  // src/commands/link.ts
6534
- import fs20 from "fs/promises";
6804
+ import fs21 from "fs/promises";
6535
6805
  import os6 from "os";
6536
- import path20 from "path";
6806
+ import path21 from "path";
6537
6807
  async function runLink(opts) {
6538
6808
  const log = createLogger({ quiet: opts.quiet });
6539
6809
  log.plain(c.bold("genex link"));
@@ -6615,7 +6885,7 @@ async function runLink(opts) {
6615
6885
  }
6616
6886
  async function isEmptyDir(cwd) {
6617
6887
  try {
6618
- const entries = await fs20.readdir(cwd);
6888
+ const entries = await fs21.readdir(cwd);
6619
6889
  return entries.every((e) => e === ".git" || e === ".genex" || e === ".DS_Store");
6620
6890
  } catch {
6621
6891
  return false;
@@ -6629,13 +6899,13 @@ async function downloadSource(apiUrl, token, project, log) {
6629
6899
  return false;
6630
6900
  }
6631
6901
  log.step(`Downloading the ${grant.sourceRef === "preview" ? "draft" : "published"} source\u2026`);
6632
- const staging = await fs20.mkdtemp(path20.join(os6.tmpdir(), "genex-link-"));
6633
- const fresh = path20.join(staging, "source");
6902
+ const staging = await fs21.mkdtemp(path21.join(os6.tmpdir(), "genex-link-"));
6903
+ const fresh = path21.join(staging, "source");
6634
6904
  try {
6635
6905
  if (!await cloneSource(grant, fresh, log)) return false;
6636
- await fs20.rm(path20.join(fresh, ".git"), { recursive: true, force: true });
6637
- for (const entry of await fs20.readdir(fresh)) {
6638
- await fs20.cp(path20.join(fresh, entry), path20.join(process.cwd(), entry), {
6906
+ await fs21.rm(path21.join(fresh, ".git"), { recursive: true, force: true });
6907
+ for (const entry of await fs21.readdir(fresh)) {
6908
+ await fs21.cp(path21.join(fresh, entry), path21.join(process.cwd(), entry), {
6639
6909
  recursive: true,
6640
6910
  force: true
6641
6911
  });
@@ -6643,28 +6913,28 @@ async function downloadSource(apiUrl, token, project, log) {
6643
6913
  log.success("Downloaded.");
6644
6914
  return true;
6645
6915
  } finally {
6646
- await fs20.rm(staging, { recursive: true, force: true }).catch(() => {
6916
+ await fs21.rm(staging, { recursive: true, force: true }).catch(() => {
6647
6917
  });
6648
6918
  }
6649
6919
  }
6650
6920
  async function ensureSlugEnv(slug, log, cwd = process.cwd()) {
6651
- const file = path20.join(cwd, ".env");
6921
+ const file = path21.join(cwd, ".env");
6652
6922
  let content;
6653
6923
  try {
6654
- content = await fs20.readFile(file, "utf8");
6924
+ content = await fs21.readFile(file, "utf8");
6655
6925
  } catch {
6656
6926
  return;
6657
6927
  }
6658
6928
  const re = /^(\s*VITE_GENEX_SLUG=)(.*)$/m;
6659
6929
  const m = content.match(re);
6660
6930
  if (!m) {
6661
- await fs20.appendFile(file, `${content.endsWith("\n") ? "" : "\n"}VITE_GENEX_SLUG=${slug}
6931
+ await fs21.appendFile(file, `${content.endsWith("\n") ? "" : "\n"}VITE_GENEX_SLUG=${slug}
6662
6932
  `);
6663
6933
  log.dim(` added VITE_GENEX_SLUG=${slug} to .env`);
6664
6934
  return;
6665
6935
  }
6666
6936
  if (m[2].trim() === slug) return;
6667
- await fs20.writeFile(file, content.replace(re, `$1${slug}`));
6937
+ await fs21.writeFile(file, content.replace(re, `$1${slug}`));
6668
6938
  log.dim(` updated .env: VITE_GENEX_SLUG=${slug} (was ${m[2].trim()})`);
6669
6939
  }
6670
6940
  async function fetchOwnProject(apiUrl, token, slug, log) {
@@ -6718,8 +6988,8 @@ async function listOwnSlugs(apiUrl, token, log) {
6718
6988
  }
6719
6989
 
6720
6990
  // src/commands/pull.ts
6721
- import fs21 from "fs/promises";
6722
- import path21 from "path";
6991
+ import fs22 from "fs/promises";
6992
+ import path22 from "path";
6723
6993
  import os7 from "os";
6724
6994
  function isMachineLocal(entry) {
6725
6995
  if (entry === ".genex" || entry === "node_modules" || entry === ".git") return true;
@@ -6779,23 +7049,23 @@ async function runPull(opts) {
6779
7049
  process.exitCode = 1;
6780
7050
  return;
6781
7051
  }
6782
- const staging = await fs21.mkdtemp(path21.join(os7.tmpdir(), "genex-pull-"));
6783
- const fresh = path21.join(staging, "source");
7052
+ const staging = await fs22.mkdtemp(path22.join(os7.tmpdir(), "genex-pull-"));
7053
+ const fresh = path22.join(staging, "source");
6784
7054
  try {
6785
7055
  if (!await cloneSource(grant, fresh, log)) {
6786
7056
  process.exitCode = 1;
6787
7057
  return;
6788
7058
  }
6789
- await fs21.rm(path21.join(fresh, ".git"), { recursive: true, force: true });
7059
+ await fs22.rm(path22.join(fresh, ".git"), { recursive: true, force: true });
6790
7060
  const kept = await keepReplaced(cwd, log);
6791
7061
  await replaceTree(cwd, fresh);
6792
7062
  if (kept) {
6793
7063
  log.plain("");
6794
- log.info(`What was here is kept at ${c.cyan(path21.relative(cwd, kept) || kept)}`);
7064
+ log.info(`What was here is kept at ${c.cyan(path22.relative(cwd, kept) || kept)}`);
6795
7065
  log.dim(" Nothing was thrown away \u2014 re-apply from there, or delete it when you are done.");
6796
7066
  }
6797
7067
  } finally {
6798
- await fs21.rm(staging, { recursive: true, force: true }).catch(() => {
7068
+ await fs22.rm(staging, { recursive: true, force: true }).catch(() => {
6799
7069
  });
6800
7070
  }
6801
7071
  if (await hasRemixProfile(cwd)) await installRemixProfile(cwd, apiUrl, log);
@@ -6815,13 +7085,13 @@ async function runPull(opts) {
6815
7085
  }
6816
7086
  async function keepReplaced(cwd, log) {
6817
7087
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6818
- const dest = path21.join(cwd, ".genex", `replaced-${stamp}`);
7088
+ const dest = path22.join(cwd, ".genex", `replaced-${stamp}`);
6819
7089
  try {
6820
- const entries = (await fs21.readdir(cwd)).filter((e) => !isMachineLocal(e));
7090
+ const entries = (await fs22.readdir(cwd)).filter((e) => !isMachineLocal(e));
6821
7091
  if (entries.length === 0) return null;
6822
- await fs21.mkdir(dest, { recursive: true });
7092
+ await fs22.mkdir(dest, { recursive: true });
6823
7093
  for (const entry of entries) {
6824
- await fs21.cp(path21.join(cwd, entry), path21.join(dest, entry), { recursive: true });
7094
+ await fs22.cp(path22.join(cwd, entry), path22.join(dest, entry), { recursive: true });
6825
7095
  }
6826
7096
  return dest;
6827
7097
  } catch (err) {
@@ -6831,19 +7101,19 @@ async function keepReplaced(cwd, log) {
6831
7101
  }
6832
7102
  }
6833
7103
  async function replaceTree(dest, src) {
6834
- for (const entry of await fs21.readdir(dest)) {
7104
+ for (const entry of await fs22.readdir(dest)) {
6835
7105
  if (isMachineLocal(entry)) continue;
6836
- await fs21.rm(path21.join(dest, entry), { recursive: true, force: true });
7106
+ await fs22.rm(path22.join(dest, entry), { recursive: true, force: true });
6837
7107
  }
6838
- for (const entry of await fs21.readdir(src)) {
7108
+ for (const entry of await fs22.readdir(src)) {
6839
7109
  if (isMachineLocal(entry)) continue;
6840
- await fs21.cp(path21.join(src, entry), path21.join(dest, entry), { recursive: true });
7110
+ await fs22.cp(path22.join(src, entry), path22.join(dest, entry), { recursive: true });
6841
7111
  }
6842
7112
  }
6843
7113
 
6844
7114
  // src/commands/rename.ts
6845
- import fs22 from "fs/promises";
6846
- import path22 from "path";
7115
+ import fs23 from "fs/promises";
7116
+ import path23 from "path";
6847
7117
  async function runRename(opts) {
6848
7118
  const log = createLogger({ quiet: opts.quiet });
6849
7119
  log.plain(c.bold("genex rename"));
@@ -6931,23 +7201,23 @@ async function runRename(opts) {
6931
7201
  log.info("Run `genex preview` (or `publish`) to rebuild \u2014 the new slug is baked into the bundle.");
6932
7202
  }
6933
7203
  async function rewriteSlugEnv(from, to, log, cwd = process.cwd()) {
6934
- const file = path22.join(cwd, ".env");
7204
+ const file = path23.join(cwd, ".env");
6935
7205
  let content;
6936
7206
  try {
6937
- content = await fs22.readFile(file, "utf8");
7207
+ content = await fs23.readFile(file, "utf8");
6938
7208
  } catch {
6939
7209
  return;
6940
7210
  }
6941
7211
  const re = /^(\s*VITE_GENEX_SLUG=)(.*)$/m;
6942
7212
  if (!re.test(content)) return;
6943
- await fs22.writeFile(file, content.replace(re, `$1${to}`));
7213
+ await fs23.writeFile(file, content.replace(re, `$1${to}`));
6944
7214
  log.dim(` .env: VITE_GENEX_SLUG=${to} (was ${from})`);
6945
7215
  }
6946
7216
  async function rewriteBakedSlug(from, to, log, cwd = process.cwd()) {
6947
- const file = path22.join(cwd, "src", "genex.config.ts");
7217
+ const file = path23.join(cwd, "src", "genex.config.ts");
6948
7218
  let content;
6949
7219
  try {
6950
- content = await fs22.readFile(file, "utf8");
7220
+ content = await fs23.readFile(file, "utf8");
6951
7221
  } catch {
6952
7222
  return;
6953
7223
  }
@@ -6957,7 +7227,7 @@ async function rewriteBakedSlug(from, to, log, cwd = process.cwd()) {
6957
7227
  log.warn(` src/genex.config.ts has no "${from}" literal \u2014 check its slug by hand.`);
6958
7228
  return;
6959
7229
  }
6960
- await fs22.writeFile(file, content.replace(quoted, `"${to}"`));
7230
+ await fs23.writeFile(file, content.replace(quoted, `"${to}"`));
6961
7231
  log.dim(` src/genex.config.ts: baked slug -> ${to}`);
6962
7232
  }
6963
7233
 
@@ -7365,7 +7635,7 @@ async function runShop(opts) {
7365
7635
  const auth = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
7366
7636
  const call = (url, init2) => apiFetch(url, { ...init2, headers: { ...auth, ...init2?.headers ?? {} } });
7367
7637
  const projectSkus = `${apiUrl}/api/coin/projects/${encodeURIComponent(meta.id)}/skus`;
7368
- async function fail4(res2, what) {
7638
+ async function fail5(res2, what) {
7369
7639
  if (printedStructuredError(res2)) {
7370
7640
  process.exitCode = 1;
7371
7641
  return;
@@ -7386,7 +7656,7 @@ async function runShop(opts) {
7386
7656
  }
7387
7657
  if (sub === "list") {
7388
7658
  const res2 = await call(projectSkus);
7389
- if (!res2.ok) return fail4(res2, "read your shop");
7659
+ if (!res2.ok) return fail5(res2, "read your shop");
7390
7660
  const body = await res2.json();
7391
7661
  if (opts.json) {
7392
7662
  log.plain(JSON.stringify(body, null, 2));
@@ -7431,7 +7701,7 @@ async function runShop(opts) {
7431
7701
  ...opts.resellable !== void 0 ? { resellable: opts.resellable } : {}
7432
7702
  })
7433
7703
  });
7434
- if (!res2.ok) return fail4(res2, "add that item");
7704
+ if (!res2.ok) return fail5(res2, "add that item");
7435
7705
  const sku = await res2.json();
7436
7706
  if (opts.json) {
7437
7707
  log.plain(JSON.stringify(sku, null, 2));
@@ -7452,7 +7722,7 @@ async function runShop(opts) {
7452
7722
  const skuUrl = `${apiUrl}/api/coin/skus/${encodeURIComponent(skuId)}`;
7453
7723
  if (sub === "test") {
7454
7724
  const res2 = await call(`${skuUrl}/test-grant`, { method: "POST" });
7455
- if (!res2.ok) return fail4(res2, "grant yourself a test copy");
7725
+ if (!res2.ok) return fail5(res2, "grant yourself a test copy");
7456
7726
  const got = await res2.json();
7457
7727
  log.success(`You now own ${c.cyan(got.name)} in this game.`);
7458
7728
  log.dim(" It cost nothing and no sale was recorded \u2014 it is a test copy.");
@@ -7475,14 +7745,14 @@ async function runShop(opts) {
7475
7745
  return;
7476
7746
  }
7477
7747
  const res2 = await call(skuUrl, { method: "PATCH", body: JSON.stringify(patch) });
7478
- if (!res2.ok) return fail4(res2, "update that item");
7748
+ if (!res2.ok) return fail5(res2, "update that item");
7479
7749
  const sku = await res2.json();
7480
7750
  log.success(`Updated ${c.cyan(sku.name)} \u2014 ${sku.priceCoins} coin.`);
7481
7751
  if (opts.resellable === false) log.dim(" Listings already on the market stay up.");
7482
7752
  return;
7483
7753
  }
7484
7754
  const res = await call(skuUrl, { method: "DELETE" });
7485
- if (!res.ok) return fail4(res, "remove that item");
7755
+ if (!res.ok) return fail5(res, "remove that item");
7486
7756
  log.success("Removed from the shop.");
7487
7757
  log.dim(" Players who already bought it keep it.");
7488
7758
  }
@@ -7668,6 +7938,12 @@ async function runPublish(opts) {
7668
7938
  process.exitCode = 1;
7669
7939
  return;
7670
7940
  }
7941
+ const listed = await res.json().catch(() => null);
7942
+ if (opts.regenerateCover && listed?.project?.coverAiEnabled === false) {
7943
+ log.dim(" --regenerate-cover did nothing: this stand doesn't make AI covers.");
7944
+ log.dim(" The cover is a real frame of the game \u2014 set one with `npx genex cover <file>`.");
7945
+ }
7946
+ await pickUpScratchCover({ apiUrl, token, projectId: meta.id, log });
7671
7947
  log.plain("");
7672
7948
  log.success("Published. \u{1F389}");
7673
7949
  const dashboard = meta.dashboardOrigins?.[0];
@@ -7956,9 +8232,101 @@ async function runRollback(opts) {
7956
8232
  log.dim(` To undo this and go forward again: ${c.cyan("npx genex rollback")}`);
7957
8233
  }
7958
8234
 
8235
+ // src/commands/cover.ts
8236
+ import fs24 from "fs/promises";
8237
+ import path24 from "path";
8238
+ var WHAT_A_COVER_IS = "one real frame of the game: its signature moment, real gameplay, well lit, no menus, popups or busy HUD, landscape 16:9, no text";
8239
+ async function runCover(opts) {
8240
+ const log = createLogger({ quiet: opts.quiet || opts.json });
8241
+ log.plain(c.bold("genex cover"));
8242
+ log.plain("");
8243
+ const meta = await readProject();
8244
+ if (!meta) {
8245
+ return fail2("No genex project here. Run `genex init` (or `genex link <slug>`) in this directory first.", opts, log);
8246
+ }
8247
+ const apiUrl = getApiUrl(opts.apiUrl ?? meta.apiUrl);
8248
+ const token = await readUserToken(opts.envPath, apiUrl);
8249
+ if (!token) return fail2("Not signed in. Run `genex auth` first.", opts, log);
8250
+ const file = opts.imagePath?.trim();
8251
+ if (!file) {
8252
+ await showCover(apiUrl, token, meta.slug, opts, log);
8253
+ return;
8254
+ }
8255
+ let bytes;
8256
+ try {
8257
+ const stat = await fs24.stat(file);
8258
+ if (!stat.isFile()) throw new Error("not a file");
8259
+ if (stat.size > COVER_MAX_BYTES) {
8260
+ return finish(
8261
+ { kind: "invalid", message: `${file} is ${(stat.size / 1024 / 1024).toFixed(1)} MB; a cover can be at most ${COVER_MAX_BYTES / 1024 / 1024} MB. Save the frame as a JPEG and try again.` },
8262
+ file,
8263
+ opts,
8264
+ log
8265
+ );
8266
+ }
8267
+ bytes = await fs24.readFile(file);
8268
+ } catch {
8269
+ return finish({ kind: "invalid", message: `Can't read ${file} \u2014 pass the path of a PNG, JPEG or WebP image.` }, file, opts, log);
8270
+ }
8271
+ log.step(`Uploading ${path24.basename(file)} as the cover\u2026`);
8272
+ const outcome2 = await uploadCover({ apiUrl, token, projectId: meta.id, bytes, label: file });
8273
+ await rememberNomination(bytes, file, outcome2);
8274
+ finish(outcome2, file, opts, log);
8275
+ }
8276
+ async function rememberNomination(bytes, file, outcome2) {
8277
+ const scratch = await findScratchCover();
8278
+ if (!scratch || path24.resolve(scratch) === path24.resolve(file)) {
8279
+ await rememberCoverUpload(bytes, file, outcome2);
8280
+ return;
8281
+ }
8282
+ if (outcome2.kind !== "applied" && outcome2.kind !== "outranked") return;
8283
+ const stale = await fs24.readFile(scratch).catch(() => null);
8284
+ if (stale) await rememberCoverUpload(stale, scratch, outcome2);
8285
+ }
8286
+ function fail2(message, opts, log) {
8287
+ if (opts.json) process.stdout.write(`${JSON.stringify({ file: opts.imagePath?.trim() || null, kind: "failed", message })}
8288
+ `);
8289
+ else log.error(message);
8290
+ process.exitCode = 1;
8291
+ }
8292
+ function finish(outcome2, file, opts, log) {
8293
+ if (opts.json) {
8294
+ process.stdout.write(`${JSON.stringify({ file, ...outcome2 })}
8295
+ `);
8296
+ } else {
8297
+ reportCoverOutcome(log, outcome2, {
8298
+ label: file,
8299
+ retry: "Capture a brighter moment of real gameplay and run this again \u2014 or, if the game is dark by design, stop here: never change the game's lighting for a cover.",
8300
+ showUrl: true,
8301
+ asError: true
8302
+ });
8303
+ }
8304
+ if (outcome2.kind !== "applied" && outcome2.kind !== "outranked") process.exitCode = 1;
8305
+ }
8306
+ async function showCover(apiUrl, token, slug, opts, log) {
8307
+ const view = await fetchOwnerProject(apiUrl, token, slug);
8308
+ if (!view) return fail2("Couldn't read this game from Genex \u2014 check the sign-in (`genex doctor`) and try again.", opts, log);
8309
+ if (opts.json) {
8310
+ process.stdout.write(`${JSON.stringify({ coverUrl: view.coverUrl, coverSource: view.coverSource ?? null })}
8311
+ `);
8312
+ return;
8313
+ }
8314
+ if (!view.coverUrl) {
8315
+ log.plain(" This game has no cover yet.");
8316
+ } else {
8317
+ log.plain(` cover: ${c.cyan(view.coverUrl)}`);
8318
+ log.plain(` source: ${describeCoverSource(view.coverSource)}`);
8319
+ }
8320
+ if (!view.coverUrl || coverIsStandIn(view.coverSource)) {
8321
+ log.plain("");
8322
+ log.dim(` A cover is ${WHAT_A_COVER_IS}.`);
8323
+ log.dim(` Save one as ${SCRATCH_DIR}/cover.png and run: npx genex cover ${SCRATCH_DIR}/cover.png`);
8324
+ }
8325
+ }
8326
+
7959
8327
  // src/commands/generate.ts
7960
- import fs25 from "fs/promises";
7961
- import path25 from "path";
8328
+ import fs27 from "fs/promises";
8329
+ import path27 from "path";
7962
8330
  import { PNG as PNG4 } from "pngjs";
7963
8331
 
7964
8332
  // src/lib/glass.ts
@@ -8140,13 +8508,13 @@ function ceilingVerdict(input) {
8140
8508
  }
8141
8509
 
8142
8510
  // src/lib/generation-admission.ts
8143
- import fs24 from "fs/promises";
8144
- import path24 from "path";
8511
+ import fs26 from "fs/promises";
8512
+ import path26 from "path";
8145
8513
  import { createHash, randomUUID } from "crypto";
8146
8514
 
8147
8515
  // src/lib/asset-budget.ts
8148
- import fs23 from "fs/promises";
8149
- import path23 from "path";
8516
+ import fs25 from "fs/promises";
8517
+ import path25 from "path";
8150
8518
  var DEFAULT_ASSET_BUDGET_SHARE = 0.5;
8151
8519
  var DEFAULT_ASSET_BUDGET_CAP = 500;
8152
8520
  function envShare(raw, fallback) {
@@ -8173,10 +8541,10 @@ function allowanceFor(input) {
8173
8541
  if (!Number.isFinite(spendable) || spendable <= 0) return 0;
8174
8542
  return Math.max(0, Math.min(Math.floor(spendable * config.share), config.cap));
8175
8543
  }
8176
- var storePath = (cwd) => path23.join(cwd, ".genex", "asset-budget.json");
8544
+ var storePath = (cwd) => path25.join(cwd, ".genex", "asset-budget.json");
8177
8545
  async function readBudgetStore(cwd = process.cwd()) {
8178
8546
  try {
8179
- const raw = await fs23.readFile(storePath(cwd), "utf8");
8547
+ const raw = await fs25.readFile(storePath(cwd), "utf8");
8180
8548
  const parsed = JSON.parse(raw);
8181
8549
  if (!parsed || parsed.v !== 1 || typeof parsed.allowanceCredits !== "number" || !Number.isFinite(parsed.allowanceCredits) || parsed.setBy !== "default" && parsed.setBy !== "user") {
8182
8550
  return null;
@@ -8195,8 +8563,8 @@ async function readBudgetStore(cwd = process.cwd()) {
8195
8563
  }
8196
8564
  async function writeBudgetStore(cwd, store) {
8197
8565
  try {
8198
- await fs23.access(path23.join(cwd, ".genex"));
8199
- await fs23.writeFile(storePath(cwd), `${JSON.stringify(store, null, 2)}
8566
+ await fs25.access(path25.join(cwd, ".genex"));
8567
+ await fs25.writeFile(storePath(cwd), `${JSON.stringify(store, null, 2)}
8200
8568
  `, "utf8");
8201
8569
  return true;
8202
8570
  } catch {
@@ -8207,7 +8575,7 @@ async function ensureBudgetStore(cwd, input) {
8207
8575
  const existing = await readBudgetStore(cwd);
8208
8576
  if (existing) return existing;
8209
8577
  try {
8210
- await fs23.stat(storePath(cwd));
8578
+ await fs25.stat(storePath(cwd));
8211
8579
  throw new Error("Stored asset allowance is unreadable or invalid; reconcile it before paid generation.");
8212
8580
  } catch (error) {
8213
8581
  if (error.code !== "ENOENT") throw error;
@@ -8311,12 +8679,12 @@ async function assetBudgetGate(input) {
8311
8679
 
8312
8680
  // src/lib/generation-admission.ts
8313
8681
  async function withAdmissionLock(cwd, work, waitMs = 3e4) {
8314
- const lock = path24.join(cwd, ".genex", "generation-admission.lock");
8682
+ const lock = path26.join(cwd, ".genex", "generation-admission.lock");
8315
8683
  const deadline = Date.now() + waitMs;
8316
8684
  let file;
8317
8685
  while (!file) {
8318
8686
  try {
8319
- file = await fs24.open(lock, "wx");
8687
+ file = await fs26.open(lock, "wx");
8320
8688
  } catch (error) {
8321
8689
  if (error.code !== "EEXIST") throw error;
8322
8690
  if (Date.now() >= deadline) throw new Error("Another generation admission holds .genex/generation-admission.lock. If its process died, reconcile pending reservations before removing that lock.");
@@ -8328,7 +8696,7 @@ async function withAdmissionLock(cwd, work, waitMs = 3e4) {
8328
8696
  return await work();
8329
8697
  } finally {
8330
8698
  await file.close();
8331
- await fs24.unlink(lock);
8699
+ await fs26.unlink(lock);
8332
8700
  }
8333
8701
  }
8334
8702
  async function enqueueWithAllowance(input) {
@@ -8577,7 +8945,7 @@ var isRemoteRef = (value) => /^(https?:\/\/|data:)/i.test(value);
8577
8945
  async function inlineLocalImage(filePath, flag) {
8578
8946
  let bytes;
8579
8947
  try {
8580
- bytes = await fs25.readFile(filePath);
8948
+ bytes = await fs27.readFile(filePath);
8581
8949
  } catch {
8582
8950
  return { ok: false, error: `Couldn't read the ${flag} file at ${filePath}.` };
8583
8951
  }
@@ -8587,7 +8955,7 @@ async function inlineLocalImage(filePath, flag) {
8587
8955
  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.`
8588
8956
  };
8589
8957
  }
8590
- const mime = IMAGE_MIME_BY_EXT[path25.extname(filePath).toLowerCase()] ?? "image/png";
8958
+ const mime = IMAGE_MIME_BY_EXT[path27.extname(filePath).toLowerCase()] ?? "image/png";
8591
8959
  return { ok: true, dataUri: `data:${mime};base64,${bytes.toString("base64")}` };
8592
8960
  }
8593
8961
  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.";
@@ -8782,7 +9150,7 @@ async function runGenerate(kind, opts) {
8782
9150
  let typedPrompt = opts.prompt?.trim();
8783
9151
  if (!typedPrompt && kind === "model" && opts.imageUrl) {
8784
9152
  const ref = opts.imageUrl.startsWith("data:") ? "local image" : opts.imageUrl;
8785
- typedPrompt = `from image: ${path25.basename(ref).slice(0, 120)}`;
9153
+ typedPrompt = `from image: ${path27.basename(ref).slice(0, 120)}`;
8786
9154
  }
8787
9155
  if (kind === "model" && opts.texture !== void 0 && !MODEL_TEXTURE_TIERS.includes(opts.texture)) {
8788
9156
  log.error(`--texture ${opts.texture} is a character texture size. \`genex model\` takes a texture TIER: ${MODEL_TEXTURE_TIERS.join("|")} (default detailed).`);
@@ -8831,7 +9199,7 @@ async function runGenerate(kind, opts) {
8831
9199
  return;
8832
9200
  }
8833
9201
  try {
8834
- const bytes = await fs25.readFile(opts.inpaintUrl);
9202
+ const bytes = await fs27.readFile(opts.inpaintUrl);
8835
9203
  opts = { ...opts, inpaintUrl: `data:image/png;base64,${bytes.toString("base64")}` };
8836
9204
  } catch {
8837
9205
  log.error(`Couldn't read the --inpaint mask at ${opts.inpaintUrl}.`);
@@ -9000,7 +9368,7 @@ async function reportGlassTerminal(view, outDir, log, json) {
9000
9368
  return;
9001
9369
  }
9002
9370
  await recordTerminal(view.id, "completed", files.map((f) => f.url));
9003
- await fs25.mkdir(outDir, { recursive: true });
9371
+ await fs27.mkdir(outDir, { recursive: true });
9004
9372
  const solved = [];
9005
9373
  for (let i = 0; i < files.length; i++) {
9006
9374
  const f = files[i];
@@ -9032,8 +9400,8 @@ async function reportGlassTerminal(view, outDir, log, json) {
9032
9400
  });
9033
9401
  continue;
9034
9402
  }
9035
- const outPath = path25.join(outDir, `glass-${i + 1}.png`);
9036
- await fs25.writeFile(outPath, PNG4.sync.write(r.png));
9403
+ const outPath = path27.join(outDir, `glass-${i + 1}.png`);
9404
+ await fs27.writeFile(outPath, PNG4.sync.write(r.png));
9037
9405
  solved.push({
9038
9406
  path: outPath,
9039
9407
  url: f.url,
@@ -9663,8 +10031,8 @@ function writeJson(value) {
9663
10031
  }
9664
10032
 
9665
10033
  // src/commands/model-sub.ts
9666
- import fs26 from "fs/promises";
9667
- import path26 from "path";
10034
+ import fs28 from "fs/promises";
10035
+ import path28 from "path";
9668
10036
  var MODEL_SUBCOMMANDS = ["segment", "rig", "animate", "import"];
9669
10037
  function apiErrorMessage(data, fallback) {
9670
10038
  if (typeof data !== "object" || data === null) return fallback;
@@ -9679,29 +10047,29 @@ async function importModelFile(args) {
9679
10047
  const { log } = args;
9680
10048
  const filePath = args.filePath.trim();
9681
10049
  if (!/\.glb$/i.test(filePath)) {
9682
- log.error(`\`import\` takes a .glb file (binary glTF 2.0). Export ${path26.basename(filePath) || "the model"} as GLB first \u2014 Blender: File \u2192 Export \u2192 glTF 2.0, format "glTF Binary".`);
10050
+ log.error(`\`import\` takes a .glb file (binary glTF 2.0). Export ${path28.basename(filePath) || "the model"} as GLB first \u2014 Blender: File \u2192 Export \u2192 glTF 2.0, format "glTF Binary".`);
9683
10051
  return null;
9684
10052
  }
9685
10053
  let bytes;
9686
10054
  try {
9687
- bytes = await fs26.readFile(filePath);
10055
+ bytes = await fs28.readFile(filePath);
9688
10056
  } catch {
9689
10057
  log.error(`Couldn't read ${filePath}.`);
9690
10058
  return null;
9691
10059
  }
9692
10060
  if (bytes.byteLength > MODEL_IMPORT_MAX_BYTES) {
9693
- log.error(`${path26.basename(filePath)} is ${(bytes.byteLength / 1e6).toFixed(1)} MB; imports are capped at ${MODEL_IMPORT_MAX_BYTES / 1024 / 1024} MB. Shrink its textures (they are usually the bulk) and try again.`);
10061
+ log.error(`${path28.basename(filePath)} is ${(bytes.byteLength / 1e6).toFixed(1)} MB; imports are capped at ${MODEL_IMPORT_MAX_BYTES / 1024 / 1024} MB. Shrink its textures (they are usually the bulk) and try again.`);
9694
10062
  return null;
9695
10063
  }
9696
10064
  if (bytes.byteLength < 12 || bytes.readUInt32LE(0) !== GLB_MAGIC) {
9697
- log.error(`${path26.basename(filePath)} is not a GLB (no glTF magic). A .gltf + .bin pair must be exported as one binary .glb.`);
10065
+ log.error(`${path28.basename(filePath)} is not a GLB (no glTF magic). A .gltf + .bin pair must be exported as one binary .glb.`);
9698
10066
  return null;
9699
10067
  }
9700
10068
  const headers = { "Content-Type": "application/json", Authorization: `Bearer ${args.token}` };
9701
10069
  const minted = await apiFetch(`${args.apiUrl}/api/generations/import`, {
9702
10070
  method: "POST",
9703
10071
  headers,
9704
- body: JSON.stringify({ filename: path26.basename(filePath), bytes: bytes.byteLength, contentType: "model/gltf-binary" })
10072
+ body: JSON.stringify({ filename: path28.basename(filePath), bytes: bytes.byteLength, contentType: "model/gltf-binary" })
9705
10073
  });
9706
10074
  if (printedStructuredError(minted)) return null;
9707
10075
  if (!minted.ok) {
@@ -9710,7 +10078,7 @@ async function importModelFile(args) {
9710
10078
  return null;
9711
10079
  }
9712
10080
  const { id, uploadUrl, url } = await minted.json();
9713
- log.dim(` uploading ${path26.basename(filePath)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
10081
+ log.dim(` uploading ${path28.basename(filePath)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
9714
10082
  const put = await fetch(uploadUrl, {
9715
10083
  method: "PUT",
9716
10084
  headers: { "Content-Type": "model/gltf-binary", "Content-Length": String(bytes.byteLength) },
@@ -9858,7 +10226,7 @@ async function runModelAnimate(opts) {
9858
10226
  }
9859
10227
 
9860
10228
  // src/commands/wait.ts
9861
- import path27 from "path";
10229
+ import path29 from "path";
9862
10230
  var MAX_TIMEOUT_SEC = 2e6;
9863
10231
  var STILL_RUNNING_EXIT = 2;
9864
10232
  var TERMINAL2 = /* @__PURE__ */ new Set(["completed", "failed"]);
@@ -10065,7 +10433,7 @@ async function runWaitAll(opts) {
10065
10433
  if (v?.status !== "completed" || !v.files?.length) continue;
10066
10434
  const local = localDeliveryFor("tools", { ...opts, outDir: opts.outDir ?? e.outDir }, e.prompt || e.kind);
10067
10435
  if (!local) continue;
10068
- const outDir = path27.join(path27.relative(process.cwd(), cwd) || ".", local.outDir);
10436
+ const outDir = path29.join(path29.relative(process.cwd(), cwd) || ".", local.outDir);
10069
10437
  const target = { kind: e.kind, prompt: local.prompt, id: e.id, outDir };
10070
10438
  const missing = await undeliveredFiles(v.files, target);
10071
10439
  if (missing.length === 0) continue;
@@ -10177,8 +10545,8 @@ async function toRow(e, v, cwd) {
10177
10545
  }
10178
10546
 
10179
10547
  // src/commands/controller.ts
10180
- import fs28 from "fs/promises";
10181
- import path29 from "path";
10548
+ import fs30 from "fs/promises";
10549
+ import path31 from "path";
10182
10550
 
10183
10551
  // ../../packages/meshy-animation-catalog/src/index.ts
10184
10552
  import { createHash as createHash2 } from "crypto";
@@ -19308,9 +19676,9 @@ function searchMeshyAnimations(query, options = {}) {
19308
19676
  }
19309
19677
 
19310
19678
  // src/lib/anims.ts
19311
- import fs27 from "fs/promises";
19312
- import path28 from "path";
19313
- var ANIMS_DEST = path28.join("public", "assets", "anims");
19679
+ import fs29 from "fs/promises";
19680
+ import path30 from "path";
19681
+ var ANIMS_DEST = path30.join("public", "assets", "anims");
19314
19682
  var HIDDEN_TAG = "reference";
19315
19683
  async function runAnims(opts) {
19316
19684
  const log = createLogger({ quiet: opts.quiet });
@@ -19326,7 +19694,7 @@ async function runAnims(opts) {
19326
19694
  printCatalog(log, manifest, selectors);
19327
19695
  return;
19328
19696
  }
19329
- const controllerMarker = path28.join(root, "src", "controllers", "character");
19697
+ const controllerMarker = path30.join(root, "src", "controllers", "character");
19330
19698
  if (!await exists3(controllerMarker)) {
19331
19699
  log.error(
19332
19700
  `No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
@@ -19335,11 +19703,11 @@ async function runAnims(opts) {
19335
19703
  process.exitCode = 1;
19336
19704
  return;
19337
19705
  }
19338
- const destDir = path28.join(root, ANIMS_DEST);
19339
- const gameManifestPath = path28.join(destDir, "manifest.json");
19706
+ const destDir = path30.join(root, ANIMS_DEST);
19707
+ const gameManifestPath = path30.join(destDir, "manifest.json");
19340
19708
  if (opts.reset) {
19341
- await fs27.rm(destDir, { recursive: true, force: true });
19342
- log.step(`Cleared ${c.cyan(ANIMS_DEST + path28.sep)} (--reset)`);
19709
+ await fs29.rm(destDir, { recursive: true, force: true });
19710
+ log.step(`Cleared ${c.cyan(ANIMS_DEST + path30.sep)} (--reset)`);
19343
19711
  }
19344
19712
  if (selectors.length === 0) {
19345
19713
  const installed = await readGameManifest(gameManifestPath);
@@ -19377,35 +19745,35 @@ async function runAnims(opts) {
19377
19745
  }
19378
19746
  }
19379
19747
  const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
19380
- const cacheDir = path28.join(
19748
+ const cacheDir = path30.join(
19381
19749
  opts.cacheDir ?? getAnimsCacheDir(),
19382
19750
  `${manifest.library}-v${manifest.version}`
19383
19751
  );
19384
- await fs27.mkdir(cacheDir, { recursive: true });
19385
- await fs27.mkdir(destDir, { recursive: true });
19752
+ await fs29.mkdir(cacheDir, { recursive: true });
19753
+ await fs29.mkdir(destDir, { recursive: true });
19386
19754
  const base = getAnimsBase(opts.animsBase);
19387
19755
  let installedCount = 0;
19388
19756
  let presentCount = 0;
19389
19757
  let addedBytes = 0;
19390
19758
  const failures = [];
19391
19759
  for (const entry of wanted) {
19392
- const dest = path28.join(destDir, entry.file);
19760
+ const dest = path30.join(destDir, entry.file);
19393
19761
  if (await hasSize(dest, entry.bytes)) {
19394
19762
  presentCount++;
19395
19763
  continue;
19396
19764
  }
19397
19765
  try {
19398
- const cached = path28.join(cacheDir, entry.file);
19766
+ const cached = path30.join(cacheDir, entry.file);
19399
19767
  if (!await hasSize(cached, entry.bytes)) {
19400
19768
  const res = await fetch(base + entry.file);
19401
19769
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
19402
19770
  const buf = Buffer.from(await res.arrayBuffer());
19403
- await fs27.writeFile(cached, buf);
19771
+ await fs29.writeFile(cached, buf);
19404
19772
  }
19405
- await fs27.copyFile(cached, dest);
19773
+ await fs29.copyFile(cached, dest);
19406
19774
  installedCount++;
19407
19775
  addedBytes += entry.bytes;
19408
- log.dim(` ${path28.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
19776
+ log.dim(` ${path30.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
19409
19777
  } catch (err) {
19410
19778
  failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
19411
19779
  }
@@ -19421,13 +19789,13 @@ async function runAnims(opts) {
19421
19789
  version: manifest.version,
19422
19790
  clips: [...union].sort((a, b) => a.localeCompare(b))
19423
19791
  };
19424
- await fs27.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
19792
+ await fs29.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
19425
19793
  log.plain("");
19426
19794
  const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
19427
19795
  if (presentCount > 0) parts.push(`${presentCount} already present`);
19428
19796
  if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
19429
19797
  log.success(
19430
- `${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path28.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
19798
+ `${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path30.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
19431
19799
  );
19432
19800
  for (const [selector, entries] of resolved) {
19433
19801
  const names2 = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
@@ -19459,8 +19827,8 @@ async function loadManifest(baseOverride) {
19459
19827
  }
19460
19828
  } catch {
19461
19829
  }
19462
- const snapshotPath = path28.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
19463
- const manifest = JSON.parse(await fs27.readFile(snapshotPath, "utf8"));
19830
+ const snapshotPath = path30.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
19831
+ const manifest = JSON.parse(await fs29.readFile(snapshotPath, "utf8"));
19464
19832
  return { manifest, source: "snapshot" };
19465
19833
  }
19466
19834
  function resolveSelectors(manifest, selectors) {
@@ -19578,21 +19946,21 @@ function printCatalog(log, manifest, selectors) {
19578
19946
  }
19579
19947
  async function readGameManifest(file) {
19580
19948
  try {
19581
- return JSON.parse(await fs27.readFile(file, "utf8"));
19949
+ return JSON.parse(await fs29.readFile(file, "utf8"));
19582
19950
  } catch {
19583
19951
  return null;
19584
19952
  }
19585
19953
  }
19586
19954
  async function hasSize(file, bytes) {
19587
19955
  try {
19588
- return (await fs27.stat(file)).size === bytes;
19956
+ return (await fs29.stat(file)).size === bytes;
19589
19957
  } catch {
19590
19958
  return false;
19591
19959
  }
19592
19960
  }
19593
19961
  async function exists3(p) {
19594
19962
  try {
19595
- await fs27.access(p);
19963
+ await fs29.access(p);
19596
19964
  return true;
19597
19965
  } catch {
19598
19966
  return false;
@@ -19793,8 +20161,8 @@ var CONTROLLER_FILE_SETS = {
19793
20161
  ]
19794
20162
  }
19795
20163
  };
19796
- var CODE_DEST = path29.join("src", "controllers");
19797
- var ASSETS_DEST = path29.join("public", "assets");
20164
+ var CODE_DEST = path31.join("src", "controllers");
20165
+ var ASSETS_DEST = path31.join("public", "assets");
19798
20166
  async function runController(opts) {
19799
20167
  const log = createLogger({ quiet: opts.quiet });
19800
20168
  if (opts.kind?.trim() === "anims") {
@@ -19811,31 +20179,31 @@ async function runController(opts) {
19811
20179
  process.exitCode = 1;
19812
20180
  return;
19813
20181
  }
19814
- const srcDir = path29.join(getTemplatesDir(), "controllers");
20182
+ const srcDir = path31.join(getTemplatesDir(), "controllers");
19815
20183
  const root = opts.cwd ?? process.cwd();
19816
20184
  const set = CONTROLLER_FILE_SETS[kind];
19817
20185
  log.plain(c.bold(`genex controller ${kind}`));
19818
20186
  log.plain("");
19819
- log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path29.sep)}`);
20187
+ log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path31.sep)}`);
19820
20188
  const plan = [
19821
- ...set.code.map((rel) => ({ from: rel, rel: path29.join(CODE_DEST, rel) })),
20189
+ ...set.code.map((rel) => ({ from: rel, rel: path31.join(CODE_DEST, rel) })),
19822
20190
  ...set.assets.map((rel) => ({
19823
20191
  from: rel,
19824
- rel: path29.join(ASSETS_DEST, path29.basename(rel))
20192
+ rel: path31.join(ASSETS_DEST, path31.basename(rel))
19825
20193
  }))
19826
20194
  ];
19827
20195
  let copied = 0;
19828
20196
  let skipped = 0;
19829
20197
  try {
19830
20198
  for (const file of plan) {
19831
- const dest = path29.join(root, file.rel);
20199
+ const dest = path31.join(root, file.rel);
19832
20200
  if (!opts.force && await exists4(dest)) {
19833
20201
  skipped++;
19834
20202
  log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
19835
20203
  continue;
19836
20204
  }
19837
- await fs28.mkdir(path29.dirname(dest), { recursive: true });
19838
- await fs28.copyFile(path29.join(srcDir, file.from), dest);
20205
+ await fs30.mkdir(path31.dirname(dest), { recursive: true });
20206
+ await fs30.copyFile(path31.join(srcDir, file.from), dest);
19839
20207
  copied++;
19840
20208
  log.dim(` ${file.rel}`);
19841
20209
  }
@@ -19888,7 +20256,7 @@ async function runController(opts) {
19888
20256
  for (const line of set.sketch) {
19889
20257
  log.dim(` ${line}`);
19890
20258
  }
19891
- if (kind === "character" && !await exists4(path29.join(root, ASSETS_DEST, "meshy-character.json"))) {
20259
+ if (kind === "character" && !await exists4(path31.join(root, ASSETS_DEST, "meshy-character.json"))) {
19892
20260
  log.plain("");
19893
20261
  log.plain(
19894
20262
  ` ${stepOffset + 3}. This game has no generated character yet \u2014 ${c.cyan(
@@ -19920,9 +20288,9 @@ async function installMeshyCharacterManifest(args) {
19920
20288
  throw new Error("The API returned an invalid Meshy character manifest.");
19921
20289
  }
19922
20290
  assertCompleteMeshyControllerPack(manifest);
19923
- const destination = path29.join(args.root, ASSETS_DEST, "meshy-character.json");
19924
- await fs28.mkdir(path29.dirname(destination), { recursive: true });
19925
- await fs28.writeFile(
20291
+ const destination = path31.join(args.root, ASSETS_DEST, "meshy-character.json");
20292
+ await fs30.mkdir(path31.dirname(destination), { recursive: true });
20293
+ await fs30.writeFile(
19926
20294
  destination,
19927
20295
  `${JSON.stringify(manifest, null, 2)}
19928
20296
  `
@@ -20067,14 +20435,14 @@ function assertCompleteMeshyControllerPack(manifest) {
20067
20435
  }
20068
20436
  async function installFallbackAvatar(args) {
20069
20437
  const { root, srcDir, log } = args;
20070
- const dest = path29.join(root, ASSETS_DEST, "avatar.vrm");
20071
- await fs28.mkdir(path29.dirname(dest), { recursive: true });
20072
- await fs28.copyFile(path29.join(srcDir, "assets", "default-avatar.vrm"), dest);
20438
+ const dest = path31.join(root, ASSETS_DEST, "avatar.vrm");
20439
+ await fs30.mkdir(path31.dirname(dest), { recursive: true });
20440
+ await fs30.copyFile(path31.join(srcDir, "assets", "default-avatar.vrm"), dest);
20073
20441
  log.dim(" public/assets/avatar.vrm (fallback avatar \u2014 bundled CC0 default)");
20074
20442
  }
20075
20443
  async function exists4(p) {
20076
20444
  try {
20077
- await fs28.access(p);
20445
+ await fs30.access(p);
20078
20446
  return true;
20079
20447
  } catch {
20080
20448
  return false;
@@ -20082,8 +20450,8 @@ async function exists4(p) {
20082
20450
  }
20083
20451
 
20084
20452
  // src/commands/character.ts
20085
- import fs29 from "fs/promises";
20086
- import path30 from "path";
20453
+ import fs31 from "fs/promises";
20454
+ import path32 from "path";
20087
20455
  function exactAnimation(selector) {
20088
20456
  const trimmed = selector.trim();
20089
20457
  if (/^-?\d+$/.test(trimmed)) return animationById(Number(trimmed));
@@ -20164,7 +20532,7 @@ function showAmbiguity(selector, candidates, log, json = false) {
20164
20532
  }
20165
20533
  process.exitCode = 1;
20166
20534
  }
20167
- var INSTALLED_MANIFEST = path30.join("public", "assets", "meshy-character.json");
20535
+ var INSTALLED_MANIFEST = path32.join("public", "assets", "meshy-character.json");
20168
20536
  async function resolveAdoptTarget(selector) {
20169
20537
  const trimmed = selector?.trim();
20170
20538
  if (trimmed && !trimmed.endsWith(".json")) {
@@ -20173,7 +20541,7 @@ async function resolveAdoptTarget(selector) {
20173
20541
  const file = trimmed ?? INSTALLED_MANIFEST;
20174
20542
  let raw;
20175
20543
  try {
20176
- raw = await fs29.readFile(file, "utf8");
20544
+ raw = await fs31.readFile(file, "utf8");
20177
20545
  } catch {
20178
20546
  return {
20179
20547
  ok: false,
@@ -20197,12 +20565,12 @@ async function runCharacterAdopt(opts) {
20197
20565
  const log = createLogger({ quiet: opts.quiet || opts.json });
20198
20566
  const target = await resolveAdoptTarget(opts.characterId);
20199
20567
  if (!target.ok) {
20200
- fail2(opts, target.message, "character");
20568
+ fail3(opts, target.message, "character");
20201
20569
  return;
20202
20570
  }
20203
20571
  const ctx = await context(opts);
20204
20572
  if (!ctx) {
20205
- fail2(opts, "Not authorized. Run `genex init` first to sign in.", "character");
20573
+ fail3(opts, "Not authorized. Run `genex init` first to sign in.", "character");
20206
20574
  return;
20207
20575
  }
20208
20576
  const ws = await resolveWorkspace();
@@ -20213,7 +20581,7 @@ async function runCharacterAdopt(opts) {
20213
20581
  }
20214
20582
  const project = await readProject();
20215
20583
  if (!project?.id) {
20216
- fail2(
20584
+ fail3(
20217
20585
  opts,
20218
20586
  "No linked game here. Run this from the game folder that remixed the original (`genex link` if it is not linked yet).",
20219
20587
  "character"
@@ -20258,20 +20626,20 @@ async function runCharacterImport(opts) {
20258
20626
  const log = createLogger({ quiet: opts.quiet || opts.json });
20259
20627
  const filePath = opts.importPath?.trim();
20260
20628
  if (!filePath) {
20261
- fail2(opts, "Missing file. Usage: genex character import <file.glb> [--height 1.8] [--no-fingers]");
20629
+ fail3(opts, "Missing file. Usage: genex character import <file.glb> [--height 1.8] [--no-fingers]");
20262
20630
  return;
20263
20631
  }
20264
20632
  if (opts.texture !== void 0 || opts.ultra === false || opts.pose !== void 0 || opts.polycount !== void 0) {
20265
- fail2(opts, "--texture / --no-ultra / --pose / --polycount are Meshy generation knobs; an import keeps your mesh exactly as it is. Uthana only adds the skeleton (--no-fingers skips finger joints).");
20633
+ fail3(opts, "--texture / --no-ultra / --pose / --polycount are Meshy generation knobs; an import keeps your mesh exactly as it is. Uthana only adds the skeleton (--no-fingers skips finger joints).");
20266
20634
  return;
20267
20635
  }
20268
20636
  if (opts.controllerPack === false || (opts.actions?.length ?? 0) > 0) {
20269
- fail2(opts, "An imported character has no Meshy controller pack and takes no catalog --animation: its motion comes from `genex character animate <id> --locomotion` and plain-word verbs.");
20637
+ fail3(opts, "An imported character has no Meshy controller pack and takes no catalog --animation: its motion comes from `genex character animate <id> --locomotion` and plain-word verbs.");
20270
20638
  return;
20271
20639
  }
20272
20640
  const ctx = await context(opts);
20273
20641
  if (!ctx) {
20274
- fail2(opts, "Not authorized. Run `genex init` first to sign in.");
20642
+ fail3(opts, "Not authorized. Run `genex init` first to sign in.");
20275
20643
  return;
20276
20644
  }
20277
20645
  const imported = await importModelFile({ apiUrl: ctx.apiUrl, token: ctx.token, filePath, log });
@@ -20298,7 +20666,7 @@ async function runCharacterImport(opts) {
20298
20666
  opts,
20299
20667
  ctx,
20300
20668
  kind: "character",
20301
- prompt: `Import ${path30.basename(filePath)} as a rigged character`,
20669
+ prompt: `Import ${path32.basename(filePath)} as a rigged character`,
20302
20670
  createPath: "/api/characters/import",
20303
20671
  body,
20304
20672
  quote: price,
@@ -20334,16 +20702,16 @@ async function runCharacter(opts) {
20334
20702
  const references = opts.imageUrls ?? (opts.imageUrl === void 0 ? [] : [opts.imageUrl]);
20335
20703
  if (references.length > 0) {
20336
20704
  if (opts.directText) {
20337
- fail2(opts, "--image cannot be combined with --direct-text. Omit --direct-text to build from the supplied references.");
20705
+ fail3(opts, "--image cannot be combined with --direct-text. Omit --direct-text to build from the supplied references.");
20338
20706
  return;
20339
20707
  }
20340
20708
  if (opts.candidate !== void 0 || opts.userApproved !== void 0 || opts.approveRemesh !== void 0) {
20341
- fail2(opts, "--image starts a one-shot character. --candidate / --user-approved / --approve-remesh belong to the guided preview/finalize flow; use --polycount for the reference character's face budget.");
20709
+ fail3(opts, "--image starts a one-shot character. --candidate / --user-approved / --approve-remesh belong to the guided preview/finalize flow; use --polycount for the reference character's face budget.");
20342
20710
  return;
20343
20711
  }
20344
20712
  const prepared = await prepareCharacterImages(references);
20345
20713
  if (!prepared.ok) {
20346
- fail2(opts, prepared.error);
20714
+ fail3(opts, prepared.error);
20347
20715
  return;
20348
20716
  }
20349
20717
  await runOneShotCharacter(opts, prepared.imageUrls);
@@ -20382,14 +20750,14 @@ async function prepareCharacterImages(references) {
20382
20750
  }
20383
20751
  return { ok: true, imageUrls };
20384
20752
  }
20385
- function fail2(opts, message, kind = "character") {
20753
+ function fail3(opts, message, kind = "character") {
20386
20754
  if (opts.json) writeJson({ kind, status: "failed", error: message });
20387
20755
  else createLogger({ quiet: opts.quiet }).error(message);
20388
20756
  process.exitCode = 1;
20389
20757
  }
20390
20758
  function refuseSubcommandImages(opts) {
20391
20759
  if (opts.imageUrl === void 0 && !opts.imageUrls?.length) return false;
20392
- fail2(opts, "--image starts a new reference character; it cannot be combined with a character subcommand.");
20760
+ fail3(opts, "--image starts a new reference character; it cannot be combined with a character subcommand.");
20393
20761
  return true;
20394
20762
  }
20395
20763
  function requestedBuild(opts, actionIds) {
@@ -20450,7 +20818,7 @@ async function runWorkflow(args) {
20450
20818
  } : (progress) => log.dim(` ${progress}%`)
20451
20819
  );
20452
20820
  if (!view) {
20453
- fail2(args.opts, "Timed out waiting for the character workflow.", args.kind);
20821
+ fail3(args.opts, "Timed out waiting for the character workflow.", args.kind);
20454
20822
  return;
20455
20823
  }
20456
20824
  if (view.status !== "completed") {
@@ -20464,11 +20832,11 @@ async function runCharacterConcept(opts) {
20464
20832
  const log = createLogger({ quiet: opts.quiet || opts.json });
20465
20833
  const prompt = opts.prompt?.trim();
20466
20834
  if (!prompt) {
20467
- fail2(opts, 'Missing brief. Usage: genex character "<brief>".', "character_concept");
20835
+ fail3(opts, 'Missing brief. Usage: genex character "<brief>".', "character_concept");
20468
20836
  return;
20469
20837
  }
20470
20838
  if (prompt.length > CHARACTER_BRIEF_MAX_CHARS) {
20471
- fail2(
20839
+ fail3(
20472
20840
  opts,
20473
20841
  `The brief is ${prompt.length} characters \u2014 the character concept endpoint caps it at ${CHARACTER_BRIEF_MAX_CHARS}. Tighten it to the look that matters (silhouette, materials, palette, mood); gameplay details don't belong in the brief.`,
20474
20842
  "character_concept"
@@ -20476,15 +20844,15 @@ async function runCharacterConcept(opts) {
20476
20844
  return;
20477
20845
  }
20478
20846
  if (opts.polycount !== void 0) {
20479
- fail2(opts, "--polycount is only available with --direct-text or --image. The approval flow takes its face budget on `character finalize --approve-remesh <faces>`.", "character_concept");
20847
+ fail3(opts, "--polycount is only available with --direct-text or --image. The approval flow takes its face budget on `character finalize --approve-remesh <faces>`.", "character_concept");
20480
20848
  return;
20481
20849
  }
20482
20850
  if (opts.texture !== void 0 || opts.ultra === false || opts.pose !== void 0) {
20483
- fail2(opts, "--texture / --no-ultra / --pose apply to the paid mesh call: pass them to `character preview` (guided) or `character --direct-text` / `character --image` / `creature` (one shot). Concepts are images.", "character_concept");
20851
+ fail3(opts, "--texture / --no-ultra / --pose apply to the paid mesh call: pass them to `character preview` (guided) or `character --direct-text` / `character --image` / `creature` (one shot). Concepts are images.", "character_concept");
20484
20852
  return;
20485
20853
  }
20486
20854
  if (opts.controllerPack === false) {
20487
- fail2(opts, "--no-controller-pack is only available with --direct-text or --image. The guided Meshy parity flow always installs neutral-v3.", "character_concept");
20855
+ fail3(opts, "--no-controller-pack is only available with --direct-text or --image. The guided Meshy parity flow always installs neutral-v3.", "character_concept");
20488
20856
  return;
20489
20857
  }
20490
20858
  const resolved = resolveActionSelectors(opts.actions ?? []);
@@ -20494,7 +20862,7 @@ async function runCharacterConcept(opts) {
20494
20862
  }
20495
20863
  const ctx = await context(opts);
20496
20864
  if (!ctx) {
20497
- fail2(opts, "Not authorized. Run `genex init` first to sign in.", "character_concept");
20865
+ fail3(opts, "Not authorized. Run `genex init` first to sign in.", "character_concept");
20498
20866
  return;
20499
20867
  }
20500
20868
  const build = requestedBuild(opts, resolved.actionIds);
@@ -20505,7 +20873,7 @@ async function runCharacterConcept(opts) {
20505
20873
  return;
20506
20874
  }
20507
20875
  if (price.candidateCount !== 3) {
20508
- fail2(opts, `The concept quote returned ${price.candidateCount} candidates; this workflow requires exactly 3.`, "character_concept");
20876
+ fail3(opts, `The concept quote returned ${price.candidateCount} candidates; this workflow requires exactly 3.`, "character_concept");
20509
20877
  return;
20510
20878
  }
20511
20879
  if (!opts.json) {
@@ -20533,20 +20901,20 @@ async function runOneShotCharacter(opts, imageUrls) {
20533
20901
  const log = createLogger({ quiet: opts.quiet || opts.json });
20534
20902
  const prompt = opts.prompt?.trim() || (imageUrls ? "Character from supplied reference images" : void 0);
20535
20903
  if (!prompt) {
20536
- fail2(opts, 'Missing prompt. Usage: genex character "<prompt>" --direct-text.');
20904
+ fail3(opts, 'Missing prompt. Usage: genex character "<prompt>" --direct-text.');
20537
20905
  return;
20538
20906
  }
20539
20907
  const badTexture = badCharacterTexture(opts);
20540
20908
  if (badTexture) {
20541
- fail2(opts, badTexture);
20909
+ fail3(opts, badTexture);
20542
20910
  return;
20543
20911
  }
20544
20912
  if (opts.polycount !== void 0 && (!Number.isInteger(opts.polycount) || opts.polycount < REMESH_TARGET_MIN || opts.polycount > REMESH_TARGET_MAX)) {
20545
- fail2(opts, `--polycount must be an integer between ${REMESH_TARGET_MIN} and ${REMESH_TARGET_MAX}.`);
20913
+ fail3(opts, `--polycount must be an integer between ${REMESH_TARGET_MIN} and ${REMESH_TARGET_MAX}.`);
20546
20914
  return;
20547
20915
  }
20548
20916
  if (opts.pose !== void 0 && !["a-pose", "t-pose"].includes(opts.pose)) {
20549
- fail2(opts, "--pose must be a-pose or t-pose.");
20917
+ fail3(opts, "--pose must be a-pose or t-pose.");
20550
20918
  return;
20551
20919
  }
20552
20920
  const resolved = resolveActionSelectors(opts.actions ?? []);
@@ -20556,7 +20924,7 @@ async function runOneShotCharacter(opts, imageUrls) {
20556
20924
  }
20557
20925
  const ctx = await context(opts);
20558
20926
  if (!ctx) {
20559
- fail2(opts, "Not authorized. Run `genex init` first to sign in.");
20927
+ fail3(opts, "Not authorized. Run `genex init` first to sign in.");
20560
20928
  return;
20561
20929
  }
20562
20930
  const controllerPack = opts.controllerPack !== false;
@@ -20575,7 +20943,7 @@ async function runOneShotCharacter(opts, imageUrls) {
20575
20943
  return;
20576
20944
  }
20577
20945
  if (imageUrls && price.referenceImageCount !== imageUrls.length) {
20578
- fail2(opts, "This server did not confirm support for all character reference images. No generation was started. Update the Genex API to a version supporting character --image, then run this command.");
20946
+ fail3(opts, "This server did not confirm support for all character reference images. No generation was started. Update the Genex API to a version supporting character --image, then run this command.");
20579
20947
  return;
20580
20948
  }
20581
20949
  if (!opts.json) {
@@ -20619,25 +20987,25 @@ async function runCharacterPreview(opts) {
20619
20987
  const log = createLogger({ quiet: opts.quiet || opts.json });
20620
20988
  const conceptId = opts.conceptId?.trim();
20621
20989
  if (!conceptId) {
20622
- fail2(opts, "Missing concept id. Usage: genex character preview <concept-id> --candidate <1|2|3> --user-approved.", "character_preview");
20990
+ fail3(opts, "Missing concept id. Usage: genex character preview <concept-id> --candidate <1|2|3> --user-approved.", "character_preview");
20623
20991
  return;
20624
20992
  }
20625
20993
  if (!Number.isInteger(opts.candidate) || ![1, 2, 3].includes(opts.candidate)) {
20626
- fail2(opts, "--candidate must be exactly 1, 2, or 3.", "character_preview");
20994
+ fail3(opts, "--candidate must be exactly 1, 2, or 3.", "character_preview");
20627
20995
  return;
20628
20996
  }
20629
20997
  if (opts.userApproved !== true) {
20630
- fail2(opts, "STOP: preview creation requires the user's explicit selection. Re-run with --user-approved only after they choose this candidate.", "character_preview");
20998
+ fail3(opts, "STOP: preview creation requires the user's explicit selection. Re-run with --user-approved only after they choose this candidate.", "character_preview");
20631
20999
  return;
20632
21000
  }
20633
21001
  const ctx = await context(opts);
20634
21002
  if (!ctx) {
20635
- fail2(opts, "Not authorized. Run `genex init` first to sign in.", "character_preview");
21003
+ fail3(opts, "Not authorized. Run `genex init` first to sign in.", "character_preview");
20636
21004
  return;
20637
21005
  }
20638
21006
  const badTexture = badCharacterTexture(opts);
20639
21007
  if (badTexture) {
20640
- fail2(opts, badTexture, "character_preview");
21008
+ fail3(opts, badTexture, "character_preview");
20641
21009
  return;
20642
21010
  }
20643
21011
  const body = { candidateIndex: opts.candidate, userApproved: true, ...meshyQualityOptions(opts) };
@@ -20673,31 +21041,31 @@ async function runCharacterFinalize(opts) {
20673
21041
  const log = createLogger({ quiet: opts.quiet || opts.json });
20674
21042
  const previewId = opts.previewId?.trim();
20675
21043
  if (!previewId) {
20676
- fail2(opts, "Missing preview id. Usage: genex character finalize <preview-id> --user-approved --approve-remesh 10000.");
21044
+ fail3(opts, "Missing preview id. Usage: genex character finalize <preview-id> --user-approved --approve-remesh 10000.");
20677
21045
  return;
20678
21046
  }
20679
21047
  if (opts.userApproved !== true) {
20680
- fail2(opts, "STOP: finalization requires explicit user approval of all four preview views. Re-run with --user-approved only after approval.");
21048
+ fail3(opts, "STOP: finalization requires explicit user approval of all four preview views. Re-run with --user-approved only after approval.");
20681
21049
  return;
20682
21050
  }
20683
21051
  const remeshTarget = opts.approveRemesh;
20684
21052
  if (remeshTarget === void 0 || !Number.isInteger(remeshTarget) || remeshTarget < REMESH_TARGET_MIN || remeshTarget > REMESH_TARGET_MAX) {
20685
- fail2(
21053
+ fail3(
20686
21054
  opts,
20687
21055
  `--approve-remesh <faces> is required: the face budget of the rigging copy, ${REMESH_TARGET_MIN}-${REMESH_TARGET_MAX} (10000 for a crowd body, 30000 or more for a hero). The number is the approval.`
20688
21056
  );
20689
21057
  return;
20690
21058
  }
20691
21059
  if (opts.polycount !== void 0) {
20692
- fail2(opts, "Do not combine --polycount with finalize; --approve-remesh <faces> is the rigging-copy target.");
21060
+ fail3(opts, "Do not combine --polycount with finalize; --approve-remesh <faces> is the rigging-copy target.");
20693
21061
  return;
20694
21062
  }
20695
21063
  if (opts.texture !== void 0 || opts.ultra === false) {
20696
- fail2(opts, "--texture / --no-ultra belong to `genex character preview` \u2014 the preview is the paid Meshy call; finalize remeshes and rigs what it made.");
21064
+ fail3(opts, "--texture / --no-ultra belong to `genex character preview` \u2014 the preview is the paid Meshy call; finalize remeshes and rigs what it made.");
20697
21065
  return;
20698
21066
  }
20699
21067
  if (opts.controllerPack === false) {
20700
- fail2(opts, "--no-controller-pack is only available with --direct-text or --image. Guided finalization requires neutral-v3.");
21068
+ fail3(opts, "--no-controller-pack is only available with --direct-text or --image. Guided finalization requires neutral-v3.");
20701
21069
  return;
20702
21070
  }
20703
21071
  const resolved = resolveActionSelectors(opts.actions ?? []);
@@ -20707,7 +21075,7 @@ async function runCharacterFinalize(opts) {
20707
21075
  }
20708
21076
  const ctx = await context(opts);
20709
21077
  if (!ctx) {
20710
- fail2(opts, "Not authorized. Run `genex init` first to sign in.");
21078
+ fail3(opts, "Not authorized. Run `genex init` first to sign in.");
20711
21079
  return;
20712
21080
  }
20713
21081
  const body = {
@@ -20843,22 +21211,22 @@ async function context2(opts) {
20843
21211
  if (!token) return null;
20844
21212
  return { token, apiUrl };
20845
21213
  }
20846
- async function readVideo(path44, log) {
21214
+ async function readVideo(path46, log) {
20847
21215
  let bytes;
20848
21216
  try {
20849
- bytes = await readFile(path44);
21217
+ bytes = await readFile(path46);
20850
21218
  } catch {
20851
- log.error(`Can't read ${path44}.`);
21219
+ log.error(`Can't read ${path46}.`);
20852
21220
  return null;
20853
21221
  }
20854
21222
  if (bytes.byteLength > MAX_VIDEO_BYTES) {
20855
- log.error(`${basename(path44)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
21223
+ log.error(`${basename(path46)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
20856
21224
  return null;
20857
21225
  }
20858
21226
  return bytes;
20859
21227
  }
20860
- async function uploadVideo(apiUrl, token, characterId, path44, bytes, log) {
20861
- const contentType = /\.mov$/i.test(path44) ? "video/quicktime" : "video/mp4";
21228
+ async function uploadVideo(apiUrl, token, characterId, path46, bytes, log) {
21229
+ const contentType = /\.mov$/i.test(path46) ? "video/quicktime" : "video/mp4";
20862
21230
  const minted = await apiFetch(
20863
21231
  `${apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/video-url`,
20864
21232
  {
@@ -20873,7 +21241,7 @@ async function uploadVideo(apiUrl, token, characterId, path44, bytes, log) {
20873
21241
  return null;
20874
21242
  }
20875
21243
  const { uploadUrl, videoUrl } = await minted.json();
20876
- log.dim(` uploading ${basename(path44)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
21244
+ log.dim(` uploading ${basename(path46)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
20877
21245
  const put = await fetch(uploadUrl, {
20878
21246
  method: "PUT",
20879
21247
  headers: { "Content-Type": contentType, "Content-Length": String(bytes.byteLength) },
@@ -21205,8 +21573,8 @@ function rank(items, query) {
21205
21573
  }
21206
21574
 
21207
21575
  // src/commands/motion.ts
21208
- import fs30 from "fs/promises";
21209
- import path31 from "path";
21576
+ import fs32 from "fs/promises";
21577
+ import path33 from "path";
21210
21578
 
21211
21579
  // src/lib/motion/npz.ts
21212
21580
  import zlib from "zlib";
@@ -22457,7 +22825,7 @@ async function motionGen(opts, log) {
22457
22825
  }
22458
22826
  if (opts.constraintsPath !== void 0) {
22459
22827
  try {
22460
- const raw = await fs30.readFile(opts.constraintsPath, "utf8");
22828
+ const raw = await fs32.readFile(opts.constraintsPath, "utf8");
22461
22829
  generationOptions.constraints = JSON.parse(raw);
22462
22830
  } catch {
22463
22831
  log.error(`Couldn't read the --constraints JSON at ${opts.constraintsPath}.`);
@@ -22481,10 +22849,10 @@ async function motionGen(opts, log) {
22481
22849
  async function expandTakes(selectors) {
22482
22850
  const out = [];
22483
22851
  for (const sel of selectors) {
22484
- const st = await fs30.stat(sel).catch(() => null);
22852
+ const st = await fs32.stat(sel).catch(() => null);
22485
22853
  if (st?.isDirectory()) {
22486
- const names2 = await fs30.readdir(sel);
22487
- for (const n of names2.sort()) if (n.endsWith(".npz")) out.push(path31.join(sel, n));
22854
+ const names2 = await fs32.readdir(sel);
22855
+ for (const n of names2.sort()) if (n.endsWith(".npz")) out.push(path33.join(sel, n));
22488
22856
  } else if (st?.isFile()) {
22489
22857
  out.push(sel);
22490
22858
  } else {
@@ -22519,7 +22887,7 @@ async function motionVerify(opts, log) {
22519
22887
  let gates = DEFAULT_GATES;
22520
22888
  if (opts.gatesPath) {
22521
22889
  try {
22522
- gates = mergeGates(DEFAULT_GATES, JSON.parse(await fs30.readFile(opts.gatesPath, "utf8")));
22890
+ gates = mergeGates(DEFAULT_GATES, JSON.parse(await fs32.readFile(opts.gatesPath, "utf8")));
22523
22891
  } catch {
22524
22892
  log.error(`Couldn't read the --gates JSON at ${opts.gatesPath}.`);
22525
22893
  process.exitCode = 1;
@@ -22541,9 +22909,9 @@ async function motionVerify(opts, log) {
22541
22909
  }
22542
22910
  const reports = [];
22543
22911
  for (const file of files) {
22544
- const stem = path31.basename(file).replace(/\.npz$/, "");
22912
+ const stem = path33.basename(file).replace(/\.npz$/, "");
22545
22913
  try {
22546
- reports.push(analyzeTake(stem, await fs30.readFile(file), gates));
22914
+ reports.push(analyzeTake(stem, await fs32.readFile(file), gates));
22547
22915
  } catch (err) {
22548
22916
  reports.push({
22549
22917
  take: stem,
@@ -22581,7 +22949,7 @@ async function motionCompile(opts, log) {
22581
22949
  let cfg = DEFAULT_MOTION_CONFIG;
22582
22950
  if (opts.configPath) {
22583
22951
  try {
22584
- const patch = JSON.parse(await fs30.readFile(opts.configPath, "utf8"));
22952
+ const patch = JSON.parse(await fs32.readFile(opts.configPath, "utf8"));
22585
22953
  cfg = { ...DEFAULT_MOTION_CONFIG, ...patch, idleLoop: { ...DEFAULT_MOTION_CONFIG.idleLoop, ...patch.idleLoop } };
22586
22954
  } catch {
22587
22955
  log.error(`Couldn't read the --config JSON at ${opts.configPath}.`);
@@ -22599,16 +22967,16 @@ async function motionCompile(opts, log) {
22599
22967
  }
22600
22968
  const inputs = [];
22601
22969
  for (const file of files) {
22602
- const stem = path31.basename(file).replace(/\.npz$/, "");
22970
+ const stem = path33.basename(file).replace(/\.npz$/, "");
22603
22971
  try {
22604
- inputs.push({ stem, take: loadTake(await fs30.readFile(file)) });
22972
+ inputs.push({ stem, take: loadTake(await fs32.readFile(file)) });
22605
22973
  } catch (err) {
22606
22974
  log.error(`${stem}: ${err instanceof Error ? err.message : String(err)}`);
22607
22975
  process.exitCode = 1;
22608
22976
  return;
22609
22977
  }
22610
22978
  }
22611
- const setName = opts.set ?? path31.basename(opts.out).replace(/\.json$/, "");
22979
+ const setName = opts.set ?? path33.basename(opts.out).replace(/\.json$/, "");
22612
22980
  let result;
22613
22981
  try {
22614
22982
  result = compileSet(inputs, setName, cfg);
@@ -22623,9 +22991,9 @@ async function motionCompile(opts, log) {
22623
22991
  process.exitCode = 1;
22624
22992
  return;
22625
22993
  }
22626
- await fs30.mkdir(path31.dirname(path31.resolve(opts.out)), { recursive: true });
22994
+ await fs32.mkdir(path33.dirname(path33.resolve(opts.out)), { recursive: true });
22627
22995
  const json = JSON.stringify(result.data);
22628
- await fs30.writeFile(opts.out, json);
22996
+ await fs32.writeFile(opts.out, json);
22629
22997
  if (opts.json) {
22630
22998
  writeJson({ out: opts.out, set: setName, tracks: Object.keys(result.data.gaits), bytes: json.length });
22631
22999
  return;
@@ -22643,9 +23011,9 @@ var MOTION_RUNTIME_FILES = [
22643
23011
  var MOTION_PRESETS = {
22644
23012
  rifle: ["sets/rifle.json", "sets/jumps.json"]
22645
23013
  };
22646
- var MOTION_DEST = path31.join("src", "motion");
23014
+ var MOTION_DEST = path33.join("src", "motion");
22647
23015
  async function motionInstall(opts, log) {
22648
- const srcDir = path31.join(getTemplatesDir(), "motion");
23016
+ const srcDir = path33.join(getTemplatesDir(), "motion");
22649
23017
  const root = opts.cwd ?? process.cwd();
22650
23018
  const preset = opts.set;
22651
23019
  if (preset !== void 0 && !MOTION_PRESETS[preset]) {
@@ -22656,21 +23024,21 @@ async function motionInstall(opts, log) {
22656
23024
  const files = [...MOTION_RUNTIME_FILES, ...preset ? MOTION_PRESETS[preset] : []];
22657
23025
  log.plain(c.bold(`genex motion install${preset ? ` --set ${preset}` : ""}`));
22658
23026
  log.plain("");
22659
- log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path31.sep)}`);
23027
+ log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path33.sep)}`);
22660
23028
  let copied = 0, skipped = 0;
22661
23029
  try {
22662
23030
  for (const rel of files) {
22663
- const dest = path31.join(root, MOTION_DEST, rel);
22664
- const exists5 = await fs30.access(dest).then(() => true, () => false);
23031
+ const dest = path33.join(root, MOTION_DEST, rel);
23032
+ const exists5 = await fs32.access(dest).then(() => true, () => false);
22665
23033
  if (!opts.force && exists5) {
22666
23034
  skipped++;
22667
- log.dim(` skipped ${path31.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
23035
+ log.dim(` skipped ${path33.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
22668
23036
  continue;
22669
23037
  }
22670
- await fs30.mkdir(path31.dirname(dest), { recursive: true });
22671
- await fs30.copyFile(path31.join(srcDir, rel), dest);
23038
+ await fs32.mkdir(path33.dirname(dest), { recursive: true });
23039
+ await fs32.copyFile(path33.join(srcDir, rel), dest);
22672
23040
  copied++;
22673
- log.dim(` ${path31.join(MOTION_DEST, rel)}`);
23041
+ log.dim(` ${path33.join(MOTION_DEST, rel)}`);
22674
23042
  }
22675
23043
  } catch (err) {
22676
23044
  log.error(`Copy failed: ${String(err)}`);
@@ -22711,7 +23079,7 @@ async function motionConstraints(opts, log) {
22711
23079
  }
22712
23080
  const doc = directionConstraint(dir, speed, duration);
22713
23081
  const out = opts.out ?? "constraints.json";
22714
- await fs30.writeFile(out, JSON.stringify(doc));
23082
+ await fs32.writeFile(out, JSON.stringify(doc));
22715
23083
  if (opts.json) {
22716
23084
  writeJson({ out, dir: dirName, speed, duration, waypoints: doc[0].frame_indices.length });
22717
23085
  return;
@@ -22748,14 +23116,14 @@ async function runMotion(opts) {
22748
23116
  }
22749
23117
 
22750
23118
  // src/commands/blender.ts
22751
- import fs31 from "fs/promises";
22752
- import path32 from "path";
23119
+ import fs33 from "fs/promises";
23120
+ import path34 from "path";
22753
23121
  var SUBS2 = ["demo", "exec", "snap", "scene", "import", "export", "reset", "mcp", "serve", "seat", "release"];
22754
23122
  var DEFAULT_OUT_DIR = "assets/blender";
22755
23123
  async function writeB64(dir, name, b64) {
22756
- await fs31.mkdir(dir, { recursive: true });
22757
- const p = path32.join(dir, name);
22758
- await fs31.writeFile(p, Buffer.from(b64, "base64"));
23124
+ await fs33.mkdir(dir, { recursive: true });
23125
+ const p = path34.join(dir, name);
23126
+ await fs33.writeFile(p, Buffer.from(b64, "base64"));
22759
23127
  return p;
22760
23128
  }
22761
23129
  function reportScene(log, s) {
@@ -22781,7 +23149,7 @@ async function runBlender(opts) {
22781
23149
  return serveLocalBlender({ port, log });
22782
23150
  }
22783
23151
  if (sub === "mcp") {
22784
- const { runBlenderMcp } = await import("./blender-mcp-ATHMMRDU.js");
23152
+ const { runBlenderMcp } = await import("./blender-mcp-ZRCMYTXI.js");
22785
23153
  return runBlenderMcp();
22786
23154
  }
22787
23155
  if (sub === "seat") {
@@ -22839,7 +23207,7 @@ async function runBlender(opts) {
22839
23207
  log.plain(rest.join("\n"));
22840
23208
  return 1;
22841
23209
  }
22842
- const outDir = path32.resolve(opts.outDir ?? DEFAULT_OUT_DIR);
23210
+ const outDir = path34.resolve(opts.outDir ?? DEFAULT_OUT_DIR);
22843
23211
  const mode = opts.mode;
22844
23212
  if (mode !== void 0 && !isRenderMode(mode)) {
22845
23213
  log.error(`Unknown --mode ${mode}. Use one of: ${RENDER_MODES.join(", ")}.`);
@@ -22879,14 +23247,14 @@ async function runBlender(opts) {
22879
23247
  return 0;
22880
23248
  }
22881
23249
  case "export": {
22882
- const target = opts.out ?? path32.join(outDir, "scene.glb");
23250
+ const target = opts.out ?? path34.join(outDir, "scene.glb");
22883
23251
  const r = await blenderCall(base, "/export", { path: "/tmp/genex-export.glb" });
22884
23252
  if (!r.glbBase64) {
22885
23253
  log.error(`/export answered with no GLB bytes${r.uploaded ? " (it was uploaded, not inlined)" : ""}`);
22886
23254
  return 1;
22887
23255
  }
22888
- await fs31.mkdir(path32.dirname(target), { recursive: true });
22889
- await fs31.writeFile(target, Buffer.from(r.glbBase64, "base64"));
23256
+ await fs33.mkdir(path34.dirname(target), { recursive: true });
23257
+ await fs33.writeFile(target, Buffer.from(r.glbBase64, "base64"));
22890
23258
  log.success(`Exported ${r.bytes ?? 0} bytes`);
22891
23259
  log.plain(` ${c.cyan(target)}`);
22892
23260
  return 0;
@@ -22919,12 +23287,12 @@ async function runBlender(opts) {
22919
23287
  return 1;
22920
23288
  }
22921
23289
  try {
22922
- script = await fs31.readFile(opts.input, "utf8");
23290
+ script = await fs33.readFile(opts.input, "utf8");
22923
23291
  } catch {
22924
23292
  log.error(`Can't read ${opts.input}`);
22925
23293
  return 1;
22926
23294
  }
22927
- label = path32.basename(opts.input);
23295
+ label = path34.basename(opts.input);
22928
23296
  }
22929
23297
  const r = await blenderCall(base, "/exec", { script, ...mode ? { mode } : {}, sheet: { formats: SHEET_FORMATS } });
22930
23298
  if (r.stdout?.trim()) log.plain(r.stdout.trimEnd());
@@ -23043,9 +23411,9 @@ print(f"castle: {n} objects")
23043
23411
  `;
23044
23412
 
23045
23413
  // src/commands/asset-new.ts
23046
- import fs32 from "fs";
23414
+ import fs34 from "fs";
23047
23415
  import fsp from "fs/promises";
23048
- import path33 from "path";
23416
+ import path35 from "path";
23049
23417
  import { pathToFileURL } from "url";
23050
23418
  var EXTRA_FILES = [
23051
23419
  "genex-asset.example.json",
@@ -23139,7 +23507,7 @@ Every milestone ends with \`npm run verify\`, then \`npx genex preview --no-buil
23139
23507
  }
23140
23508
  async function runAssetNew(options) {
23141
23509
  const log = createLogger();
23142
- const cwd = options.dir ? path33.resolve(options.dir) : process.cwd();
23510
+ const cwd = options.dir ? path35.resolve(options.dir) : process.cwd();
23143
23511
  const slug = options.assetSlug;
23144
23512
  if (!slug) {
23145
23513
  log.error('Usage: genex asset new <slug> [--title "<Name>"] [--dims WxDxH] [--tri-band LOW-HIGH] [--holder "<your name>"] [--out <dir>]');
@@ -23149,14 +23517,14 @@ async function runAssetNew(options) {
23149
23517
  log.error(`The slug must be kebab-case (letters, digits, dashes), got: ${slug}`);
23150
23518
  return 1;
23151
23519
  }
23152
- const templateDir = path33.join(getTemplatesDir(), "asset-viewer");
23153
- if (!fs32.existsSync(templateDir)) {
23520
+ const templateDir = path35.join(getTemplatesDir(), "asset-viewer");
23521
+ if (!fs34.existsSync(templateDir)) {
23154
23522
  log.error(`Vendored asset-viewer template not found at ${templateDir} \u2014 reinstall the CLI.`);
23155
23523
  return 1;
23156
23524
  }
23157
- const manifestTools = await import(pathToFileURL(path33.join(templateDir, "tools", "emit-manifest.mjs")).href);
23525
+ const manifestTools = await import(pathToFileURL(path35.join(templateDir, "tools", "emit-manifest.mjs")).href);
23158
23526
  const { SHARED_FILES, PARITY_FILENAME, hashSharedFiles, pascalCase } = manifestTools;
23159
- const lockPath = path33.join(templateDir, "shared-files.sha256.json");
23527
+ const lockPath = path35.join(templateDir, "shared-files.sha256.json");
23160
23528
  const lock = JSON.parse(await fsp.readFile(lockPath, "utf8"));
23161
23529
  const actual = hashSharedFiles(templateDir);
23162
23530
  const drifted = SHARED_FILES.filter((rel) => lock.files[rel] !== actual[rel]);
@@ -23169,8 +23537,8 @@ async function runAssetNew(options) {
23169
23537
  const triBand = parseBand(options.triBand ?? "500-8000");
23170
23538
  const holder = options.holder?.trim() || HOLDER_PLACEHOLDER;
23171
23539
  const year = (/* @__PURE__ */ new Date()).getUTCFullYear();
23172
- const outDir = path33.resolve(cwd, options.out ?? slug);
23173
- if (fs32.existsSync(outDir) && fs32.readdirSync(outDir).length > 0 && !options.force) {
23540
+ const outDir = path35.resolve(cwd, options.out ?? slug);
23541
+ if (fs34.existsSync(outDir) && fs34.readdirSync(outDir).length > 0 && !options.force) {
23174
23542
  log.error(`${outDir} already holds files. Nothing was touched \u2014 pass --force to write into it anyway.`);
23175
23543
  return 1;
23176
23544
  }
@@ -23198,24 +23566,24 @@ async function runAssetNew(options) {
23198
23566
  };
23199
23567
  await fsp.mkdir(outDir, { recursive: true });
23200
23568
  for (const rel of [...SHARED_FILES, ...EXTRA_FILES]) {
23201
- const to = path33.join(outDir, rel);
23202
- await fsp.mkdir(path33.dirname(to), { recursive: true });
23203
- await fsp.copyFile(path33.join(templateDir, rel), to);
23569
+ const to = path35.join(outDir, rel);
23570
+ await fsp.mkdir(path35.dirname(to), { recursive: true });
23571
+ await fsp.copyFile(path35.join(templateDir, rel), to);
23204
23572
  }
23205
- const pkg = fillTemplate(await fsp.readFile(path33.join(templateDir, "package.json"), "utf8"), {
23573
+ const pkg = fillTemplate(await fsp.readFile(path35.join(templateDir, "package.json"), "utf8"), {
23206
23574
  slug,
23207
23575
  name,
23208
23576
  version
23209
23577
  });
23210
- await fsp.writeFile(path33.join(outDir, "package.json"), pkg, "utf8");
23211
- await fsp.writeFile(path33.join(outDir, "asset.config.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
23212
- await fsp.writeFile(path33.join(outDir, ".gitignore"), GITIGNORE, "utf8");
23578
+ await fsp.writeFile(path35.join(outDir, "package.json"), pkg, "utf8");
23579
+ await fsp.writeFile(path35.join(outDir, "asset.config.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
23580
+ await fsp.writeFile(path35.join(outDir, ".gitignore"), GITIGNORE, "utf8");
23213
23581
  await fsp.writeFile(
23214
- path33.join(outDir, "DESIGN.md"),
23582
+ path35.join(outDir, "DESIGN.md"),
23215
23583
  designDoc({ name, slug, sizeMeters, triBand, holder }),
23216
23584
  "utf8"
23217
23585
  );
23218
- const placeholder = await fsp.readFile(path33.join(templateDir, "src", "asset", "PLACEHOLDER.ts"), "utf8");
23586
+ const placeholder = await fsp.readFile(path35.join(templateDir, "src", "asset", "PLACEHOLDER.ts"), "utf8");
23219
23587
  const seeded = seedAssetSource(placeholder, {
23220
23588
  slug,
23221
23589
  name,
@@ -23226,8 +23594,8 @@ async function runAssetNew(options) {
23226
23594
  pascalCase
23227
23595
  });
23228
23596
  const assetFilename = `${slug.replace(/-([a-z0-9])/g, (_, ch) => ch.toUpperCase())}.ts`;
23229
- await fsp.mkdir(path33.join(outDir, "src", "asset"), { recursive: true });
23230
- await fsp.writeFile(path33.join(outDir, "src", "asset", assetFilename), seeded, "utf8");
23597
+ await fsp.mkdir(path35.join(outDir, "src", "asset"), { recursive: true });
23598
+ await fsp.writeFile(path35.join(outDir, "src", "asset", assetFilename), seeded, "utf8");
23231
23599
  const copied = hashSharedFiles(outDir);
23232
23600
  const mismatched = SHARED_FILES.filter((rel) => copied[rel] !== lock.files[rel]);
23233
23601
  if (mismatched.length) {
@@ -23235,7 +23603,7 @@ async function runAssetNew(options) {
23235
23603
  return 1;
23236
23604
  }
23237
23605
  await fsp.writeFile(
23238
- path33.join(outDir, PARITY_FILENAME),
23606
+ path35.join(outDir, PARITY_FILENAME),
23239
23607
  JSON.stringify(
23240
23608
  {
23241
23609
  note: "Recorded by `genex asset new`. Gate G13 recomputes these; do not hand-edit the viewer.",
@@ -23279,14 +23647,14 @@ async function runAssetNew(options) {
23279
23647
  }
23280
23648
 
23281
23649
  // src/commands/tools.ts
23282
- import path36 from "path";
23650
+ import path38 from "path";
23283
23651
 
23284
23652
  // src/commands/doctor.ts
23285
- import path35 from "path";
23653
+ import path37 from "path";
23286
23654
 
23287
23655
  // src/commands/llm.ts
23288
- import fs33 from "fs/promises";
23289
- import path34 from "path";
23656
+ import fs35 from "fs/promises";
23657
+ import path36 from "path";
23290
23658
  import { randomUUID as randomUUID2 } from "crypto";
23291
23659
  var SUBS3 = ["models", "bench", "price", "status", "cancel"];
23292
23660
  var LLM_BENCH_APPROVAL_REQUIRED = "STOP: `genex llm bench` runs the real model and spends YOUR OWN credits \u2014 your balance, not a player's, and a spend this build's asset allowance does not count. Re-run with --max-credits <n> --user-approved only after the person at the keyboard agreed to the number.";
@@ -23429,13 +23797,13 @@ async function runLlm(opts = {}) {
23429
23797
  const log = createLogger({ quiet: opts.quiet || opts.json });
23430
23798
  const cwd = opts.cwd ?? process.cwd();
23431
23799
  const sub = opts.name?.trim() || "models";
23432
- const fail4 = (command, message, extra = {}) => {
23800
+ const fail5 = (command, message, extra = {}) => {
23433
23801
  if (opts.json) writeJsonLine({ command, status: "failed", error: message, ...extra });
23434
23802
  else log.error(message);
23435
23803
  process.exitCode = 1;
23436
23804
  };
23437
23805
  if (!SUBS3.includes(sub)) {
23438
- fail4(`llm ${sub}`, `Unknown subcommand ${sub}. Use: ${SUBS3.join(", ")}.`);
23806
+ fail5(`llm ${sub}`, `Unknown subcommand ${sub}. Use: ${SUBS3.join(", ")}.`);
23439
23807
  return;
23440
23808
  }
23441
23809
  if (sub === "price") {
@@ -23446,31 +23814,31 @@ async function runLlm(opts = {}) {
23446
23814
  const toolsOnly = ws.mode === "tools" && !ws.hosted;
23447
23815
  const projectBound = sub === "bench" || sub === "status" || sub === "cancel";
23448
23816
  if (sub === "cancel" && !opts.generationId?.trim()) {
23449
- fail4("llm cancel", "`genex llm cancel` needs the id of the call to stop, e.g. genex llm cancel <id> \u2014 `genex llm status` lists the open ones.");
23817
+ fail5("llm cancel", "`genex llm cancel` needs the id of the call to stop, e.g. genex llm cancel <id> \u2014 `genex llm status` lists the open ones.");
23450
23818
  return;
23451
23819
  }
23452
23820
  if (sub === "bench") {
23453
23821
  if (opts.maxCoins !== void 0) process.stderr.write(`${c.yellow("!")} ${LLM_MAX_COINS_DEPRECATED}
23454
23822
  `);
23455
23823
  if (opts.maxCoins !== void 0 && opts.maxCredits !== void 0 && opts.maxCoins !== opts.maxCredits) {
23456
- fail4("llm bench", `Pass --max-credits alone \u2014 --max-coins is its deprecated name, and the two disagree (${opts.maxCredits} vs ${opts.maxCoins}).`);
23824
+ fail5("llm bench", `Pass --max-credits alone \u2014 --max-coins is its deprecated name, and the two disagree (${opts.maxCredits} vs ${opts.maxCoins}).`);
23457
23825
  return;
23458
23826
  }
23459
23827
  const approvedCredits = opts.maxCredits ?? opts.maxCoins;
23460
23828
  if (approvedCredits === void 0 || opts.userApproved !== true) {
23461
- fail4("llm bench", LLM_BENCH_APPROVAL_REQUIRED);
23829
+ fail5("llm bench", LLM_BENCH_APPROVAL_REQUIRED);
23462
23830
  return;
23463
23831
  }
23464
23832
  if (!Number.isInteger(approvedCredits) || approvedCredits < 1) {
23465
- fail4("llm bench", `--max-credits takes a whole number of credits, 1 or more (got ${String(approvedCredits)}).`);
23833
+ fail5("llm bench", `--max-credits takes a whole number of credits, 1 or more (got ${String(approvedCredits)}).`);
23466
23834
  return;
23467
23835
  }
23468
23836
  if (!opts.benchPrompt?.trim()) {
23469
- fail4("llm bench", '`genex llm bench` needs the prompt your game would send, e.g. genex llm bench "<prompt>" --max-credits <n> --user-approved.');
23837
+ fail5("llm bench", '`genex llm bench` needs the prompt your game would send, e.g. genex llm bench "<prompt>" --max-credits <n> --user-approved.');
23470
23838
  return;
23471
23839
  }
23472
23840
  if (opts.jsonOutput && opts.textOutput) {
23473
- fail4("llm bench", "Pass --json-output or --text, not both.");
23841
+ fail5("llm bench", "Pass --json-output or --text, not both.");
23474
23842
  return;
23475
23843
  }
23476
23844
  }
@@ -23486,7 +23854,7 @@ async function runLlm(opts = {}) {
23486
23854
  }
23487
23855
  const meta = await readProject(cwd);
23488
23856
  if (projectBound && !meta?.id) {
23489
- fail4(
23857
+ fail5(
23490
23858
  `llm ${sub}`,
23491
23859
  sub === "bench" ? "This folder isn't linked to a game \u2014 a benchmark is billed to a project you own." : `This folder isn't linked to a game \u2014 \`genex llm ${sub}\` works on the development calls of a project you own.`
23492
23860
  );
@@ -23499,7 +23867,7 @@ async function runLlm(opts = {}) {
23499
23867
  let token = opts.token ?? await readUserToken(opts.envPath, apiUrl);
23500
23868
  if (!token) {
23501
23869
  if (opts.noAuth) {
23502
- fail4(`llm ${sub}`, "Not signed in. Re-run without --no-auth to connect.");
23870
+ fail5(`llm ${sub}`, "Not signed in. Re-run without --no-auth to connect.");
23503
23871
  return;
23504
23872
  }
23505
23873
  log.plain("Not signed in \u2014 connecting\u2026");
@@ -23516,7 +23884,7 @@ async function runLlm(opts = {}) {
23516
23884
  log.dim(`Then re-run ${c.cyan(`genex llm ${sub}`)}.`);
23517
23885
  return;
23518
23886
  }
23519
- fail4(`llm ${sub}`, `Sign-in didn't complete: ${err instanceof Error ? err.message : String(err)}`);
23887
+ fail5(`llm ${sub}`, `Sign-in didn't complete: ${err instanceof Error ? err.message : String(err)}`);
23520
23888
  return;
23521
23889
  }
23522
23890
  await writeUserToken(token, opts.envPath, apiUrl);
@@ -23807,7 +24175,7 @@ async function runBench(args) {
23807
24175
  let schema;
23808
24176
  if (opts.schemaPath) {
23809
24177
  try {
23810
- schema = JSON.parse(await fs33.readFile(path34.resolve(cwd, opts.schemaPath), "utf8"));
24178
+ schema = JSON.parse(await fs35.readFile(path36.resolve(cwd, opts.schemaPath), "utf8"));
23811
24179
  } catch (err) {
23812
24180
  benchFailed(opts, log, `Couldn't read --schema ${opts.schemaPath}: ${err instanceof Error ? err.message : String(err)}`);
23813
24181
  return;
@@ -23873,7 +24241,7 @@ async function runBench(args) {
23873
24241
  }
23874
24242
  const base = `${apiUrl}/api/runtime/development/projects/${encodeURIComponent(projectId)}/generations`;
23875
24243
  const results = [];
23876
- let refusal = null;
24244
+ let refusal2 = null;
23877
24245
  let providerRefused = 0;
23878
24246
  let inflight = null;
23879
24247
  const onSigint = () => {
@@ -23906,7 +24274,7 @@ async function runBench(args) {
23906
24274
  });
23907
24275
  if (!res.ok) {
23908
24276
  const verdict = await explainBenchRefusal(res, call, base, log, opts, i, results.length);
23909
- if (verdict.refusal) refusal = verdict.refusal;
24277
+ if (verdict.refusal) refusal2 = verdict.refusal;
23910
24278
  if (verdict.stop) break;
23911
24279
  continue;
23912
24280
  }
@@ -24003,7 +24371,7 @@ async function runBench(args) {
24003
24371
  results,
24004
24372
  // The create-time refusal that ended the run, or null. Beside the
24005
24373
  // samples, never among them: no attempt existed and nothing was spent.
24006
- refusal,
24374
+ refusal: refusal2,
24007
24375
  // Over the settled samples: the real provider cost (pico-dollar strings
24008
24376
  // and USD) and the whole credits charged.
24009
24377
  costUsdPicos: record.cost,
@@ -24125,40 +24493,40 @@ async function explainBenchRefusal(res, call, base, log, opts, index, settledSoF
24125
24493
  return { stop: true, refusal: { status: res.status, error: typeof printed2.error === "string" ? printed2.error : null, slotsHeld: null, slotLimit: null } };
24126
24494
  }
24127
24495
  const body = await res.json().catch(() => ({}));
24128
- const refusal = { status: res.status, error: body.error ?? null, slotsHeld: null, slotLimit: null };
24496
+ const refusal2 = { status: res.status, error: body.error ?? null, slotsHeld: null, slotLimit: null };
24129
24497
  if (res.status === 429 && body.error === "generation_limit") {
24130
24498
  const counts = serverSlotCounts(body) ?? await readSlotCounts(call, base);
24131
- refusal.slotsHeld = counts?.held ?? null;
24132
- refusal.slotLimit = counts?.limit ?? null;
24499
+ refusal2.slotsHeld = counts?.held ?? null;
24500
+ refusal2.slotLimit = counts?.limit ?? null;
24133
24501
  if (!opts.json) log.error(` Sample ${index + 1} ${benchSlotsHeldSentence(counts)}`);
24134
- return { stop: true, refusal };
24502
+ return { stop: true, refusal: refusal2 };
24135
24503
  }
24136
24504
  if (res.status === 403 && body.error === "credits_unverified") {
24137
24505
  if (!opts.json) log.error(` ${LLM_BENCH_UNVERIFIED_LINE}`);
24138
- return { stop: true, refusal };
24506
+ return { stop: true, refusal: refusal2 };
24139
24507
  }
24140
- if (opts.json) return { stop: res.status !== 429, refusal };
24508
+ if (opts.json) return { stop: res.status !== 429, refusal: refusal2 };
24141
24509
  if (res.status === 404 && body.error === "not_found") {
24142
24510
  log.error(` ${LLM_LANE_OFF_LINE}`);
24143
- return { stop: true, refusal };
24511
+ return { stop: true, refusal: refusal2 };
24144
24512
  }
24145
24513
  if (res.status === 403 && body.error === "credential_scope") {
24146
24514
  log.error(" This credential can't reach the runtime lane \u2014 an API key or MCP token is scoped out.");
24147
24515
  log.dim(` Sign in with ${c.cyan("genex auth")} and re-run.`);
24148
- return { stop: true, refusal };
24516
+ return { stop: true, refusal: refusal2 };
24149
24517
  }
24150
24518
  if (res.status === 403 && body.error === "project_owner_required") {
24151
24519
  log.error(" A benchmark is billed to a project YOU own, and this one isn't yours.");
24152
- return { stop: true, refusal };
24520
+ return { stop: true, refusal: refusal2 };
24153
24521
  }
24154
24522
  if (res.status === 402) {
24155
24523
  log.error(" Not enough credits to start the attempt \u2014 a bench is paid from your own balance.");
24156
- return { stop: true, refusal };
24524
+ return { stop: true, refusal: refusal2 };
24157
24525
  }
24158
24526
  log.error(
24159
24527
  ` Sample ${index + 1} was refused: ${body.message ?? body.error ?? `HTTP ${res.status}`}${settledSoFar > 0 ? " (earlier samples still count)" : ""}`
24160
24528
  );
24161
- return { stop: res.status >= 500 || res.status === 401 || res.status === 403, refusal };
24529
+ return { stop: res.status >= 500 || res.status === 401 || res.status === 403, refusal: refusal2 };
24162
24530
  }
24163
24531
  function printProviderRefusal(log, status2, message, awaitingBill = false) {
24164
24532
  if (awaitingBill) log.dim(` The provider answered ${status2} after routing \u2014 its bill is not final, so what this call reserved stays held until it resolves (see \`genex llm status\`); it is not a sample.`);
@@ -24189,9 +24557,9 @@ async function pollSettled(call, url, timeoutSec) {
24189
24557
  }
24190
24558
  async function saveBench(cwd, record) {
24191
24559
  try {
24192
- const file = path34.join(cwd, LLM_BENCH_FILE);
24193
- await fs33.mkdir(path34.dirname(file), { recursive: true });
24194
- await fs33.writeFile(file, JSON.stringify(record, null, 2) + "\n");
24560
+ const file = path36.join(cwd, LLM_BENCH_FILE);
24561
+ await fs35.mkdir(path36.dirname(file), { recursive: true });
24562
+ await fs35.writeFile(file, JSON.stringify(record, null, 2) + "\n");
24195
24563
  return LLM_BENCH_FILE;
24196
24564
  } catch {
24197
24565
  return null;
@@ -24404,7 +24772,7 @@ async function cancelCall(args) {
24404
24772
  async function readSavedBench(cwd) {
24405
24773
  let parsed;
24406
24774
  try {
24407
- parsed = JSON.parse(await fs33.readFile(path34.join(cwd, LLM_BENCH_FILE), "utf8"));
24775
+ parsed = JSON.parse(await fs35.readFile(path36.join(cwd, LLM_BENCH_FILE), "utf8"));
24408
24776
  } catch {
24409
24777
  return null;
24410
24778
  }
@@ -24836,7 +25204,7 @@ async function fetchLegalStatus(apiUrl, token) {
24836
25204
  }
24837
25205
  async function firstSkillsMarker() {
24838
25206
  for (const target of resolveAgentTargets()) {
24839
- const marker = await readSkillsMarker(path35.join(target.baseDir, "skills"));
25207
+ const marker = await readSkillsMarker(path37.join(target.baseDir, "skills"));
24840
25208
  if (marker) return marker;
24841
25209
  }
24842
25210
  return null;
@@ -24887,8 +25255,8 @@ async function runTools(opts) {
24887
25255
  let totalNew = 0;
24888
25256
  let totalUpdated = 0;
24889
25257
  for (const t of targets) {
24890
- const dest = path36.join(t.baseDir, "skills");
24891
- const { copied, updated } = await copyTemplates(path36.join(templatesDir, "skills"), dest, {
25258
+ const dest = path38.join(t.baseDir, "skills");
25259
+ const { copied, updated } = await copyTemplates(path38.join(templatesDir, "skills"), dest, {
24892
25260
  filter: (rel) => skillFamilyFilter("tools")(`skills/${rel}`)
24893
25261
  });
24894
25262
  await pruneRemovedSkills(dest, log);
@@ -24948,8 +25316,8 @@ async function runTools(opts) {
24948
25316
  }
24949
25317
 
24950
25318
  // src/commands/budget.ts
24951
- import fs34 from "fs/promises";
24952
- import path37 from "path";
25319
+ import fs36 from "fs/promises";
25320
+ import path39 from "path";
24953
25321
  var BUDGET_SOURCE_LINE = "Live from your account; prices can change without a deploy.";
24954
25322
  var BUDGET_APPROVAL_REQUIRED = "STOP: raising the asset allowance requires the player's explicit approval. Re-run with --user-approved only after they agreed to the number.";
24955
25323
  function internalKind(kind) {
@@ -24957,7 +25325,7 @@ function internalKind(kind) {
24957
25325
  }
24958
25326
  async function isGenexProject(cwd) {
24959
25327
  try {
24960
- await fs34.access(path37.join(cwd, ".genex"));
25328
+ await fs36.access(path39.join(cwd, ".genex"));
24961
25329
  return true;
24962
25330
  } catch {
24963
25331
  return false;
@@ -24978,25 +25346,25 @@ async function runBudget(opts = {}) {
24978
25346
  const log = createLogger({ quiet: opts.quiet || opts.json });
24979
25347
  const cwd = opts.cwd ?? process.cwd();
24980
25348
  const config = budgetConfig();
24981
- const fail4 = (message) => {
25349
+ const fail5 = (message) => {
24982
25350
  if (opts.json) writeJson({ command: "budget", status: "failed", error: message });
24983
25351
  else log.error(message);
24984
25352
  process.exitCode = 1;
24985
25353
  };
24986
25354
  const setting = opts.assets !== void 0;
24987
25355
  if (setting && opts.userApproved !== true) {
24988
- fail4(BUDGET_APPROVAL_REQUIRED);
25356
+ fail5(BUDGET_APPROVAL_REQUIRED);
24989
25357
  return;
24990
25358
  }
24991
25359
  if (setting && (!Number.isInteger(opts.assets) || opts.assets < 0)) {
24992
- fail4(`--assets takes a whole number of credits, 0 or more (got ${String(opts.assets)}).`);
25360
+ fail5(`--assets takes a whole number of credits, 0 or more (got ${String(opts.assets)}).`);
24993
25361
  return;
24994
25362
  }
24995
25363
  const meta = await readProject(cwd);
24996
25364
  const apiUrl = getApiUrl(opts.apiUrl ?? meta?.apiUrl);
24997
25365
  const token = opts.token ?? await readUserToken(opts.envPath, apiUrl);
24998
25366
  if (!token) {
24999
- fail4("Not authorized. Run `genex init` first to sign in.");
25367
+ fail5("Not authorized. Run `genex init` first to sign in.");
25000
25368
  return;
25001
25369
  }
25002
25370
  const snapshot = await fetchCreditsSnapshot(apiUrl, token);
@@ -25014,7 +25382,7 @@ async function runBudget(opts = {}) {
25014
25382
  };
25015
25383
  persisted = await writeBudgetStore(cwd, store);
25016
25384
  if (!persisted) {
25017
- fail4("This folder is not a Genex project (no .genex/), so the allowance has nowhere to live. Run this inside the game folder.");
25385
+ fail5("This folder is not a Genex project (no .genex/), so the allowance has nowhere to live. Run this inside the game folder.");
25018
25386
  return;
25019
25387
  }
25020
25388
  } else if (snapshot && !snapshot.unlimited && !budgetDisabled(config)) {
@@ -25106,39 +25474,39 @@ async function runBudget(opts = {}) {
25106
25474
  }
25107
25475
 
25108
25476
  // src/commands/player.ts
25109
- import fs39 from "fs";
25477
+ import fs41 from "fs";
25110
25478
  import fsp2 from "fs/promises";
25111
25479
  import os12 from "os";
25112
- import path43 from "path";
25480
+ import path45 from "path";
25113
25481
  import { spawn as spawn8 } from "child_process";
25114
25482
 
25115
25483
  // src/lib/player/paths.ts
25116
25484
  import os8 from "os";
25117
- import path38 from "path";
25485
+ import path40 from "path";
25118
25486
  var PLAYER_DRY_RUN_ENV = "GENEX_PLAYER_DRY_RUN";
25119
25487
  function playerDryRun() {
25120
25488
  const raw = process.env[PLAYER_DRY_RUN_ENV];
25121
25489
  return raw === "1" || raw === "true";
25122
25490
  }
25123
25491
  function playerPaths(home = os8.homedir()) {
25124
- const dir = home === os8.homedir() ? getGenexDir() : path38.join(home, ".genex");
25125
- const logDir = path38.join(dir, "logs");
25492
+ const dir = home === os8.homedir() ? getGenexDir() : path40.join(home, ".genex");
25493
+ const logDir = path40.join(dir, "logs");
25126
25494
  return {
25127
- credential: path38.join(dir, "player.json"),
25128
- state: path38.join(dir, "player-state.json"),
25129
- relaunch: path38.join(dir, "player-relaunch.json"),
25495
+ credential: path40.join(dir, "player.json"),
25496
+ state: path40.join(dir, "player-state.json"),
25497
+ relaunch: path40.join(dir, "player-relaunch.json"),
25130
25498
  logDir,
25131
- log: path38.join(logDir, "player.log"),
25132
- errorLog: path38.join(logDir, "player.error.log")
25499
+ log: path40.join(logDir, "player.log"),
25500
+ errorLog: path40.join(logDir, "player.error.log")
25133
25501
  };
25134
25502
  }
25135
25503
 
25136
25504
  // src/lib/player/store.ts
25137
- import fs35 from "fs/promises";
25138
- import path39 from "path";
25505
+ import fs37 from "fs/promises";
25506
+ import path41 from "path";
25139
25507
  async function readPlayerInstall(home) {
25140
25508
  try {
25141
- const raw = await fs35.readFile(playerPaths(home).credential, "utf8");
25509
+ const raw = await fs37.readFile(playerPaths(home).credential, "utf8");
25142
25510
  const parsed = JSON.parse(raw);
25143
25511
  if (!parsed || typeof parsed.token !== "string" || typeof parsed.apiUrl !== "string") return null;
25144
25512
  return parsed;
@@ -25168,7 +25536,7 @@ function describeInstall(install2) {
25168
25536
  }
25169
25537
  async function readPlayerState(home) {
25170
25538
  try {
25171
- return JSON.parse(await fs35.readFile(playerPaths(home).state, "utf8"));
25539
+ return JSON.parse(await fs37.readFile(playerPaths(home).state, "utf8"));
25172
25540
  } catch {
25173
25541
  return null;
25174
25542
  }
@@ -25176,8 +25544,8 @@ async function readPlayerState(home) {
25176
25544
  async function writePlayerState(state, home) {
25177
25545
  const file = playerPaths(home).state;
25178
25546
  try {
25179
- await fs35.mkdir(path39.dirname(file), { recursive: true, mode: 448 });
25180
- await fs35.writeFile(file, JSON.stringify(state, null, 2) + "\n", { mode: 384 });
25547
+ await fs37.mkdir(path41.dirname(file), { recursive: true, mode: 448 });
25548
+ await fs37.writeFile(file, JSON.stringify(state, null, 2) + "\n", { mode: 384 });
25181
25549
  } catch {
25182
25550
  }
25183
25551
  }
@@ -25185,8 +25553,8 @@ async function removePlayerFiles(home) {
25185
25553
  const paths = playerPaths(home);
25186
25554
  const removed = [];
25187
25555
  for (const file of [paths.credential, paths.state, paths.relaunch]) {
25188
- const existed = await fs35.stat(file).then(() => true, () => false);
25189
- await fs35.rm(file, { force: true }).catch(() => {
25556
+ const existed = await fs37.stat(file).then(() => true, () => false);
25557
+ await fs37.rm(file, { force: true }).catch(() => {
25190
25558
  });
25191
25559
  if (existed) removed.push(file);
25192
25560
  }
@@ -25320,14 +25688,14 @@ var PlayerClient = class {
25320
25688
  this.version = options.version ?? getCliVersion();
25321
25689
  this.timeoutMs = options.timeoutMs ?? 3e4;
25322
25690
  }
25323
- async call(path44, init2 = {}, provider) {
25691
+ async call(path46, init2 = {}, provider) {
25324
25692
  const headers = {
25325
25693
  Authorization: `Bearer ${this.token}`,
25326
25694
  [PLAYER_VERSION_HEADER]: this.version,
25327
25695
  ...provider ? { [EXTERNAL_PROVIDER_HEADER]: provider } : {},
25328
25696
  ...init2.body === void 0 ? {} : { "Content-Type": "application/json" }
25329
25697
  };
25330
- const res = await apiFetch(`${this.apiUrl}${path44}`, {
25698
+ const res = await apiFetch(`${this.apiUrl}${path46}`, {
25331
25699
  ...init2,
25332
25700
  headers,
25333
25701
  signal: AbortSignal.timeout(this.timeoutMs)
@@ -25396,10 +25764,10 @@ var PlayerClient = class {
25396
25764
 
25397
25765
  // src/lib/player/runners.ts
25398
25766
  import { spawn as spawn6, spawnSync } from "child_process";
25399
- import fs36 from "fs/promises";
25767
+ import fs38 from "fs/promises";
25400
25768
  import fsSync from "fs";
25401
25769
  import os9 from "os";
25402
- import path40 from "path";
25770
+ import path42 from "path";
25403
25771
  import { randomUUID as randomUUID3 } from "crypto";
25404
25772
  var WATCHER_RUNNERS = ["claude", "codex"];
25405
25773
  var RUNNER_FOR_FAMILY = { claude: "claude", chatgpt: "codex" };
@@ -25457,7 +25825,7 @@ function resolveOnPath(name, env = process.env, platform = process.platform) {
25457
25825
  const candidates = win ? [`${name}.cmd`, `${name}.exe`, `${name}.bat`, name] : [name];
25458
25826
  for (const dir of dirs) {
25459
25827
  for (const candidate of candidates) {
25460
- const file = path40.join(dir, candidate);
25828
+ const file = path42.join(dir, candidate);
25461
25829
  try {
25462
25830
  const stat = fsSync.statSync(file);
25463
25831
  if (!stat.isFile()) continue;
@@ -25621,17 +25989,17 @@ async function runJob(job, options = {}) {
25621
25989
  if (!resolved) {
25622
25990
  return { ok: false, release: "runner_unavailable", detail: `missing:${runner}`, durationMs: Date.now() - started };
25623
25991
  }
25624
- const cwd = path40.join(os9.tmpdir(), `genex-player-${randomUUID3()}`);
25625
- await fs36.mkdir(cwd, { recursive: true, mode: 448 });
25626
- const outFile = path40.join(cwd, "result.out");
25992
+ const cwd = path42.join(os9.tmpdir(), `genex-player-${randomUUID3()}`);
25993
+ await fs38.mkdir(cwd, { recursive: true, mode: 448 });
25994
+ const outFile = path42.join(cwd, "result.out");
25627
25995
  let argv;
25628
25996
  if (runner === "claude") {
25629
25997
  argv = claudeArgv(job, policy.claude ?? FALLBACK_POLICY.claude ?? {});
25630
25998
  } else {
25631
25999
  let schemaFile;
25632
26000
  if (job.outputFormat === "json" && job.schema !== void 0) {
25633
- schemaFile = path40.join(cwd, "schema.json");
25634
- await fs36.writeFile(schemaFile, JSON.stringify(job.schema), { mode: 384 });
26001
+ schemaFile = path42.join(cwd, "schema.json");
26002
+ await fs38.writeFile(schemaFile, JSON.stringify(job.schema), { mode: 384 });
25635
26003
  }
25636
26004
  argv = codexArgv(job, policy.codex ?? FALLBACK_POLICY.codex ?? {}, { cwd, outFile, ...schemaFile ? { schemaFile } : {} });
25637
26005
  }
@@ -25693,7 +26061,7 @@ async function runJob(job, options = {}) {
25693
26061
  if (!read2.ok) return { ok: false, release: "failed", detail: read2.detail, exitCode: code, durationMs };
25694
26062
  return { ok: true, output: read2.output, exitCode: code, durationMs };
25695
26063
  }
25696
- const text = await fs36.readFile(outFile, "utf8").catch(() => "");
26064
+ const text = await fs38.readFile(outFile, "utf8").catch(() => "");
25697
26065
  if (code !== 0 && !text.trim()) {
25698
26066
  const classified = classifyFailure(`${stderrText} exit ${code}`);
25699
26067
  return { ok: false, ...classified, detail: `exit_${code}`, exitCode: code, durationMs };
@@ -25702,15 +26070,15 @@ async function runJob(job, options = {}) {
25702
26070
  if (!read.ok) return { ok: false, release: "failed", detail: read.detail, exitCode: code, durationMs };
25703
26071
  return { ok: true, output: read.output, exitCode: code, durationMs };
25704
26072
  } finally {
25705
- await fs36.rm(cwd, { recursive: true, force: true }).catch(() => {
26073
+ await fs38.rm(cwd, { recursive: true, force: true }).catch(() => {
25706
26074
  });
25707
26075
  }
25708
26076
  }
25709
26077
 
25710
26078
  // src/lib/player/service.ts
25711
- import fs37 from "fs/promises";
26079
+ import fs39 from "fs/promises";
25712
26080
  import os10 from "os";
25713
- import path41 from "path";
26081
+ import path43 from "path";
25714
26082
  import { execFile } from "child_process";
25715
26083
  import { promisify } from "util";
25716
26084
  var run2 = promisify(execFile);
@@ -25727,9 +26095,9 @@ function servicePlatform(platform = process.platform) {
25727
26095
  }
25728
26096
  function servicePaths(platform, home = os10.homedir()) {
25729
26097
  const { log, errorLog } = playerPaths(home);
25730
- if (platform === "darwin") return { log, errorLog, unit: path41.join(home, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`) };
25731
- if (platform === "linux") return { log, errorLog, unit: path41.join(home, ".config", "systemd", "user", SYSTEMD_UNIT) };
25732
- return { log, errorLog, unit: path41.join(home, "AppData", "Roaming", "Microsoft", "Windows", "Start Menu", "Programs", "Startup", "genex-player.cmd") };
26098
+ if (platform === "darwin") return { log, errorLog, unit: path43.join(home, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`) };
26099
+ if (platform === "linux") return { log, errorLog, unit: path43.join(home, ".config", "systemd", "user", SYSTEMD_UNIT) };
26100
+ return { log, errorLog, unit: path43.join(home, "AppData", "Roaming", "Microsoft", "Windows", "Start Menu", "Programs", "Startup", "genex-player.cmd") };
25733
26101
  }
25734
26102
  function xmlEscape(value) {
25735
26103
  return value.replace(/[<>&'"]/g, (ch) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;", "'": "&apos;", '"': "&quot;" })[ch]);
@@ -25808,9 +26176,9 @@ function autostartCommand(opts) {
25808
26176
  }
25809
26177
  async function installService(input, platform = servicePlatform()) {
25810
26178
  const paths = servicePaths(platform, input.home);
25811
- await fs37.mkdir(path41.dirname(paths.unit), { recursive: true });
25812
- await fs37.mkdir(path41.dirname(paths.log), { recursive: true, mode: 448 });
25813
- await fs37.writeFile(paths.unit, unitFile(platform, input, paths), { mode: platform === "win32" ? 420 : 384 });
26179
+ await fs39.mkdir(path43.dirname(paths.unit), { recursive: true });
26180
+ await fs39.mkdir(path43.dirname(paths.log), { recursive: true, mode: 448 });
26181
+ await fs39.writeFile(paths.unit, unitFile(platform, input, paths), { mode: platform === "win32" ? 420 : 384 });
25814
26182
  if (playerDryRun()) return { unit: paths.unit, loaded: false, detail: "dry run \u2014 the unit was written, the system was not asked" };
25815
26183
  if (platform === "darwin") {
25816
26184
  const domain = `gui/${process.getuid?.() ?? 501}`;
@@ -25839,7 +26207,7 @@ async function installService(input, platform = servicePlatform()) {
25839
26207
  }
25840
26208
  async function serviceInstalled(home = os10.homedir(), platform = servicePlatform()) {
25841
26209
  const paths = servicePaths(platform, home);
25842
- return await fs37.stat(paths.unit).then(() => paths.unit, () => null);
26210
+ return await fs39.stat(paths.unit).then(() => paths.unit, () => null);
25843
26211
  }
25844
26212
  async function stopService(home = os10.homedir(), platform = servicePlatform()) {
25845
26213
  const paths = servicePaths(platform, home);
@@ -25869,15 +26237,15 @@ async function uninstallService(home = os10.homedir(), platform = servicePlatfor
25869
26237
  });
25870
26238
  }
25871
26239
  }
25872
- await fs37.rm(paths.unit, { force: true }).catch(() => {
26240
+ await fs39.rm(paths.unit, { force: true }).catch(() => {
25873
26241
  });
25874
26242
  return { unit: paths.unit, loaded: false, detail: present ? "removed" : "nothing installed" };
25875
26243
  }
25876
26244
 
25877
26245
  // src/lib/player/relaunch.ts
25878
- import fs38 from "fs/promises";
26246
+ import fs40 from "fs/promises";
25879
26247
  import fsSync2 from "fs";
25880
- import path42 from "path";
26248
+ import path44 from "path";
25881
26249
  import { spawn as spawn7 } from "child_process";
25882
26250
  var RELAUNCH_COOLDOWN_MS = 10 * 60 * 1e3;
25883
26251
  function relaunchPlan(input) {
@@ -25905,7 +26273,7 @@ function relaunchPossible() {
25905
26273
  }
25906
26274
  async function relaunchOnCooldown(home, now = Date.now()) {
25907
26275
  try {
25908
- const raw = await fs38.readFile(playerPaths(home).relaunch, "utf8");
26276
+ const raw = await fs40.readFile(playerPaths(home).relaunch, "utf8");
25909
26277
  const at = Date.parse(JSON.parse(raw).at ?? "");
25910
26278
  return Number.isFinite(at) && now - at < RELAUNCH_COOLDOWN_MS;
25911
26279
  } catch {
@@ -25915,8 +26283,8 @@ async function relaunchOnCooldown(home, now = Date.now()) {
25915
26283
  async function markRelaunch(version, home) {
25916
26284
  const file = playerPaths(home).relaunch;
25917
26285
  try {
25918
- await fs38.mkdir(path42.dirname(file), { recursive: true, mode: 448 });
25919
- await fs38.writeFile(file, JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), version }) + "\n", { mode: 384 });
26286
+ await fs40.mkdir(path44.dirname(file), { recursive: true, mode: 448 });
26287
+ await fs40.writeFile(file, JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), version }) + "\n", { mode: 384 });
25920
26288
  } catch {
25921
26289
  }
25922
26290
  }
@@ -25940,7 +26308,7 @@ async function relaunchWatcher(input) {
25940
26308
  let err;
25941
26309
  const paths = playerPaths(input.home);
25942
26310
  try {
25943
- await fs38.mkdir(paths.logDir, { recursive: true, mode: 448 });
26311
+ await fs40.mkdir(paths.logDir, { recursive: true, mode: 448 });
25944
26312
  out = fsSync2.openSync(paths.log, "a", 384);
25945
26313
  err = fsSync2.openSync(paths.errorLog, "a", 384);
25946
26314
  } catch {
@@ -26286,7 +26654,7 @@ async function runPlayer(opts = {}) {
26286
26654
  const log = createLogger({ quiet: opts.quiet || opts.json });
26287
26655
  const sub = opts.name?.trim() || "status";
26288
26656
  if (!SUBS4.includes(sub)) {
26289
- fail3(opts, log, `player ${sub}`, `Unknown subcommand ${sub}. Use: ${SUBS4.join(", ")}.`);
26657
+ fail4(opts, log, `player ${sub}`, `Unknown subcommand ${sub}. Use: ${SUBS4.join(", ")}.`);
26290
26658
  return;
26291
26659
  }
26292
26660
  if (opts.dryRun) process.env[PLAYER_DRY_RUN_ENV] = "1";
@@ -26307,7 +26675,7 @@ async function install(opts, log) {
26307
26675
  ...opts.timeoutSec ? { waitMs: opts.timeoutSec * 1e3 } : {}
26308
26676
  });
26309
26677
  } catch (error) {
26310
- fail3(opts, log, "player install", error instanceof Error ? error.message : String(error));
26678
+ fail4(opts, log, "player install", error instanceof Error ? error.message : String(error));
26311
26679
  return;
26312
26680
  }
26313
26681
  let record = {
@@ -26344,13 +26712,13 @@ async function install(opts, log) {
26344
26712
  log.warn(`This Genex needs player watcher ${error.minVersion} or newer; ${error.latestVersion} is current.`);
26345
26713
  if (!outcome2.launched) {
26346
26714
  log.plain(` ${c.cyan("\u2192")} Update and re-run: ${c.cyan(`npx -y @genex-ai/cli-demo@${error.latestVersion} player install`)}`);
26347
- fail3(opts, log, "player install", "player_update_required", { minVersion: error.minVersion, latestVersion: error.latestVersion });
26715
+ fail4(opts, log, "player install", "player_update_required", { minVersion: error.minVersion, latestVersion: error.latestVersion });
26348
26716
  return;
26349
26717
  }
26350
26718
  log.plain(` ${c.cyan("\u2192")} Picked up by ${c.cyan(`@genex-ai/cli-demo@${error.latestVersion}`)} \u2014 it finishes the install.`);
26351
26719
  return;
26352
26720
  }
26353
- fail3(opts, log, "player install", error instanceof PlayerAuthError ? "Genex refused the approval this machine just collected. Re-run `genex player install`." : `Couldn't reach Genex to register this machine (${error instanceof Error ? error.message : String(error)}).`);
26721
+ fail4(opts, log, "player install", error instanceof PlayerAuthError ? "Genex refused the approval this machine just collected. Re-run `genex player install`." : `Couldn't reach Genex to register this machine (${error instanceof Error ? error.message : String(error)}).`);
26354
26722
  return;
26355
26723
  }
26356
26724
  const platform = servicePlatform();
@@ -26421,8 +26789,8 @@ async function spawnDetachedRun(apiUrl, opts) {
26421
26789
  try {
26422
26790
  const paths = playerPaths();
26423
26791
  await fsp2.mkdir(paths.logDir, { recursive: true, mode: 448 });
26424
- out = fs39.openSync(paths.log, "a", 384);
26425
- err = fs39.openSync(paths.errorLog, "a", 384);
26792
+ out = fs41.openSync(paths.log, "a", 384);
26793
+ err = fs41.openSync(paths.errorLog, "a", 384);
26426
26794
  const args = [process.argv[1] ?? "", "player", "run", ...opts.apiUrl ? ["--api-url", apiUrl] : []];
26427
26795
  const child = spawn8(process.execPath, args, { detached: true, stdio: ["ignore", out, err] });
26428
26796
  child.on("error", () => {
@@ -26433,7 +26801,7 @@ async function spawnDetachedRun(apiUrl, opts) {
26433
26801
  return false;
26434
26802
  } finally {
26435
26803
  for (const fd of [out, err]) if (fd !== void 0) try {
26436
- fs39.closeSync(fd);
26804
+ fs41.closeSync(fd);
26437
26805
  } catch {
26438
26806
  }
26439
26807
  }
@@ -26444,11 +26812,11 @@ function usesNonDefaultStand(opts, apiUrl) {
26444
26812
  async function run3(opts, log) {
26445
26813
  const record = await readPlayerInstall();
26446
26814
  if (!record) {
26447
- fail3(opts, log, "player run", PLAYER_NOT_INSTALLED);
26815
+ fail4(opts, log, "player run", PLAYER_NOT_INSTALLED);
26448
26816
  return;
26449
26817
  }
26450
26818
  if (installExpired(record)) {
26451
- fail3(opts, log, "player run", `This machine's Genex approval expired on ${record.expiresAt} \u2014 run \`genex player install\` again.`);
26819
+ fail4(opts, log, "player run", `This machine's Genex approval expired on ${record.expiresAt} \u2014 run \`genex player install\` again.`);
26452
26820
  return;
26453
26821
  }
26454
26822
  const apiUrl = opts.apiUrl ? normalizeApiOrigin(getApiUrl(opts.apiUrl)) : record.apiUrl;
@@ -26632,14 +27000,14 @@ async function uninstall(opts, log) {
26632
27000
  writeJsonLine2({ command: "player uninstall", status: "ok", error: null, unit: service.unit, detail: service.detail, removed });
26633
27001
  return;
26634
27002
  }
26635
- log.success(`Removed: ${service.detail} (${path43.basename(service.unit)})`);
27003
+ log.success(`Removed: ${service.detail} (${path45.basename(service.unit)})`);
26636
27004
  for (const file of removed) log.dim(` deleted ${file}`);
26637
27005
  if (removed.length === 0) log.dim(" no credential was stored here");
26638
27006
  log.plain("");
26639
27007
  log.plain(" This machine can no longer answer in-game requests. The approval itself can also be");
26640
27008
  log.plain(" revoked from your Genex dashboard, which ends it everywhere at once.");
26641
27009
  }
26642
- function fail3(opts, log, command, message, extra = {}) {
27010
+ function fail4(opts, log, command, message, extra = {}) {
26643
27011
  if (opts.json) writeJsonLine2({ command, status: "failed", error: message, ...extra });
26644
27012
  else log.error(message);
26645
27013
  process.exitCode = 1;
@@ -26768,6 +27136,14 @@ ${c.bold("Usage")}
26768
27136
  genex rollback [--yes] Put players back on the previous version. One
26769
27137
  version back, instantly \u2014 your code is not
26770
27138
  changed, only what the play URL serves.
27139
+ genex cover [<file>] Show this game's cover and where it came from, or
27140
+ make <file> (PNG/JPEG/WebP, \u22648 MB) the cover: one
27141
+ real frame of the game \u2014 its signature moment,
27142
+ real gameplay, well lit, no menus or popups,
27143
+ landscape 16:9, no text. A cover the owner chose
27144
+ on the dashboard is never replaced from here.
27145
+ 'genex preview' also uploads a new
27146
+ .genex/scratch/cover.png by itself.
26771
27147
  genex make-remixable [options] Make THIS game remixable by everyone \u2014 migrates a
26772
27148
  private source onto a public managed genex repo.
26773
27149
  genex domain <sub> [host] Play this game on a domain you own:
@@ -27067,7 +27443,10 @@ ${c.bold("Options for `preview` / `publish`")}
27067
27443
  --description <text> (publish) Gallery description.
27068
27444
  --categories <list> (publish) 1-3 gallery categories, comma-separated.
27069
27445
  Valid: games, assets, physics, terrain, lighting, vfx.
27070
- --regenerate-cover (publish) Re-mint the disc cover (concept changed).
27446
+ --regenerate-cover (publish) Ask for a new AI cover. Only on stands with AI
27447
+ covers switched on (otherwise ignored), and it never
27448
+ replaces a cover the owner or 'genex cover' set \u2014 for a
27449
+ real frame of the game, use 'genex cover <file>'.
27071
27450
  --source-repo-url <url> (publish) Upstream repo this game is a port of \u2014
27072
27451
  shown as "Ported from \u2026" credit on the game page.
27073
27452
  --source-author <name> (publish) Upstream author to credit.
@@ -27084,6 +27463,13 @@ ${c.bold("Options for `list`")}
27084
27463
  --env <path> Token env file (default: ~/.genex/env).
27085
27464
  --auth-url <url> Override the auth site (used only if sign-in is needed).
27086
27465
 
27466
+ ${c.bold("Options for `cover`")}
27467
+ <file> The image to make the cover (positional). Omit it to see
27468
+ the current cover and where it came from.
27469
+ --json One machine-readable result object.
27470
+ --api-url <url> Override the API base URL.
27471
+ --env <path> Token env file (default: ~/.genex/env).
27472
+
27087
27473
  ${c.bold("Options for `character` / `animations search`")}
27088
27474
  --image <path|url> Build from supplied PNG/JPEG references; repeat up to 4 times.
27089
27475
  One view per image, same character; local files total \u22644 MB,
@@ -27147,6 +27533,8 @@ ${c.bold("Examples")}
27147
27533
  genex preview
27148
27534
  genex publish
27149
27535
  genex publish --categories games,vfx
27536
+ genex cover
27537
+ genex cover .genex/scratch/cover.png
27150
27538
  genex make-remixable
27151
27539
  genex domain add play.mygame.com
27152
27540
  genex domain list
@@ -28601,6 +28989,9 @@ async function main() {
28601
28989
  case "rollback":
28602
28990
  await runRollback(parsed.options);
28603
28991
  break;
28992
+ case "cover":
28993
+ await runCover({ ...parsed.options, imagePath: parsed.options.name });
28994
+ break;
28604
28995
  case "publish":
28605
28996
  await runPublish(parsed.options);
28606
28997
  break;