@genex-ai/cli-demo 1.36.7-dev.908 → 1.36.8-dev.910
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/README.md +1 -0
- package/dist/index.js +189 -5
- package/package.json +1 -1
- package/templates/skills/genex-cover/SKILL.md +3 -0
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ genex list # list your games — slug, status, page link (find t
|
|
|
12
12
|
genex preview # build + push to the hosted draft URL (unlisted)
|
|
13
13
|
genex publish # build + push, then list the game in the gallery
|
|
14
14
|
genex cover [file] # show the game's cover, or make one real frame (PNG/JPEG/WebP) the cover
|
|
15
|
+
genex cover --clip <file> --commit <sha> # make a recording of real play (MP4/WebM) the cover clip
|
|
15
16
|
genex make-remixable # make this game remixable — migrate a private source to a public repo
|
|
16
17
|
genex model "<prompt>" # generate a 3D model → prints an asset URL
|
|
17
18
|
genex sfx "<prompt>" # generate a sound fx → prints an asset URL
|
package/dist/index.js
CHANGED
|
@@ -6344,7 +6344,9 @@ async function fetchOwnerProject(apiUrl, token, slug) {
|
|
|
6344
6344
|
return {
|
|
6345
6345
|
status: project.status ?? "draft",
|
|
6346
6346
|
coverUrl: project.thumbnailUrl ?? null,
|
|
6347
|
-
coverSource: "coverSource" in project ? project.coverSource ?? null : void 0
|
|
6347
|
+
coverSource: "coverSource" in project ? project.coverSource ?? null : void 0,
|
|
6348
|
+
clipUrl: project.coverClipUrl ?? null,
|
|
6349
|
+
clipSource: project.coverClipSource ?? null
|
|
6348
6350
|
};
|
|
6349
6351
|
} catch {
|
|
6350
6352
|
return null;
|
|
@@ -6525,6 +6527,134 @@ async function uploadCover(args) {
|
|
|
6525
6527
|
const view = { coverUrl: body?.coverUrl ?? null, coverSource: body?.coverSource ?? null };
|
|
6526
6528
|
return body?.applied === false ? { kind: "outranked", ...view } : { kind: "applied", ...view };
|
|
6527
6529
|
}
|
|
6530
|
+
var CLIP_MAX_BYTES = 8 * 1024 * 1024;
|
|
6531
|
+
var BUILD_COMMIT = /^[0-9a-f]{7,40}$/;
|
|
6532
|
+
function sniffClip(bytes) {
|
|
6533
|
+
if (bytes.byteLength < 16) return null;
|
|
6534
|
+
const b = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
6535
|
+
if (b.toString("ascii", 4, 8) === "ftyp") return "video/mp4";
|
|
6536
|
+
if (b.readUInt32BE(0) === 440786851) return "video/webm";
|
|
6537
|
+
return null;
|
|
6538
|
+
}
|
|
6539
|
+
function normaliseBuildCommit(commit) {
|
|
6540
|
+
const sha = commit?.trim().toLowerCase() ?? "";
|
|
6541
|
+
return BUILD_COMMIT.test(sha) ? sha : null;
|
|
6542
|
+
}
|
|
6543
|
+
var CLIP_CLOSED = "nothing was uploaded";
|
|
6544
|
+
function clipRefusal(res, error, what) {
|
|
6545
|
+
const status2 = res.status;
|
|
6546
|
+
const wait = waitWords(res.headers.get("Retry-After"));
|
|
6547
|
+
if (status2 === 401) return "Genex refused this machine's sign-in \u2014 run `npx genex auth`, then try again.";
|
|
6548
|
+
if (error === "cover_clip_off") return `Cover clips are switched off on this Genex stand \u2014 ${CLIP_CLOSED}.`;
|
|
6549
|
+
if (error === "cover_clip_lane_closed") return `This Genex stand doesn't take cover clips from the CLI yet \u2014 ${CLIP_CLOSED}.`;
|
|
6550
|
+
if (status2 === 403) return "Only the game's owner can set its cover clip, and this machine is signed in as someone else.";
|
|
6551
|
+
if (status2 === 404) return "Genex doesn't know this game under this account \u2014 check `.genex/project.json` (or `npx genex link <slug>`).";
|
|
6552
|
+
if (error === "cover_clip_not_a_game") return "This project is an asset page, not a game \u2014 it takes no cover clip.";
|
|
6553
|
+
if (error === "cover_clip_taken_down") return "This game was taken down \u2014 it takes no cover clip.";
|
|
6554
|
+
if (error === "cover_clip_stale_build") {
|
|
6555
|
+
return "That --commit is not a build this game ships \u2014 record the build on its preview (draft) or live (published) channel and pass that build's commit.";
|
|
6556
|
+
}
|
|
6557
|
+
if (status2 === 413) return `That recording is too big for a cover clip (at most ${CLIP_MAX_BYTES / 1024 / 1024} MB).`;
|
|
6558
|
+
if (error === "cover_clip_rate_limited") {
|
|
6559
|
+
return `Too many cover clip uploads on this account for now (rate limited) \u2014 Genex takes the next one ${wait}.`;
|
|
6560
|
+
}
|
|
6561
|
+
if (error === "cover_clip_capacity") return "Today's cover clips are used up (daily cap) \u2014 Genex takes the next one after midnight UTC.";
|
|
6562
|
+
if (error === "cover_clip_busy") return `Genex is busy encoding another clip for this game \u2014 try again ${wait}.`;
|
|
6563
|
+
if (error === "cover_clip_upload_failed") return "Genex couldn't read the uploaded clip yet \u2014 try again.";
|
|
6564
|
+
if (error === "cover_clip_unavailable" || error === "cover_clip_encode_failed") {
|
|
6565
|
+
return "Genex's clip encoder is unavailable right now \u2014 try again in a few minutes.";
|
|
6566
|
+
}
|
|
6567
|
+
return `Couldn't ${what} (HTTP ${status2}) \u2014 try again.`;
|
|
6568
|
+
}
|
|
6569
|
+
async function uploadClip(args) {
|
|
6570
|
+
const { apiUrl, token, projectId, bytes, label } = args;
|
|
6571
|
+
const buildCommit = normaliseBuildCommit(args.buildCommit);
|
|
6572
|
+
if (!buildCommit) return { kind: "invalid", message: `--commit must be the build's git sha (7 to 40 hex characters).` };
|
|
6573
|
+
const type = sniffClip(bytes);
|
|
6574
|
+
if (!type) return { kind: "invalid", message: `${label} is not an MP4 or WebM video.` };
|
|
6575
|
+
if (bytes.byteLength > CLIP_MAX_BYTES) {
|
|
6576
|
+
return { kind: "invalid", message: `${label} is ${mb(bytes.byteLength)}; a cover clip recording can be at most ${CLIP_MAX_BYTES / 1024 / 1024} MB.` };
|
|
6577
|
+
}
|
|
6578
|
+
const headers = { "Content-Type": "application/json", Authorization: `Bearer ${token}` };
|
|
6579
|
+
const base = `${apiUrl}/api/projects/${encodeURIComponent(projectId)}/cover/clip`;
|
|
6580
|
+
const errorOf = async (res) => await res.json().catch(() => null);
|
|
6581
|
+
let minted;
|
|
6582
|
+
try {
|
|
6583
|
+
minted = await apiFetch(`${base}/upload-url`, { method: "POST", headers, body: JSON.stringify({ bytes: bytes.byteLength, contentType: type }) });
|
|
6584
|
+
} catch {
|
|
6585
|
+
return { kind: "failed", message: "Couldn't reach Genex to upload the cover clip." };
|
|
6586
|
+
}
|
|
6587
|
+
if (printedStructuredError(minted)) return { kind: "failed", message: null };
|
|
6588
|
+
if (!minted.ok) return { kind: "failed", message: clipRefusal(minted, (await errorOf(minted))?.error, "start the cover clip upload") };
|
|
6589
|
+
const grant = await minted.json().catch(() => null);
|
|
6590
|
+
if (!grant?.uploadUrl || !grant.key) {
|
|
6591
|
+
return { kind: "failed", message: "Genex answered the cover clip upload unexpectedly \u2014 nothing was uploaded." };
|
|
6592
|
+
}
|
|
6593
|
+
let put;
|
|
6594
|
+
try {
|
|
6595
|
+
put = await fetch(grant.uploadUrl, {
|
|
6596
|
+
method: "PUT",
|
|
6597
|
+
// Both are part of what the URL was signed for; send them back unchanged.
|
|
6598
|
+
headers: { "Content-Type": type, "Content-Length": String(bytes.byteLength) },
|
|
6599
|
+
body: new Uint8Array(bytes)
|
|
6600
|
+
});
|
|
6601
|
+
} catch {
|
|
6602
|
+
return { kind: "failed", message: "The cover clip upload didn't reach storage (network error) \u2014 try again." };
|
|
6603
|
+
}
|
|
6604
|
+
if (!put.ok) return { kind: "failed", message: `The cover clip upload failed (HTTP ${put.status}) \u2014 try again.` };
|
|
6605
|
+
let done;
|
|
6606
|
+
try {
|
|
6607
|
+
done = await apiFetch(`${base}/commit`, { method: "POST", headers, body: JSON.stringify({ key: grant.key, buildCommit }) });
|
|
6608
|
+
} catch {
|
|
6609
|
+
return { kind: "failed", message: "Couldn't reach Genex to finish the cover clip upload." };
|
|
6610
|
+
}
|
|
6611
|
+
if (printedStructuredError(done)) return { kind: "failed", message: null };
|
|
6612
|
+
const body = await done.json().catch(() => null);
|
|
6613
|
+
if (done.status === 422 && body?.error === "cover_clip_rejected") {
|
|
6614
|
+
return { kind: "rejected", reason: body.reason ?? "rejected", stats: body.stats ?? null };
|
|
6615
|
+
}
|
|
6616
|
+
if (!done.ok) return { kind: "failed", message: clipRefusal(done, body?.error, "set the cover clip") };
|
|
6617
|
+
const view = { clipUrl: body?.clipUrl ?? null, clipSource: body?.clipSource ?? null };
|
|
6618
|
+
return body?.applied === false ? { kind: "outranked", ...view } : { kind: "applied", ...view };
|
|
6619
|
+
}
|
|
6620
|
+
var CLIP_SOURCE_LABEL = {
|
|
6621
|
+
owner: "recorded by the owner",
|
|
6622
|
+
agent: "sent from the build (genex cover --clip)",
|
|
6623
|
+
owner_play: "recorded while the owner played"
|
|
6624
|
+
};
|
|
6625
|
+
function describeClipSource(source) {
|
|
6626
|
+
if (source === null) return "none";
|
|
6627
|
+
return CLIP_SOURCE_LABEL[source] ?? source;
|
|
6628
|
+
}
|
|
6629
|
+
var CLIP_REJECTION = {
|
|
6630
|
+
too_dark: "is too dark to read as the game on a card",
|
|
6631
|
+
flat: "is almost one flat colour \u2014 a loading screen or a fade reads as nothing",
|
|
6632
|
+
static: "barely moves \u2014 a frozen or paused screen is not a clip",
|
|
6633
|
+
flashing: "flashes too fast to show anyone (a strobe can hurt whoever watches it)",
|
|
6634
|
+
too_short: "is too short (record 5\u20136 s)",
|
|
6635
|
+
too_long: "is too long (at most 12 s)"
|
|
6636
|
+
};
|
|
6637
|
+
function reportClipOutcome(log, outcome2, opts) {
|
|
6638
|
+
switch (outcome2.kind) {
|
|
6639
|
+
case "applied":
|
|
6640
|
+
log.success(`Cover clip set \u2014 ${opts.label} is this game's cover clip now.`);
|
|
6641
|
+
if (outcome2.clipUrl) log.dim(` ${outcome2.clipUrl}`);
|
|
6642
|
+
return;
|
|
6643
|
+
case "outranked":
|
|
6644
|
+
log.info("Cover clip unchanged \u2014 the owner chose this game's clip themselves (or removed it), and that choice stands. Nothing to retry.");
|
|
6645
|
+
return;
|
|
6646
|
+
case "rejected":
|
|
6647
|
+
log.error(`Cover clip not used \u2014 ${opts.label} ${CLIP_REJECTION[outcome2.reason] ?? `was refused (${outcome2.reason})`}.`);
|
|
6648
|
+
log.dim(" Record 5\u20136 s of real, moving, lit gameplay and run this again \u2014 never change the game for a clip.");
|
|
6649
|
+
return;
|
|
6650
|
+
case "invalid":
|
|
6651
|
+
log.error(`Cover clip not used \u2014 ${outcome2.message}`);
|
|
6652
|
+
return;
|
|
6653
|
+
case "failed":
|
|
6654
|
+
if (outcome2.message) log.error(outcome2.message);
|
|
6655
|
+
return;
|
|
6656
|
+
}
|
|
6657
|
+
}
|
|
6528
6658
|
var pct = (n) => `${Math.round(n * 100)}%`;
|
|
6529
6659
|
function reportCoverOutcome(log, outcome2, opts) {
|
|
6530
6660
|
const fail5 = (m) => opts.asError ? log.error(m) : log.warn(m);
|
|
@@ -8824,6 +8954,11 @@ async function runCover(opts) {
|
|
|
8824
8954
|
const apiUrl = getApiUrl(opts.apiUrl ?? meta.apiUrl);
|
|
8825
8955
|
const token = await readUserToken(opts.envPath, apiUrl);
|
|
8826
8956
|
if (!token) return fail2("Not signed in. Run `genex auth` first.", opts, log);
|
|
8957
|
+
const clip = opts.clipPath?.trim();
|
|
8958
|
+
if (clip || opts.commit !== void 0) {
|
|
8959
|
+
await sendClip(apiUrl, token, meta.id, clip, opts, log);
|
|
8960
|
+
return;
|
|
8961
|
+
}
|
|
8827
8962
|
const file = opts.imagePath?.trim();
|
|
8828
8963
|
if (!file) {
|
|
8829
8964
|
await showCover(apiUrl, token, meta.slug, opts, log);
|
|
@@ -8860,8 +8995,36 @@ async function rememberNomination(bytes, file, outcome2) {
|
|
|
8860
8995
|
const stale = await fs24.readFile(scratch).catch(() => null);
|
|
8861
8996
|
if (stale) await rememberCoverUpload(stale, scratch, outcome2);
|
|
8862
8997
|
}
|
|
8998
|
+
async function sendClip(apiUrl, token, projectId, clip, opts, log) {
|
|
8999
|
+
const file = clip ?? "";
|
|
9000
|
+
const invalid = (message) => finishClip({ kind: "invalid", message }, file, opts, log);
|
|
9001
|
+
if (!clip) return invalid("--commit goes with --clip <file>: pass the recording to send.");
|
|
9002
|
+
if (opts.imagePath?.trim()) return invalid("Pass either an image (the cover) or --clip <file> (the cover clip), not both.");
|
|
9003
|
+
if (!opts.commit?.trim()) return invalid("--clip needs --commit <sha>: the git commit of the build the recording shows.");
|
|
9004
|
+
let bytes;
|
|
9005
|
+
try {
|
|
9006
|
+
const stat = await fs24.stat(clip);
|
|
9007
|
+
if (!stat.isFile()) throw new Error("not a file");
|
|
9008
|
+
if (stat.size > CLIP_MAX_BYTES) {
|
|
9009
|
+
return invalid(`${clip} is ${(stat.size / 1024 / 1024).toFixed(1)} MB; a cover clip recording can be at most ${CLIP_MAX_BYTES / 1024 / 1024} MB.`);
|
|
9010
|
+
}
|
|
9011
|
+
bytes = await fs24.readFile(clip);
|
|
9012
|
+
} catch {
|
|
9013
|
+
return invalid(`Can't read ${clip} \u2014 pass the path of an MP4 or WebM recording.`);
|
|
9014
|
+
}
|
|
9015
|
+
log.step(`Uploading ${path24.basename(clip)} as the cover clip\u2026`);
|
|
9016
|
+
const outcome2 = await uploadClip({ apiUrl, token, projectId, bytes, label: clip, buildCommit: opts.commit });
|
|
9017
|
+
finishClip(outcome2, clip, opts, log);
|
|
9018
|
+
}
|
|
9019
|
+
function finishClip(outcome2, file, opts, log) {
|
|
9020
|
+
if (opts.json) process.stdout.write(`${JSON.stringify({ file: file || null, ...outcome2 })}
|
|
9021
|
+
`);
|
|
9022
|
+
else reportClipOutcome(log, outcome2, { label: file });
|
|
9023
|
+
if (outcome2.kind !== "applied" && outcome2.kind !== "outranked") process.exitCode = 1;
|
|
9024
|
+
}
|
|
8863
9025
|
function fail2(message, opts, log) {
|
|
8864
|
-
|
|
9026
|
+
const file = opts.clipPath?.trim() || opts.imagePath?.trim() || null;
|
|
9027
|
+
if (opts.json) process.stdout.write(`${JSON.stringify({ file, kind: "failed", message })}
|
|
8865
9028
|
`);
|
|
8866
9029
|
else log.error(message);
|
|
8867
9030
|
process.exitCode = 1;
|
|
@@ -8884,7 +9047,8 @@ async function showCover(apiUrl, token, slug, opts, log) {
|
|
|
8884
9047
|
const view = await fetchOwnerProject(apiUrl, token, slug);
|
|
8885
9048
|
if (!view) return fail2("Couldn't read this game from Genex \u2014 check the sign-in (`genex doctor`) and try again.", opts, log);
|
|
8886
9049
|
if (opts.json) {
|
|
8887
|
-
|
|
9050
|
+
const clip = { clipUrl: view.clipUrl, clipSource: view.clipSource };
|
|
9051
|
+
process.stdout.write(`${JSON.stringify({ coverUrl: view.coverUrl, coverSource: view.coverSource ?? null, ...clip })}
|
|
8888
9052
|
`);
|
|
8889
9053
|
return;
|
|
8890
9054
|
}
|
|
@@ -8894,6 +9058,10 @@ async function showCover(apiUrl, token, slug, opts, log) {
|
|
|
8894
9058
|
log.plain(` cover: ${c.cyan(view.coverUrl)}`);
|
|
8895
9059
|
log.plain(` source: ${describeCoverSource(view.coverSource)}`);
|
|
8896
9060
|
}
|
|
9061
|
+
if (view.clipUrl) {
|
|
9062
|
+
log.plain(` clip: ${c.cyan(view.clipUrl)}`);
|
|
9063
|
+
log.plain(` source: ${describeClipSource(view.clipSource)}`);
|
|
9064
|
+
}
|
|
8897
9065
|
if (!view.coverUrl || coverIsStandIn(view.coverSource)) {
|
|
8898
9066
|
log.plain("");
|
|
8899
9067
|
log.dim(` A cover is ${WHAT_A_COVER_IS}.`);
|
|
@@ -27463,6 +27631,9 @@ ${c.bold("Usage")}
|
|
|
27463
27631
|
on the dashboard is never replaced from here.
|
|
27464
27632
|
'genex preview' also uploads a new
|
|
27465
27633
|
.genex/scratch/cover.png by itself.
|
|
27634
|
+
genex cover --clip <file> --commit <sha>
|
|
27635
|
+
Make a 5-6 s recording of real play (MP4/WebM,
|
|
27636
|
+
\u22648 MB) of build <sha> the game's cover clip.
|
|
27466
27637
|
genex make-remixable [options] Make THIS game remixable by everyone \u2014 migrates a
|
|
27467
27638
|
private source onto a public managed genex repo.
|
|
27468
27639
|
genex domain <sub> [host] Play this game on a domain you own:
|
|
@@ -27784,7 +27955,10 @@ ${c.bold("Options for `list`")}
|
|
|
27784
27955
|
|
|
27785
27956
|
${c.bold("Options for `cover`")}
|
|
27786
27957
|
<file> The image to make the cover (positional). Omit it to see
|
|
27787
|
-
the current cover and where
|
|
27958
|
+
the current cover and clip, and where they came from.
|
|
27959
|
+
--clip <file> A recording of real play (MP4 or WebM, \u22648 MB) to make the
|
|
27960
|
+
cover clip instead; the server keeps 5 s of it.
|
|
27961
|
+
--commit <sha> With --clip: the build the recording shows (7-40 hex).
|
|
27788
27962
|
--json One machine-readable result object.
|
|
27789
27963
|
--api-url <url> Override the API base URL.
|
|
27790
27964
|
--env <path> Token env file (default: ~/.genex/env).
|
|
@@ -27854,6 +28028,7 @@ ${c.bold("Examples")}
|
|
|
27854
28028
|
genex publish --categories games,vfx
|
|
27855
28029
|
genex cover
|
|
27856
28030
|
genex cover .genex/scratch/cover.png
|
|
28031
|
+
genex cover --clip .genex/scratch/clip.mp4 --commit 3f9c2ab
|
|
27857
28032
|
genex make-remixable
|
|
27858
28033
|
genex domain add play.mygame.com
|
|
27859
28034
|
genex domain list
|
|
@@ -28402,7 +28577,10 @@ function parseArgs(argv) {
|
|
|
28402
28577
|
"--tri-band",
|
|
28403
28578
|
"--holder",
|
|
28404
28579
|
"--summary",
|
|
28405
|
-
"--tags"
|
|
28580
|
+
"--tags",
|
|
28581
|
+
// `genex cover --clip <file> --commit <sha>`: the clip and the build it shows.
|
|
28582
|
+
"--clip",
|
|
28583
|
+
"--commit"
|
|
28406
28584
|
]);
|
|
28407
28585
|
let i = 0;
|
|
28408
28586
|
while (i < argv.length) {
|
|
@@ -29087,6 +29265,12 @@ function applyValueFlag(options, flag, value) {
|
|
|
29087
29265
|
case "--src":
|
|
29088
29266
|
options.src = value;
|
|
29089
29267
|
break;
|
|
29268
|
+
case "--clip":
|
|
29269
|
+
options.clipPath = value;
|
|
29270
|
+
break;
|
|
29271
|
+
case "--commit":
|
|
29272
|
+
options.commit = value;
|
|
29273
|
+
break;
|
|
29090
29274
|
}
|
|
29091
29275
|
}
|
|
29092
29276
|
async function main() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genex-ai/cli-demo",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.8-dev.910",
|
|
4
4
|
"description": "Set up your project's agent workspace (.claude/.codex/.cursor in the game folder), authorize, create a game project, generate AI assets, and publish (genex CLI).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -351,6 +351,9 @@ must not be there.
|
|
|
351
351
|
`npx genex cover .genex/scratch/cover.png` sends it now: exit 0 for set or
|
|
352
352
|
outranked, 1 otherwise; `--json` prints one object whose `kind` is
|
|
353
353
|
`applied`, `outranked`, `rejected`, `invalid` or `failed`.
|
|
354
|
+
A cover clip (5–6 s of real play, MP4 or WebM, at most 8 MB) goes up with
|
|
355
|
+
`npx genex cover --clip <file> --commit <sha of the build it shows>`, with
|
|
356
|
+
the same exit codes and kinds; the owner's own clip always outranks it.
|
|
354
357
|
4. **No browser that can capture the game?** Do not set one up just for this;
|
|
355
358
|
tell the owner they can set a cover on the game's page.
|
|
356
359
|
|