@genex-ai/cli-demo 1.35.4-dev.767 → 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-
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
693
|
-
import
|
|
694
|
-
import
|
|
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
|
|
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 ${
|
|
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 ${
|
|
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
|
|
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
|
-
|
|
5776
|
-
|
|
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
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
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 =
|
|
5880
|
-
if ((await
|
|
5881
|
-
const value = JSON.parse(await
|
|
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 =
|
|
5889
|
-
await
|
|
5890
|
-
const temp =
|
|
5891
|
-
await
|
|
5892
|
-
await
|
|
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
|
|
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
|
|
6168
|
+
const entries = await fs19.readdir(directory);
|
|
5899
6169
|
if (!entries.length) return null;
|
|
5900
|
-
const genex = await
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
5928
|
-
await
|
|
5929
|
-
if (await
|
|
5930
|
-
await
|
|
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 =
|
|
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
|
|
6210
|
+
const handle = await fs19.open(lock, "wx", 384);
|
|
5941
6211
|
await handle.writeFile(String(process.pid));
|
|
5942
6212
|
await handle.close();
|
|
5943
|
-
return () =>
|
|
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
|
|
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
|
|
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 =
|
|
6040
|
-
const pkg = JSON.parse(await
|
|
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
|
|
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
|
|
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 =
|
|
6080
|
-
const config = await
|
|
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
|
|
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 =
|
|
6089
|
-
const manifest = JSON.parse(await
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 ?
|
|
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 ??=
|
|
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 ??
|
|
6150
|
-
await
|
|
6151
|
-
if ((await
|
|
6152
|
-
await
|
|
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 =
|
|
6429
|
+
const staging = path19.join(state.directory, ".genex", "remix-stage");
|
|
6160
6430
|
if (state.stage === "download") {
|
|
6161
|
-
await
|
|
6162
|
-
await
|
|
6163
|
-
const archive =
|
|
6164
|
-
await
|
|
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
|
|
6449
|
+
pkg = JSON.parse(await fs19.readFile(path19.join(sourceDir, "package.json"), "utf8"));
|
|
6180
6450
|
} catch (error) {
|
|
6181
|
-
const staticEntry = await
|
|
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
|
|
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
|
|
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 =
|
|
6196
|
-
if (await
|
|
6197
|
-
if (await
|
|
6198
|
-
await
|
|
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
|
|
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
|
|
6279
|
-
import
|
|
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
|
|
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 :
|
|
6388
|
-
const dest = t.full ? t.baseDir :
|
|
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(
|
|
6397
|
-
await writeSkillsMarker(
|
|
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() ||
|
|
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(
|
|
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
|
|
6804
|
+
import fs21 from "fs/promises";
|
|
6535
6805
|
import os6 from "os";
|
|
6536
|
-
import
|
|
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
|
|
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
|
|
6633
|
-
const fresh =
|
|
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
|
|
6637
|
-
for (const entry of await
|
|
6638
|
-
await
|
|
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
|
|
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 =
|
|
6921
|
+
const file = path21.join(cwd, ".env");
|
|
6652
6922
|
let content;
|
|
6653
6923
|
try {
|
|
6654
|
-
content = await
|
|
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
|
|
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
|
|
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
|
|
6722
|
-
import
|
|
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
|
|
6783
|
-
const fresh =
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
7088
|
+
const dest = path22.join(cwd, ".genex", `replaced-${stamp}`);
|
|
6819
7089
|
try {
|
|
6820
|
-
const entries = (await
|
|
7090
|
+
const entries = (await fs22.readdir(cwd)).filter((e) => !isMachineLocal(e));
|
|
6821
7091
|
if (entries.length === 0) return null;
|
|
6822
|
-
await
|
|
7092
|
+
await fs22.mkdir(dest, { recursive: true });
|
|
6823
7093
|
for (const entry of entries) {
|
|
6824
|
-
await
|
|
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
|
|
7104
|
+
for (const entry of await fs22.readdir(dest)) {
|
|
6835
7105
|
if (isMachineLocal(entry)) continue;
|
|
6836
|
-
await
|
|
7106
|
+
await fs22.rm(path22.join(dest, entry), { recursive: true, force: true });
|
|
6837
7107
|
}
|
|
6838
|
-
for (const entry of await
|
|
7108
|
+
for (const entry of await fs22.readdir(src)) {
|
|
6839
7109
|
if (isMachineLocal(entry)) continue;
|
|
6840
|
-
await
|
|
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
|
|
6846
|
-
import
|
|
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 =
|
|
7204
|
+
const file = path23.join(cwd, ".env");
|
|
6935
7205
|
let content;
|
|
6936
7206
|
try {
|
|
6937
|
-
content = await
|
|
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
|
|
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 =
|
|
7217
|
+
const file = path23.join(cwd, "src", "genex.config.ts");
|
|
6948
7218
|
let content;
|
|
6949
7219
|
try {
|
|
6950
|
-
content = await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
7961
|
-
import
|
|
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
|
|
8144
|
-
import
|
|
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
|
|
8149
|
-
import
|
|
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) =>
|
|
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
|
|
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
|
|
8199
|
-
await
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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[
|
|
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: ${
|
|
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
|
|
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
|
|
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 =
|
|
9036
|
-
await
|
|
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
|
|
9667
|
-
import
|
|
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 ${
|
|
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
|
|
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(`${
|
|
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(`${
|
|
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:
|
|
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 ${
|
|
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
|
|
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 =
|
|
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
|
|
10181
|
-
import
|
|
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
|
|
19312
|
-
import
|
|
19313
|
-
var ANIMS_DEST =
|
|
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 =
|
|
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 =
|
|
19339
|
-
const gameManifestPath =
|
|
19706
|
+
const destDir = path30.join(root, ANIMS_DEST);
|
|
19707
|
+
const gameManifestPath = path30.join(destDir, "manifest.json");
|
|
19340
19708
|
if (opts.reset) {
|
|
19341
|
-
await
|
|
19342
|
-
log.step(`Cleared ${c.cyan(ANIMS_DEST +
|
|
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 =
|
|
19748
|
+
const cacheDir = path30.join(
|
|
19381
19749
|
opts.cacheDir ?? getAnimsCacheDir(),
|
|
19382
19750
|
`${manifest.library}-v${manifest.version}`
|
|
19383
19751
|
);
|
|
19384
|
-
await
|
|
19385
|
-
await
|
|
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 =
|
|
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 =
|
|
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
|
|
19771
|
+
await fs29.writeFile(cached, buf);
|
|
19404
19772
|
}
|
|
19405
|
-
await
|
|
19773
|
+
await fs29.copyFile(cached, dest);
|
|
19406
19774
|
installedCount++;
|
|
19407
19775
|
addedBytes += entry.bytes;
|
|
19408
|
-
log.dim(` ${
|
|
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
|
|
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 +
|
|
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 =
|
|
19463
|
-
const manifest = JSON.parse(await
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
19797
|
-
var ASSETS_DEST =
|
|
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 =
|
|
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 +
|
|
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:
|
|
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:
|
|
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 =
|
|
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
|
|
19838
|
-
await
|
|
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(
|
|
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 =
|
|
19924
|
-
await
|
|
19925
|
-
await
|
|
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 =
|
|
20071
|
-
await
|
|
20072
|
-
await
|
|
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
|
|
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
|
|
20086
|
-
import
|
|
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 =
|
|
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
|
|
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
|
-
|
|
20568
|
+
fail3(opts, target.message, "character");
|
|
20201
20569
|
return;
|
|
20202
20570
|
}
|
|
20203
20571
|
const ctx = await context(opts);
|
|
20204
20572
|
if (!ctx) {
|
|
20205
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
21214
|
+
async function readVideo(path46, log) {
|
|
20847
21215
|
let bytes;
|
|
20848
21216
|
try {
|
|
20849
|
-
bytes = await readFile(
|
|
21217
|
+
bytes = await readFile(path46);
|
|
20850
21218
|
} catch {
|
|
20851
|
-
log.error(`Can't read ${
|
|
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(
|
|
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,
|
|
20861
|
-
const contentType = /\.mov$/i.test(
|
|
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(
|
|
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
|
|
21209
|
-
import
|
|
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
|
|
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
|
|
22852
|
+
const st = await fs32.stat(sel).catch(() => null);
|
|
22485
22853
|
if (st?.isDirectory()) {
|
|
22486
|
-
const names2 = await
|
|
22487
|
-
for (const n of names2.sort()) if (n.endsWith(".npz")) out.push(
|
|
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
|
|
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 =
|
|
22912
|
+
const stem = path33.basename(file).replace(/\.npz$/, "");
|
|
22545
22913
|
try {
|
|
22546
|
-
reports.push(analyzeTake(stem, await
|
|
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
|
|
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 =
|
|
22970
|
+
const stem = path33.basename(file).replace(/\.npz$/, "");
|
|
22603
22971
|
try {
|
|
22604
|
-
inputs.push({ stem, take: loadTake(await
|
|
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 ??
|
|
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
|
|
22994
|
+
await fs32.mkdir(path33.dirname(path33.resolve(opts.out)), { recursive: true });
|
|
22627
22995
|
const json = JSON.stringify(result.data);
|
|
22628
|
-
await
|
|
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 =
|
|
23014
|
+
var MOTION_DEST = path33.join("src", "motion");
|
|
22647
23015
|
async function motionInstall(opts, log) {
|
|
22648
|
-
const srcDir =
|
|
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 +
|
|
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 =
|
|
22664
|
-
const exists5 = await
|
|
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 ${
|
|
23035
|
+
log.dim(` skipped ${path33.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
|
|
22668
23036
|
continue;
|
|
22669
23037
|
}
|
|
22670
|
-
await
|
|
22671
|
-
await
|
|
23038
|
+
await fs32.mkdir(path33.dirname(dest), { recursive: true });
|
|
23039
|
+
await fs32.copyFile(path33.join(srcDir, rel), dest);
|
|
22672
23040
|
copied++;
|
|
22673
|
-
log.dim(` ${
|
|
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
|
|
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
|
|
22752
|
-
import
|
|
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
|
|
22757
|
-
const p =
|
|
22758
|
-
await
|
|
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-
|
|
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 =
|
|
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 ??
|
|
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
|
|
22889
|
-
await
|
|
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
|
|
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 =
|
|
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
|
|
23414
|
+
import fs34 from "fs";
|
|
23047
23415
|
import fsp from "fs/promises";
|
|
23048
|
-
import
|
|
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 ?
|
|
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 =
|
|
23153
|
-
if (!
|
|
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(
|
|
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 =
|
|
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 =
|
|
23173
|
-
if (
|
|
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 =
|
|
23202
|
-
await fsp.mkdir(
|
|
23203
|
-
await fsp.copyFile(
|
|
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(
|
|
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(
|
|
23211
|
-
await fsp.writeFile(
|
|
23212
|
-
await fsp.writeFile(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
23230
|
-
await fsp.writeFile(
|
|
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
|
-
|
|
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
|
|
23650
|
+
import path38 from "path";
|
|
23283
23651
|
|
|
23284
23652
|
// src/commands/doctor.ts
|
|
23285
|
-
import
|
|
23653
|
+
import path37 from "path";
|
|
23286
23654
|
|
|
23287
23655
|
// src/commands/llm.ts
|
|
23288
|
-
import
|
|
23289
|
-
import
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23829
|
+
fail5("llm bench", LLM_BENCH_APPROVAL_REQUIRED);
|
|
23462
23830
|
return;
|
|
23463
23831
|
}
|
|
23464
23832
|
if (!Number.isInteger(approvedCredits) || approvedCredits < 1) {
|
|
23465
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
-
|
|
24132
|
-
|
|
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 =
|
|
24193
|
-
await
|
|
24194
|
-
await
|
|
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
|
|
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(
|
|
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 =
|
|
24891
|
-
const { copied, updated } = await copyTemplates(
|
|
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
|
|
24952
|
-
import
|
|
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
|
|
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
|
|
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
|
-
|
|
25356
|
+
fail5(BUDGET_APPROVAL_REQUIRED);
|
|
24989
25357
|
return;
|
|
24990
25358
|
}
|
|
24991
25359
|
if (setting && (!Number.isInteger(opts.assets) || opts.assets < 0)) {
|
|
24992
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
25477
|
+
import fs41 from "fs";
|
|
25110
25478
|
import fsp2 from "fs/promises";
|
|
25111
25479
|
import os12 from "os";
|
|
25112
|
-
import
|
|
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
|
|
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() :
|
|
25125
|
-
const logDir =
|
|
25492
|
+
const dir = home === os8.homedir() ? getGenexDir() : path40.join(home, ".genex");
|
|
25493
|
+
const logDir = path40.join(dir, "logs");
|
|
25126
25494
|
return {
|
|
25127
|
-
credential:
|
|
25128
|
-
state:
|
|
25129
|
-
relaunch:
|
|
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:
|
|
25132
|
-
errorLog:
|
|
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
|
|
25138
|
-
import
|
|
25505
|
+
import fs37 from "fs/promises";
|
|
25506
|
+
import path41 from "path";
|
|
25139
25507
|
async function readPlayerInstall(home) {
|
|
25140
25508
|
try {
|
|
25141
|
-
const raw = await
|
|
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
|
|
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
|
|
25180
|
-
await
|
|
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
|
|
25189
|
-
await
|
|
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(
|
|
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}${
|
|
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
|
|
25767
|
+
import fs38 from "fs/promises";
|
|
25400
25768
|
import fsSync from "fs";
|
|
25401
25769
|
import os9 from "os";
|
|
25402
|
-
import
|
|
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 =
|
|
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 =
|
|
25625
|
-
await
|
|
25626
|
-
const outFile =
|
|
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 =
|
|
25634
|
-
await
|
|
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
|
|
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
|
|
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
|
|
26079
|
+
import fs39 from "fs/promises";
|
|
25712
26080
|
import os10 from "os";
|
|
25713
|
-
import
|
|
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:
|
|
25731
|
-
if (platform === "linux") return { log, errorLog, unit:
|
|
25732
|
-
return { log, errorLog, unit:
|
|
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) => ({ "<": "<", ">": ">", "&": "&", "'": "'", '"': """ })[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
|
|
25812
|
-
await
|
|
25813
|
-
await
|
|
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
|
|
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
|
|
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
|
|
26246
|
+
import fs40 from "fs/promises";
|
|
25879
26247
|
import fsSync2 from "fs";
|
|
25880
|
-
import
|
|
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
|
|
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
|
|
25919
|
-
await
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
26425
|
-
err =
|
|
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
|
-
|
|
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
|
-
|
|
26815
|
+
fail4(opts, log, "player run", PLAYER_NOT_INSTALLED);
|
|
26448
26816
|
return;
|
|
26449
26817
|
}
|
|
26450
26818
|
if (installExpired(record)) {
|
|
26451
|
-
|
|
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} (${
|
|
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
|
|
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)
|
|
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;
|