@genex-ai/cli-demo 1.36.19-dev.1076 → 1.38.0-dev.1079
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
|
@@ -3123,15 +3123,15 @@ async function detectSurfaceScan(cwd = process.cwd()) {
|
|
|
3123
3123
|
detail: `repeat.set(${m[1]}, ${m[2]})`
|
|
3124
3124
|
});
|
|
3125
3125
|
}
|
|
3126
|
-
for (const
|
|
3127
|
-
if (/\b(map|alphaMap)\s*:/.test(
|
|
3126
|
+
for (const call3 of ctorArgs(content, "PointsMaterial")) {
|
|
3127
|
+
if (/\b(map|alphaMap)\s*:/.test(call3.text)) continue;
|
|
3128
3128
|
found.squarePoints.push({
|
|
3129
|
-
where: `${rel}:${lineOf(content,
|
|
3129
|
+
where: `${rel}:${lineOf(content, call3.index)}`,
|
|
3130
3130
|
detail: "PointsMaterial with no map/alphaMap"
|
|
3131
3131
|
});
|
|
3132
3132
|
}
|
|
3133
|
-
for (const
|
|
3134
|
-
const args = splitArgs(
|
|
3133
|
+
for (const call3 of ctorArgs(content, "PerspectiveCamera")) {
|
|
3134
|
+
const args = splitArgs(call3.text);
|
|
3135
3135
|
if (args.length < 4) continue;
|
|
3136
3136
|
const near = Number(args[2]);
|
|
3137
3137
|
const far = Number(args[3]);
|
|
@@ -3139,7 +3139,7 @@ async function detectSurfaceScan(cwd = process.cwd()) {
|
|
|
3139
3139
|
const ratio = far / near;
|
|
3140
3140
|
if (ratio <= DEPTH_RATIO_LIMIT) continue;
|
|
3141
3141
|
found.depthRange.push({
|
|
3142
|
-
where: `${rel}:${lineOf(content,
|
|
3142
|
+
where: `${rel}:${lineOf(content, call3.index)}`,
|
|
3143
3143
|
detail: `near ${near} / far ${far}`,
|
|
3144
3144
|
ratio
|
|
3145
3145
|
});
|
|
@@ -3434,7 +3434,7 @@ async function detectShellApis(cwd = process.cwd()) {
|
|
|
3434
3434
|
for (const m of content.matchAll(DOWNLOAD_ASSIGN)) add(found.download, m.index, "a.download");
|
|
3435
3435
|
}
|
|
3436
3436
|
for (const m of content.matchAll(DOWNLOAD_ATTR)) add(found.download, m.index, "a download link");
|
|
3437
|
-
for (const
|
|
3437
|
+
for (const call3 of dialogCalls(content)) add(found.dialogs, call3.index, `${call3.name}()`);
|
|
3438
3438
|
for (const m of content.matchAll(SERVICE_WORKER)) {
|
|
3439
3439
|
add(found.serviceWorker, m.index, "serviceWorker.register");
|
|
3440
3440
|
}
|
|
@@ -8658,10 +8658,10 @@ var PACKAGE_PART = "package";
|
|
|
8658
8658
|
function packagePart(zip) {
|
|
8659
8659
|
return { kind: PACKAGE_PART, bytes: zip, contentType: MediaContentType.Zip };
|
|
8660
8660
|
}
|
|
8661
|
-
async function uploadItemPart(
|
|
8661
|
+
async function uploadItemPart(call3, skuUrl2, part) {
|
|
8662
8662
|
const isPackage = part.kind === PACKAGE_PART;
|
|
8663
8663
|
const base = isPackage ? `${skuUrl2}/package` : `${skuUrl2}/media`;
|
|
8664
|
-
const post = (url, body) =>
|
|
8664
|
+
const post = (url, body) => call3(url, { method: "POST", body: JSON.stringify(body) });
|
|
8665
8665
|
const urlRes = await post(`${base}/upload-url`, isPackage ? { bytes: part.bytes.length } : { kind: part.kind, bytes: part.bytes.length, contentType: part.contentType });
|
|
8666
8666
|
if (!urlRes.ok) return { ok: false, refused: urlRes, what: `upload the ${part.kind}` };
|
|
8667
8667
|
const { uploadUrl, key } = await urlRes.json();
|
|
@@ -8680,19 +8680,19 @@ function bearerCaller(token) {
|
|
|
8680
8680
|
const auth = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
|
|
8681
8681
|
return (url, init) => apiFetch(url, { ...init, headers: { ...auth, ...init?.headers ?? {} } });
|
|
8682
8682
|
}
|
|
8683
|
-
async function readPriceGrid(
|
|
8684
|
-
const res = await
|
|
8683
|
+
async function readPriceGrid(call3, apiUrl, projectId) {
|
|
8684
|
+
const res = await call3(`${apiUrl}/api/coin/projects/${encodeURIComponent(projectId)}/skus`).catch(() => null);
|
|
8685
8685
|
if (!res?.ok) return null;
|
|
8686
8686
|
const body = await res.json().catch(() => null);
|
|
8687
8687
|
const grid = body?.priceGrid;
|
|
8688
8688
|
return Array.isArray(grid) && grid.every((n) => typeof n === "number") ? grid : null;
|
|
8689
8689
|
}
|
|
8690
|
-
async function storeTakesFree(
|
|
8691
|
-
const res = await
|
|
8690
|
+
async function storeTakesFree(call3, apiUrl) {
|
|
8691
|
+
const res = await call3(`${apiUrl}/api/store/catalog?limit=1`).catch(() => null);
|
|
8692
8692
|
return res?.ok === true;
|
|
8693
8693
|
}
|
|
8694
|
-
async function priceGridHint(
|
|
8695
|
-
const [grid, free] = await Promise.all([readPriceGrid(
|
|
8694
|
+
async function priceGridHint(call3, apiUrl, projectId) {
|
|
8695
|
+
const [grid, free] = await Promise.all([readPriceGrid(call3, apiUrl, projectId), storeTakesFree(call3, apiUrl)]);
|
|
8696
8696
|
return grid ? priceGridLine(grid, free) : null;
|
|
8697
8697
|
}
|
|
8698
8698
|
async function openLinkedGame(opts, log, rerun) {
|
|
@@ -9892,14 +9892,14 @@ async function runDomain(opts) {
|
|
|
9892
9892
|
}
|
|
9893
9893
|
const base = `${apiUrl}/api/projects/${encodeURIComponent(meta.id)}/domains`;
|
|
9894
9894
|
const auth = { Authorization: `Bearer ${token}` };
|
|
9895
|
-
const
|
|
9895
|
+
const call3 = (url, init) => apiFetch(url, { ...init, headers: { ...auth, "Content-Type": "application/json", ...init?.headers ?? {} } });
|
|
9896
9896
|
let res;
|
|
9897
9897
|
try {
|
|
9898
|
-
if (sub === "list") res = await
|
|
9899
|
-
else if (sub === "add") res = await
|
|
9898
|
+
if (sub === "list") res = await call3(base);
|
|
9899
|
+
else if (sub === "add") res = await call3(base, { method: "POST", body: JSON.stringify({ hostname }) });
|
|
9900
9900
|
else if (sub === "verify")
|
|
9901
|
-
res = await
|
|
9902
|
-
else res = await
|
|
9901
|
+
res = await call3(`${base}/${encodeURIComponent(hostname)}/verify`, { method: "POST" });
|
|
9902
|
+
else res = await call3(`${base}/${encodeURIComponent(hostname)}`, { method: "DELETE" });
|
|
9903
9903
|
} catch (err) {
|
|
9904
9904
|
log.error(`Couldn't reach the API at ${apiUrl}.`);
|
|
9905
9905
|
log.dim(` ${String(err)}`);
|
|
@@ -10853,13 +10853,77 @@ async function ensureRealDir(dir, label) {
|
|
|
10853
10853
|
}
|
|
10854
10854
|
}
|
|
10855
10855
|
|
|
10856
|
+
// src/lib/web-version.ts
|
|
10857
|
+
var NATIVE_PAGE_WOULD_BE_REPLACED = "native_page_would_be_replaced";
|
|
10858
|
+
function isNativePageRefusal(body) {
|
|
10859
|
+
return body?.error === NATIVE_PAGE_WOULD_BE_REPLACED || body?.code === NATIVE_PAGE_WOULD_BE_REPLACED;
|
|
10860
|
+
}
|
|
10861
|
+
function explainNativePageRefusal(log, command, uploaded = false) {
|
|
10862
|
+
log.error("This game's page currently shows its Mac/Windows/Linux app \u2014 nothing was changed.");
|
|
10863
|
+
log.dim(" Making this web build live would replace that page: visitors would get this web");
|
|
10864
|
+
log.dim(" build instead of the app.");
|
|
10865
|
+
if (uploaded) log.dim(" The build was uploaded to the preview channel only; players never see it there.");
|
|
10866
|
+
log.dim(" Re-run with --replace-native-page ONLY if this folder really is the game's web version:");
|
|
10867
|
+
log.dim(` npx genex ${command} --replace-native-page`);
|
|
10868
|
+
}
|
|
10869
|
+
var REFUSALS = {
|
|
10870
|
+
no_native_build: [
|
|
10871
|
+
"This game has no Mac, Windows or Linux build players get, so removing its web version would leave nothing to play \u2014 nothing was changed.",
|
|
10872
|
+
" A native build waiting for review does not count yet.",
|
|
10873
|
+
" To take the game out of the gallery, unpublish it; to remove it entirely, delete it from the dashboard."
|
|
10874
|
+
],
|
|
10875
|
+
game_taken_down: [
|
|
10876
|
+
"This game was taken down by moderation; its build is kept for the review \u2014 nothing was changed."
|
|
10877
|
+
],
|
|
10878
|
+
web_removal_incomplete: [
|
|
10879
|
+
"The removal stopped partway: the web build no longer serves, but it is not fully gone yet.",
|
|
10880
|
+
" Run `npx genex web remove --yes` again to finish."
|
|
10881
|
+
],
|
|
10882
|
+
release_conflict: ["Another release of this game is in progress \u2014 nothing was changed. Try again in a moment."]
|
|
10883
|
+
};
|
|
10884
|
+
async function removeWebVersion(ctx, log) {
|
|
10885
|
+
let res;
|
|
10886
|
+
try {
|
|
10887
|
+
res = await apiFetch(`${ctx.apiUrl}/api/games/${ctx.projectId}/web`, {
|
|
10888
|
+
method: "DELETE",
|
|
10889
|
+
headers: { Authorization: `Bearer ${ctx.token}` }
|
|
10890
|
+
});
|
|
10891
|
+
} catch (err) {
|
|
10892
|
+
log.error(`Couldn't reach the API at ${ctx.apiUrl}: ${String(err)}`);
|
|
10893
|
+
return null;
|
|
10894
|
+
}
|
|
10895
|
+
const body = await res.json().catch(() => null);
|
|
10896
|
+
if (res.ok) return { removed: body?.removed === true, web: false };
|
|
10897
|
+
const words2 = body?.error ? REFUSALS[body.error] : void 0;
|
|
10898
|
+
if (words2 && body?.error) {
|
|
10899
|
+
const [first, ...rest] = words2;
|
|
10900
|
+
if (first) log.error(first);
|
|
10901
|
+
for (const line of rest) log.dim(line);
|
|
10902
|
+
return { refused: body.error };
|
|
10903
|
+
}
|
|
10904
|
+
if (res.status === 401) {
|
|
10905
|
+
log.error("Not authorized (HTTP 401) \u2014 your sign-in expired or was revoked. Run `genex link <your-game>`.");
|
|
10906
|
+
return null;
|
|
10907
|
+
}
|
|
10908
|
+
if (res.status === 404) {
|
|
10909
|
+
log.error("That game wasn't found on your account (HTTP 404), or this Genex server can't remove a web version yet.");
|
|
10910
|
+
return null;
|
|
10911
|
+
}
|
|
10912
|
+
log.error(`Couldn't remove the web version (HTTP ${res.status})${body?.error ? `: ${body.error}` : ""}.`);
|
|
10913
|
+
return null;
|
|
10914
|
+
}
|
|
10915
|
+
|
|
10856
10916
|
// src/lib/promote.ts
|
|
10857
|
-
async function promoteBuild(apiUrl, projectId, token, log) {
|
|
10917
|
+
async function promoteBuild(apiUrl, projectId, token, log, opts = {}) {
|
|
10858
10918
|
let res;
|
|
10859
10919
|
try {
|
|
10860
10920
|
res = await apiFetch(`${apiUrl}/api/games/${projectId}/promote`, {
|
|
10861
10921
|
method: "POST",
|
|
10862
|
-
headers: {
|
|
10922
|
+
headers: {
|
|
10923
|
+
Authorization: `Bearer ${token}`,
|
|
10924
|
+
...opts.replaceNativePage ? { "Content-Type": "application/json" } : {}
|
|
10925
|
+
},
|
|
10926
|
+
...opts.replaceNativePage ? { body: JSON.stringify({ replaceNativePage: true }) } : {}
|
|
10863
10927
|
});
|
|
10864
10928
|
} catch (err) {
|
|
10865
10929
|
log.error(`Couldn't reach the API at ${apiUrl}: ${String(err)}`);
|
|
@@ -10867,6 +10931,11 @@ async function promoteBuild(apiUrl, projectId, token, log) {
|
|
|
10867
10931
|
}
|
|
10868
10932
|
if (res.status === 409) {
|
|
10869
10933
|
const body2 = await res.json().catch(() => null);
|
|
10934
|
+
if (isNativePageRefusal(body2)) {
|
|
10935
|
+
const command = opts.command ?? "promote";
|
|
10936
|
+
explainNativePageRefusal(log, command, command === "publish");
|
|
10937
|
+
return null;
|
|
10938
|
+
}
|
|
10870
10939
|
if (body2?.code === "live_is_newer") {
|
|
10871
10940
|
log.error("The live build is newer than your preview \u2014 nothing was changed.");
|
|
10872
10941
|
log.dim(" This happens after the live version was restored, or after a later publish.");
|
|
@@ -10999,7 +11068,10 @@ async function runPublish(opts) {
|
|
|
10999
11068
|
return;
|
|
11000
11069
|
}
|
|
11001
11070
|
log.step("Making it live\u2026");
|
|
11002
|
-
if (!await promoteBuild(apiUrl, meta.id, token, log
|
|
11071
|
+
if (!await promoteBuild(apiUrl, meta.id, token, log, {
|
|
11072
|
+
replaceNativePage: opts.replaceNativePage,
|
|
11073
|
+
command: "publish"
|
|
11074
|
+
})) {
|
|
11003
11075
|
process.exitCode = 1;
|
|
11004
11076
|
return;
|
|
11005
11077
|
}
|
|
@@ -11026,6 +11098,7 @@ async function runPublish(opts) {
|
|
|
11026
11098
|
if (opts.license) body.license = opts.license;
|
|
11027
11099
|
if (opts.purchaseUrl) body.purchaseUrl = opts.purchaseUrl;
|
|
11028
11100
|
if (opts.productName) body.productName = opts.productName;
|
|
11101
|
+
if (opts.replaceNativePage) body.replaceNativePage = true;
|
|
11029
11102
|
body.matchmaking = detections.matchmaking ?? null;
|
|
11030
11103
|
body.gameStateUsed = detections.gameStateUsed;
|
|
11031
11104
|
body.mobileControls = detections.mobileControls;
|
|
@@ -11047,6 +11120,11 @@ async function runPublish(opts) {
|
|
|
11047
11120
|
const listedOnDisk = await readProject();
|
|
11048
11121
|
await writeProject({ ...listedOnDisk ?? meta, status: "published" });
|
|
11049
11122
|
const listed = await res.json().catch(() => null);
|
|
11123
|
+
if (listed?.project?.commitSha === null && listed.project.stagingCommitSha) {
|
|
11124
|
+
log.warn("Listed with its Mac/Windows/Linux app \u2014 your preview was not made live over the app's page.");
|
|
11125
|
+
log.dim(" Re-run with --replace-native-page ONLY if this folder really is the game's web version:");
|
|
11126
|
+
log.dim(" npx genex publish --no-push --replace-native-page");
|
|
11127
|
+
}
|
|
11050
11128
|
if (opts.regenerateCover && listed?.project?.coverAiEnabled === false) {
|
|
11051
11129
|
log.dim(" --regenerate-cover did nothing: this stand doesn't make AI covers.");
|
|
11052
11130
|
log.dim(" The cover is a real frame of the game \u2014 set one with `npx genex cover <file>`.");
|
|
@@ -11086,7 +11164,10 @@ async function runPromote(opts) {
|
|
|
11086
11164
|
return;
|
|
11087
11165
|
}
|
|
11088
11166
|
log.step("Making your previewed build live\u2026");
|
|
11089
|
-
const result = await promoteBuild(apiUrl, meta.id, token, log
|
|
11167
|
+
const result = await promoteBuild(apiUrl, meta.id, token, log, {
|
|
11168
|
+
replaceNativePage: opts.replaceNativePage,
|
|
11169
|
+
command: "promote"
|
|
11170
|
+
});
|
|
11090
11171
|
if (!result) {
|
|
11091
11172
|
process.exitCode = 1;
|
|
11092
11173
|
return;
|
|
@@ -23053,7 +23134,7 @@ async function runController(opts) {
|
|
|
23053
23134
|
log.plain(c.bold(`genex controller ${kind}`));
|
|
23054
23135
|
log.plain("");
|
|
23055
23136
|
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path34.sep)}`);
|
|
23056
|
-
const
|
|
23137
|
+
const plan2 = [
|
|
23057
23138
|
...set.code.map((rel) => ({ from: rel, rel: path34.join(CODE_DEST, rel) })),
|
|
23058
23139
|
...set.assets.map((rel) => ({
|
|
23059
23140
|
from: rel,
|
|
@@ -23063,7 +23144,7 @@ async function runController(opts) {
|
|
|
23063
23144
|
let copied = 0;
|
|
23064
23145
|
let skipped = 0;
|
|
23065
23146
|
try {
|
|
23066
|
-
for (const file of
|
|
23147
|
+
for (const file of plan2) {
|
|
23067
23148
|
const dest = path34.join(root, file.rel);
|
|
23068
23149
|
if (!opts.force && await exists4(dest)) {
|
|
23069
23150
|
skipped++;
|
|
@@ -24083,22 +24164,22 @@ async function context2(opts) {
|
|
|
24083
24164
|
if (!token) return null;
|
|
24084
24165
|
return { token, apiUrl };
|
|
24085
24166
|
}
|
|
24086
|
-
async function readVideo(
|
|
24167
|
+
async function readVideo(path52, log) {
|
|
24087
24168
|
let bytes;
|
|
24088
24169
|
try {
|
|
24089
|
-
bytes = await readFile(
|
|
24170
|
+
bytes = await readFile(path52);
|
|
24090
24171
|
} catch {
|
|
24091
|
-
log.error(`Can't read ${
|
|
24172
|
+
log.error(`Can't read ${path52}.`);
|
|
24092
24173
|
return null;
|
|
24093
24174
|
}
|
|
24094
24175
|
if (bytes.byteLength > MAX_VIDEO_BYTES) {
|
|
24095
|
-
log.error(`${basename(
|
|
24176
|
+
log.error(`${basename(path52)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
|
|
24096
24177
|
return null;
|
|
24097
24178
|
}
|
|
24098
24179
|
return bytes;
|
|
24099
24180
|
}
|
|
24100
|
-
async function uploadVideo(apiUrl, token, characterId,
|
|
24101
|
-
const contentType = /\.mov$/i.test(
|
|
24181
|
+
async function uploadVideo(apiUrl, token, characterId, path52, bytes, log) {
|
|
24182
|
+
const contentType = /\.mov$/i.test(path52) ? "video/quicktime" : "video/mp4";
|
|
24102
24183
|
const minted = await apiFetch(
|
|
24103
24184
|
`${apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/video-url`,
|
|
24104
24185
|
{
|
|
@@ -24113,7 +24194,7 @@ async function uploadVideo(apiUrl, token, characterId, path50, bytes, log) {
|
|
|
24113
24194
|
return null;
|
|
24114
24195
|
}
|
|
24115
24196
|
const { uploadUrl, videoUrl } = await minted.json();
|
|
24116
|
-
log.dim(` uploading ${basename(
|
|
24197
|
+
log.dim(` uploading ${basename(path52)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
|
|
24117
24198
|
const put = await fetch(uploadUrl, {
|
|
24118
24199
|
method: "PUT",
|
|
24119
24200
|
headers: { "Content-Type": contentType, "Content-Length": String(bytes.byteLength) },
|
|
@@ -24125,9 +24206,9 @@ async function uploadVideo(apiUrl, token, characterId, path50, bytes, log) {
|
|
|
24125
24206
|
}
|
|
24126
24207
|
return videoUrl;
|
|
24127
24208
|
}
|
|
24128
|
-
function printPlan(
|
|
24209
|
+
function printPlan(plan2, log, creature) {
|
|
24129
24210
|
log.plain(c.bold(creature ? "Creature animation plan" : "Animation plan"));
|
|
24130
|
-
for (const item of
|
|
24211
|
+
for (const item of plan2.items) {
|
|
24131
24212
|
const count = item.clips > 1 ? ` (${item.clips} clips)` : "";
|
|
24132
24213
|
log.plain(` \xB7 ${c.bold(item.request)}${count} \u2014 ${item.explanation}`);
|
|
24133
24214
|
if (item.supersededSlots.length > 0) {
|
|
@@ -24136,14 +24217,14 @@ function printPlan(plan, log, creature) {
|
|
|
24136
24217
|
);
|
|
24137
24218
|
}
|
|
24138
24219
|
}
|
|
24139
|
-
if (
|
|
24220
|
+
if (plan2.secondsApplied === false) {
|
|
24140
24221
|
log.plain(
|
|
24141
24222
|
` ${c.dim("--duration sets the reference-video length; none of these verbs took the video route, so it did nothing.")}`
|
|
24142
24223
|
);
|
|
24143
24224
|
}
|
|
24144
24225
|
log.plain("");
|
|
24145
|
-
if (
|
|
24146
|
-
else log.dim(` ${
|
|
24226
|
+
if (plan2.credits > 0) log.dim(` ${plan2.totalClips} clips \xB7 ${plan2.credits} credits`);
|
|
24227
|
+
else log.dim(` ${plan2.totalClips} clips`);
|
|
24147
24228
|
log.plain("");
|
|
24148
24229
|
}
|
|
24149
24230
|
async function runCharacterAnimateMotion(opts) {
|
|
@@ -26878,10 +26959,10 @@ async function reportModels(apiUrl, token, opts, log, toolsOnly = false) {
|
|
|
26878
26959
|
shown === lane.models ? ` ${lane.models.length} model${lane.models.length === 1 ? "" : "s"} live:` : ` ${shown.length} featured model${shown.length === 1 ? "" : "s"} of ${lane.models.length} live:`
|
|
26879
26960
|
);
|
|
26880
26961
|
for (const m of shown) {
|
|
26881
|
-
const
|
|
26962
|
+
const plan2 = m.personalPlan ? ` \xB7 personal plan: ${m.personalPlan}` : "";
|
|
26882
26963
|
const kind = m.kind === "typesafe" ? " \xB7 judge (classifier): judge() under a budget, not generate() or bench" : "";
|
|
26883
26964
|
log.plain(
|
|
26884
|
-
` ${c.cyan(m.id)} ${m.label} \u2014 ${usdPerMillion(m.inputUsdPerMillion)} in / ${usdPerMillion(m.outputUsdPerMillion)} out per million tokens${
|
|
26965
|
+
` ${c.cyan(m.id)} ${m.label} \u2014 ${usdPerMillion(m.inputUsdPerMillion)} in / ${usdPerMillion(m.outputUsdPerMillion)} out per million tokens${plan2}${kind}`
|
|
26885
26966
|
);
|
|
26886
26967
|
}
|
|
26887
26968
|
if (hiddenCount > 0) {
|
|
@@ -27044,7 +27125,7 @@ async function runBench(args) {
|
|
|
27044
27125
|
const samples = opts.samples ?? DEFAULT_BENCH_SAMPLES;
|
|
27045
27126
|
const prompt = opts.benchPrompt.trim();
|
|
27046
27127
|
const auth = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
|
|
27047
|
-
const
|
|
27128
|
+
const call3 = (url, init) => apiFetch(url, { ...init, headers: { ...auth, ...init?.headers ?? {} } });
|
|
27048
27129
|
let schema;
|
|
27049
27130
|
if (opts.schemaPath) {
|
|
27050
27131
|
try {
|
|
@@ -27123,7 +27204,7 @@ async function runBench(args) {
|
|
|
27123
27204
|
if (!opts.json) log.warn("Stopping \u2014 cancelling the attempt in flight.");
|
|
27124
27205
|
void (async () => {
|
|
27125
27206
|
if (target) {
|
|
27126
|
-
await
|
|
27207
|
+
await call3(`${base}/${encodeURIComponent(target)}/cancel`, { method: "POST" }).catch(() => {
|
|
27127
27208
|
});
|
|
27128
27209
|
}
|
|
27129
27210
|
process.exit(130);
|
|
@@ -27132,7 +27213,7 @@ async function runBench(args) {
|
|
|
27132
27213
|
process.once("SIGINT", onSigint);
|
|
27133
27214
|
try {
|
|
27134
27215
|
for (let i = 0; i < samples; i++) {
|
|
27135
|
-
const res = await
|
|
27216
|
+
const res = await call3(base, {
|
|
27136
27217
|
method: "POST",
|
|
27137
27218
|
body: JSON.stringify({
|
|
27138
27219
|
maxCredits,
|
|
@@ -27146,7 +27227,7 @@ async function runBench(args) {
|
|
|
27146
27227
|
})
|
|
27147
27228
|
});
|
|
27148
27229
|
if (!res.ok) {
|
|
27149
|
-
const verdict = await explainBenchRefusal(res,
|
|
27230
|
+
const verdict = await explainBenchRefusal(res, call3, base, log, opts, i, results.length);
|
|
27150
27231
|
if (verdict.refusal) refusal2 = verdict.refusal;
|
|
27151
27232
|
if (verdict.stop) break;
|
|
27152
27233
|
continue;
|
|
@@ -27157,7 +27238,7 @@ async function runBench(args) {
|
|
|
27157
27238
|
continue;
|
|
27158
27239
|
}
|
|
27159
27240
|
inflight = started.id;
|
|
27160
|
-
const settled = await pollSettled(
|
|
27241
|
+
const settled = await pollSettled(call3, `${base}/${encodeURIComponent(started.id)}`, opts.timeoutSec ?? DEFAULT_SAMPLE_TIMEOUT_SEC);
|
|
27161
27242
|
inflight = null;
|
|
27162
27243
|
const row = sampleFrom(started.id, settled);
|
|
27163
27244
|
results.push(row);
|
|
@@ -27360,7 +27441,7 @@ function printAverage(log, record, attempts) {
|
|
|
27360
27441
|
log.dim(` = that average rounded up to a whole credit. disclosure.estimatedCreditsPerPeriod = your calls per period \xD7 ${formatCredits(record.averageCreditsPerCall)}, rounded up.`);
|
|
27361
27442
|
}
|
|
27362
27443
|
}
|
|
27363
|
-
async function explainBenchRefusal(res,
|
|
27444
|
+
async function explainBenchRefusal(res, call3, base, log, opts, index, settledSoFar) {
|
|
27364
27445
|
if (printedStructuredError(res)) {
|
|
27365
27446
|
const printed = await res.json().catch(() => ({}));
|
|
27366
27447
|
return { stop: true, refusal: { status: res.status, error: typeof printed.error === "string" ? printed.error : null, slotsHeld: null, slotLimit: null } };
|
|
@@ -27368,7 +27449,7 @@ async function explainBenchRefusal(res, call2, base, log, opts, index, settledSo
|
|
|
27368
27449
|
const body = await res.json().catch(() => ({}));
|
|
27369
27450
|
const refusal2 = { status: res.status, error: body.error ?? null, slotsHeld: null, slotLimit: null };
|
|
27370
27451
|
if (res.status === 429 && body.error === "generation_limit") {
|
|
27371
|
-
const counts = serverSlotCounts(body) ?? await readSlotCounts(
|
|
27452
|
+
const counts = serverSlotCounts(body) ?? await readSlotCounts(call3, base);
|
|
27372
27453
|
refusal2.slotsHeld = counts?.held ?? null;
|
|
27373
27454
|
refusal2.slotLimit = counts?.limit ?? null;
|
|
27374
27455
|
if (!opts.json) log.error(` Sample ${index + 1} ${benchSlotsHeldSentence(counts)}`);
|
|
@@ -27409,17 +27490,17 @@ function printProviderRefusal(log, status2, message, awaitingBill = false) {
|
|
|
27409
27490
|
log.dim(` A ${status2} here is this stand's provider configuration refusing the model \u2014 a message for the operator, not something to fix in the game.`);
|
|
27410
27491
|
}
|
|
27411
27492
|
}
|
|
27412
|
-
async function readSlotCounts(
|
|
27413
|
-
const res = await
|
|
27493
|
+
async function readSlotCounts(call3, base) {
|
|
27494
|
+
const res = await call3(`${base}?scope=open`).catch(() => null);
|
|
27414
27495
|
if (!res?.ok) return null;
|
|
27415
27496
|
const body = await res.json().catch(() => null);
|
|
27416
27497
|
return serverSlotCounts(body);
|
|
27417
27498
|
}
|
|
27418
|
-
async function pollSettled(
|
|
27499
|
+
async function pollSettled(call3, url, timeoutSec) {
|
|
27419
27500
|
const deadline = Date.now() + timeoutSec * 1e3;
|
|
27420
27501
|
let last = null;
|
|
27421
27502
|
while (Date.now() < deadline) {
|
|
27422
|
-
const res = await
|
|
27503
|
+
const res = await call3(url).catch(() => null);
|
|
27423
27504
|
if (res?.ok) {
|
|
27424
27505
|
last = await res.json().catch(() => null);
|
|
27425
27506
|
if (last?.billingStatus === "final") return last;
|
|
@@ -27482,9 +27563,9 @@ function emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples, maxCre
|
|
|
27482
27563
|
}
|
|
27483
27564
|
async function reportStatus(args) {
|
|
27484
27565
|
const { apiUrl, token, projectId, opts, log } = args;
|
|
27485
|
-
const
|
|
27566
|
+
const call3 = (url, init) => apiFetch(url, { ...init, headers: { Authorization: `Bearer ${token}`, ...init?.headers ?? {} } });
|
|
27486
27567
|
const base = `${apiUrl}/api/runtime/development/projects/${encodeURIComponent(projectId)}/generations`;
|
|
27487
|
-
const res = await
|
|
27568
|
+
const res = await call3(`${base}?scope=open`).catch(() => null);
|
|
27488
27569
|
if (!res || !res.ok) {
|
|
27489
27570
|
if (res && printedStructuredError(res)) {
|
|
27490
27571
|
if (opts.json) writeJsonLine(statusJson(apiUrl, projectId, "failed", `http_${res.status}`, [], null, null));
|
|
@@ -27575,14 +27656,14 @@ function statusJson(apiUrl, projectId, status2, error, rows, slotsHeld, slotLimi
|
|
|
27575
27656
|
}
|
|
27576
27657
|
async function cancelCall(args) {
|
|
27577
27658
|
const { apiUrl, token, projectId, id, opts, log } = args;
|
|
27578
|
-
const
|
|
27659
|
+
const call3 = (url, init) => apiFetch(url, { ...init, headers: { Authorization: `Bearer ${token}`, ...init?.headers ?? {} } });
|
|
27579
27660
|
const base = `${apiUrl}/api/runtime/development/projects/${encodeURIComponent(projectId)}/generations`;
|
|
27580
27661
|
const rowUrl = `${base}/${encodeURIComponent(id)}`;
|
|
27581
27662
|
const done = (status2, error, reason, row2) => {
|
|
27582
27663
|
if (opts.json) writeJsonLine({ command: "llm cancel", status: status2, error, id, cancelled: reason === "cancelled", reason, generation: row2 });
|
|
27583
27664
|
if (status2 === "failed") process.exitCode = 1;
|
|
27584
27665
|
};
|
|
27585
|
-
const read = await
|
|
27666
|
+
const read = await call3(rowUrl).catch(() => null);
|
|
27586
27667
|
if (!read || !read.ok) {
|
|
27587
27668
|
if (read && printedStructuredError(read)) return done("failed", `http_${read.status}`, null, null);
|
|
27588
27669
|
const body = read ? await read.json().catch(() => ({})) : {};
|
|
@@ -27621,7 +27702,7 @@ async function cancelCall(args) {
|
|
|
27621
27702
|
}
|
|
27622
27703
|
return done("ok", null, state === "awaiting bill" ? "awaiting_bill" : "already_final", row);
|
|
27623
27704
|
}
|
|
27624
|
-
const res = await
|
|
27705
|
+
const res = await call3(`${rowUrl}/cancel`, { method: "POST" }).catch(() => null);
|
|
27625
27706
|
if (!res || !res.ok) {
|
|
27626
27707
|
if (res && printedStructuredError(res)) return done("failed", `http_${res.status}`, null, row);
|
|
27627
27708
|
const body = res ? await res.json().catch(() => ({})) : {};
|
|
@@ -28577,14 +28658,14 @@ var PlayerClient = class {
|
|
|
28577
28658
|
this.version = options.version ?? getCliVersion();
|
|
28578
28659
|
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
28579
28660
|
}
|
|
28580
|
-
async call(
|
|
28661
|
+
async call(path52, init = {}, provider) {
|
|
28581
28662
|
const headers = {
|
|
28582
28663
|
Authorization: `Bearer ${this.token}`,
|
|
28583
28664
|
[PLAYER_VERSION_HEADER]: this.version,
|
|
28584
28665
|
...provider ? { [EXTERNAL_PROVIDER_HEADER]: provider } : {},
|
|
28585
28666
|
...init.body === void 0 ? {} : { "Content-Type": "application/json" }
|
|
28586
28667
|
};
|
|
28587
|
-
const res = await apiFetch(`${this.apiUrl}${
|
|
28668
|
+
const res = await apiFetch(`${this.apiUrl}${path52}`, {
|
|
28588
28669
|
...init,
|
|
28589
28670
|
headers,
|
|
28590
28671
|
signal: AbortSignal.timeout(this.timeoutMs)
|
|
@@ -29180,20 +29261,20 @@ async function markRelaunch(version, home) {
|
|
|
29180
29261
|
}
|
|
29181
29262
|
}
|
|
29182
29263
|
async function relaunchWatcher(input) {
|
|
29183
|
-
const
|
|
29264
|
+
const plan2 = relaunchPlan(input);
|
|
29184
29265
|
const log = input.log ?? (() => {
|
|
29185
29266
|
});
|
|
29186
|
-
if (input.relaunched) return { launched: false, plan, reason: "relaunched_child" };
|
|
29187
|
-
if (!relaunchPossible()) return { launched: false, plan, reason: "no_npx" };
|
|
29188
|
-
if (await relaunchOnCooldown(input.home)) return { launched: false, plan, reason: "cooldown" };
|
|
29267
|
+
if (input.relaunched) return { launched: false, plan: plan2, reason: "relaunched_child" };
|
|
29268
|
+
if (!relaunchPossible()) return { launched: false, plan: plan2, reason: "no_npx" };
|
|
29269
|
+
if (await relaunchOnCooldown(input.home)) return { launched: false, plan: plan2, reason: "cooldown" };
|
|
29189
29270
|
await markRelaunch(input.version, input.home);
|
|
29190
29271
|
if (supervisedWatcher()) {
|
|
29191
29272
|
log(`player: stopping so the login item restarts as ${input.version} (it resolves ${PLAYER_AUTOSTART_SPEC} each start)`);
|
|
29192
|
-
return { launched: false, plan, reason: "supervisor_restarts" };
|
|
29273
|
+
return { launched: false, plan: plan2, reason: "supervisor_restarts" };
|
|
29193
29274
|
}
|
|
29194
29275
|
if (playerDryRun()) {
|
|
29195
|
-
log(`player: would relaunch: ${JSON.stringify([
|
|
29196
|
-
return { launched: true, plan, reason: "dry_run" };
|
|
29276
|
+
log(`player: would relaunch: ${JSON.stringify([plan2.launcher, ...plan2.args])}`);
|
|
29277
|
+
return { launched: true, plan: plan2, reason: "dry_run" };
|
|
29197
29278
|
}
|
|
29198
29279
|
let out;
|
|
29199
29280
|
let err;
|
|
@@ -29205,7 +29286,7 @@ async function relaunchWatcher(input) {
|
|
|
29205
29286
|
} catch {
|
|
29206
29287
|
}
|
|
29207
29288
|
try {
|
|
29208
|
-
const child = spawn7(
|
|
29289
|
+
const child = spawn7(plan2.launcher, plan2.args, {
|
|
29209
29290
|
detached: true,
|
|
29210
29291
|
stdio: ["ignore", out ?? "ignore", err ?? "ignore"]
|
|
29211
29292
|
});
|
|
@@ -29218,7 +29299,7 @@ async function relaunchWatcher(input) {
|
|
|
29218
29299
|
}
|
|
29219
29300
|
}
|
|
29220
29301
|
log(`player: relaunching as ${input.version}`);
|
|
29221
|
-
return { launched: true, plan };
|
|
29302
|
+
return { launched: true, plan: plan2 };
|
|
29222
29303
|
}
|
|
29223
29304
|
|
|
29224
29305
|
// src/lib/player/loop.ts
|
|
@@ -29908,6 +29989,11 @@ function writeJsonLine2(value) {
|
|
|
29908
29989
|
`);
|
|
29909
29990
|
}
|
|
29910
29991
|
|
|
29992
|
+
// src/commands/native.ts
|
|
29993
|
+
import fs51 from "fs/promises";
|
|
29994
|
+
import os15 from "os";
|
|
29995
|
+
import path51 from "path";
|
|
29996
|
+
|
|
29911
29997
|
// src/lib/native-build.ts
|
|
29912
29998
|
import { createHash as createHash5 } from "crypto";
|
|
29913
29999
|
import { createReadStream } from "fs";
|
|
@@ -30514,6 +30600,380 @@ async function listInGallery(ctx, body, log) {
|
|
|
30514
30600
|
return null;
|
|
30515
30601
|
}
|
|
30516
30602
|
|
|
30603
|
+
// src/lib/cloud-build.ts
|
|
30604
|
+
import fs49 from "fs/promises";
|
|
30605
|
+
var CLOUD_BUILD_PLATFORMS = ["windows", "linux", "mac"];
|
|
30606
|
+
var isCloudBuildPlatform = (value) => CLOUD_BUILD_PLATFORMS.includes(value);
|
|
30607
|
+
var CLOUD_BUILD_JOB_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
30608
|
+
var UPLOAD_INCOMPLETE = "cloud_build_upload_incomplete";
|
|
30609
|
+
var PART_ATTEMPTS = 3;
|
|
30610
|
+
var PART_BACKOFF_MS = [500, 2e3];
|
|
30611
|
+
var PART_CONCURRENCY2 = 2;
|
|
30612
|
+
async function call2(ctx, route, init, log, silent) {
|
|
30613
|
+
let res;
|
|
30614
|
+
try {
|
|
30615
|
+
res = await apiFetch(`${ctx.apiUrl}/api/games/${ctx.projectId}${route}`, {
|
|
30616
|
+
...init,
|
|
30617
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${ctx.token}`, ...init.headers ?? {} }
|
|
30618
|
+
});
|
|
30619
|
+
} catch (err) {
|
|
30620
|
+
log.error(`Couldn't reach the API at ${ctx.apiUrl}: ${String(err)}`);
|
|
30621
|
+
return { ok: false, refusal: { status: 0, body: null } };
|
|
30622
|
+
}
|
|
30623
|
+
const body = await res.clone().json().catch(() => null);
|
|
30624
|
+
if (res.ok && body) return { ok: true, value: body, status: res.status };
|
|
30625
|
+
const error = typeof body?.error === "string" ? body.error : void 0;
|
|
30626
|
+
const refusal2 = { status: res.status, body: body && typeof body === "object" ? body : null, ...error ? { error } : {} };
|
|
30627
|
+
if (res.ok) {
|
|
30628
|
+
log.error(`The API answered HTTP ${res.status} with no build.`);
|
|
30629
|
+
return { ok: false, refusal: refusal2 };
|
|
30630
|
+
}
|
|
30631
|
+
if (silent !== void 0 && error === silent) return { ok: false, refusal: refusal2 };
|
|
30632
|
+
if (res.status === 401) {
|
|
30633
|
+
log.error("Not authorized (HTTP 401) \u2014 your sign-in expired or was revoked.");
|
|
30634
|
+
log.plain(" Fix: run `genex link <your-game>` and sign in as the game's owner, then re-run this command.");
|
|
30635
|
+
} else if (res.status === 404) {
|
|
30636
|
+
log.error(
|
|
30637
|
+
route.includes("/cloud-builds/") ? "No such build for this game (HTTP 404)." : "This account doesn't own this game, or Build on Genex is not on for this stand (HTTP 404)."
|
|
30638
|
+
);
|
|
30639
|
+
} else if (res.status === 429 && error === "rate_limited") {
|
|
30640
|
+
log.error(`Rate limited starting a build (HTTP 429).${retryAfterHint(res)}`);
|
|
30641
|
+
} else if (error === "cloud_build_unavailable") {
|
|
30642
|
+
log.error("Build on Genex isn't available on this stand right now (HTTP 503). Try again later.");
|
|
30643
|
+
} else {
|
|
30644
|
+
const reason = await serverReason(res);
|
|
30645
|
+
log.error(reason ?? `The API refused the build (HTTP ${res.status}).`);
|
|
30646
|
+
}
|
|
30647
|
+
return { ok: false, refusal: refusal2 };
|
|
30648
|
+
}
|
|
30649
|
+
async function startCloudBuild(ctx, body, log) {
|
|
30650
|
+
const answer = await call2(ctx, "/cloud-builds", { method: "POST", body: JSON.stringify(body) }, log);
|
|
30651
|
+
if (!answer.ok) return answer;
|
|
30652
|
+
const v = answer.value;
|
|
30653
|
+
if (!v.build) {
|
|
30654
|
+
log.error(`The API answered HTTP ${answer.status} with no build.`);
|
|
30655
|
+
return { ok: false, refusal: { status: answer.status, body: answer.value } };
|
|
30656
|
+
}
|
|
30657
|
+
return {
|
|
30658
|
+
ok: true,
|
|
30659
|
+
status: answer.status,
|
|
30660
|
+
value: {
|
|
30661
|
+
status: answer.status,
|
|
30662
|
+
build: v.build,
|
|
30663
|
+
...v.upload ? { upload: v.upload } : {},
|
|
30664
|
+
...v.deduplicated ? { deduplicated: true } : {}
|
|
30665
|
+
}
|
|
30666
|
+
};
|
|
30667
|
+
}
|
|
30668
|
+
async function completeCloudBuildSource(ctx, jobId, parts, log) {
|
|
30669
|
+
const route = `/cloud-builds/${encodeURIComponent(jobId)}/source`;
|
|
30670
|
+
const answer = await call2(ctx, route, { method: "POST", body: JSON.stringify({ parts }) }, log, UPLOAD_INCOMPLETE);
|
|
30671
|
+
if (!answer.ok) return answer;
|
|
30672
|
+
const build = answer.value.build;
|
|
30673
|
+
if (!build) {
|
|
30674
|
+
log.error(`The API answered HTTP ${answer.status} with no build.`);
|
|
30675
|
+
return { ok: false, refusal: { status: answer.status, body: answer.value } };
|
|
30676
|
+
}
|
|
30677
|
+
return { ok: true, status: answer.status, value: build };
|
|
30678
|
+
}
|
|
30679
|
+
async function readCloudBuild(ctx, jobId, log) {
|
|
30680
|
+
const answer = await call2(ctx, `/cloud-builds/${encodeURIComponent(jobId)}`, { method: "GET" }, log);
|
|
30681
|
+
if (!answer.ok) return answer;
|
|
30682
|
+
const build = answer.value.build;
|
|
30683
|
+
if (!build) {
|
|
30684
|
+
log.error(`The API answered HTTP ${answer.status} with no build.`);
|
|
30685
|
+
return { ok: false, refusal: { status: answer.status, body: answer.value } };
|
|
30686
|
+
}
|
|
30687
|
+
return { ok: true, status: answer.status, value: build };
|
|
30688
|
+
}
|
|
30689
|
+
async function readPart2(file, position, length) {
|
|
30690
|
+
const buffer = Buffer.alloc(length);
|
|
30691
|
+
const { bytesRead } = await file.read(buffer, 0, length, position);
|
|
30692
|
+
if (bytesRead !== length) throw new Error("the project zip changed while it was being uploaded");
|
|
30693
|
+
return buffer;
|
|
30694
|
+
}
|
|
30695
|
+
async function putPart(url, bytes, sleep7) {
|
|
30696
|
+
for (let attempt = 0; attempt < PART_ATTEMPTS; attempt++) {
|
|
30697
|
+
let res = null;
|
|
30698
|
+
try {
|
|
30699
|
+
res = await fetch(url, { method: "PUT", body: bytes });
|
|
30700
|
+
} catch {
|
|
30701
|
+
res = null;
|
|
30702
|
+
}
|
|
30703
|
+
if (res?.ok) {
|
|
30704
|
+
const etag = res.headers.get("etag");
|
|
30705
|
+
await res.arrayBuffer().catch(() => void 0);
|
|
30706
|
+
if (etag) return etag;
|
|
30707
|
+
} else if (res && res.status < 500 && res.status !== 408 && res.status !== 429) {
|
|
30708
|
+
return null;
|
|
30709
|
+
}
|
|
30710
|
+
if (attempt < PART_ATTEMPTS - 1) await sleep7(PART_BACKOFF_MS[attempt] ?? 2e3);
|
|
30711
|
+
}
|
|
30712
|
+
return null;
|
|
30713
|
+
}
|
|
30714
|
+
async function uploadCloudBuildSource(upload, zipPath, sizeBytes, log, onProgress, sleep7 = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
30715
|
+
const parts = [...upload.parts].sort((a, b) => a.partNumber - b.partNumber);
|
|
30716
|
+
const expected = Math.ceil(sizeBytes / upload.partBytes);
|
|
30717
|
+
if (parts.length !== expected || parts.some((p, i) => p.partNumber !== i + 1)) {
|
|
30718
|
+
log.error(`The API sent ${parts.length} upload URLs for a ${expected}-part project. Run this again.`);
|
|
30719
|
+
return null;
|
|
30720
|
+
}
|
|
30721
|
+
const file = await fs49.open(zipPath, "r");
|
|
30722
|
+
const done = [];
|
|
30723
|
+
let sent = 0;
|
|
30724
|
+
let next = 0;
|
|
30725
|
+
let failed = false;
|
|
30726
|
+
try {
|
|
30727
|
+
const worker = async () => {
|
|
30728
|
+
while (!failed) {
|
|
30729
|
+
const index = next++;
|
|
30730
|
+
const part = parts[index];
|
|
30731
|
+
if (!part) return;
|
|
30732
|
+
const start = index * upload.partBytes;
|
|
30733
|
+
const length = Math.min(upload.partBytes, sizeBytes - start);
|
|
30734
|
+
const bytes = await readPart2(file, start, length);
|
|
30735
|
+
const etag = await putPart(part.url, bytes, sleep7);
|
|
30736
|
+
if (!etag) {
|
|
30737
|
+
failed = true;
|
|
30738
|
+
log.error(`Upload failed (part ${part.partNumber}/${parts.length}).`);
|
|
30739
|
+
return;
|
|
30740
|
+
}
|
|
30741
|
+
done.push({ partNumber: part.partNumber, etag });
|
|
30742
|
+
sent += length;
|
|
30743
|
+
onProgress?.(sent);
|
|
30744
|
+
}
|
|
30745
|
+
};
|
|
30746
|
+
await Promise.all(Array.from({ length: Math.min(PART_CONCURRENCY2, parts.length) }, () => worker()));
|
|
30747
|
+
} finally {
|
|
30748
|
+
await file.close();
|
|
30749
|
+
}
|
|
30750
|
+
if (failed) return null;
|
|
30751
|
+
return done.sort((a, b) => a.partNumber - b.partNumber);
|
|
30752
|
+
}
|
|
30753
|
+
|
|
30754
|
+
// src/lib/source-zip.ts
|
|
30755
|
+
import fs50 from "fs/promises";
|
|
30756
|
+
import path50 from "path";
|
|
30757
|
+
import zlib4 from "zlib";
|
|
30758
|
+
var SOURCE_ZIP_ROOT_EXCLUDES = [".genex", "Intermediate", "Saved", "DerivedDataCache", "Binaries"];
|
|
30759
|
+
var SOURCE_ZIP_EXCLUDES_ANYWHERE = [".git", ".DS_Store", ".env"];
|
|
30760
|
+
var ENV_VARIANT = /^\.env\./;
|
|
30761
|
+
var ENV_TEMPLATE = ".env.example";
|
|
30762
|
+
var SourceZipError = class extends Error {
|
|
30763
|
+
};
|
|
30764
|
+
var DOS_TIME = 0;
|
|
30765
|
+
var DOS_DATE2 = 0 << 9 | 1 << 5 | 1;
|
|
30766
|
+
var UTF8_FLAG = 2048;
|
|
30767
|
+
var VERSION_STORED = 20;
|
|
30768
|
+
var VERSION_ZIP64 = 45;
|
|
30769
|
+
var U16_MAX2 = 65535;
|
|
30770
|
+
var U32_MAX2 = 4294967295;
|
|
30771
|
+
var CHUNK = 1024 * 1024;
|
|
30772
|
+
var isExcludedAnywhere = (name) => SOURCE_ZIP_EXCLUDES_ANYWHERE.includes(name) || ENV_VARIANT.test(name) && name !== ENV_TEMPLATE;
|
|
30773
|
+
function inside(root, target) {
|
|
30774
|
+
const rel = path50.relative(root, target);
|
|
30775
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${path50.sep}`) && !path50.isAbsolute(rel);
|
|
30776
|
+
}
|
|
30777
|
+
async function plan(root) {
|
|
30778
|
+
const entries = [];
|
|
30779
|
+
const skippedLinks = [];
|
|
30780
|
+
const walk2 = async (dir, rel) => {
|
|
30781
|
+
const children = await fs50.readdir(dir, { withFileTypes: true });
|
|
30782
|
+
let kept = 0;
|
|
30783
|
+
for (const child of children) {
|
|
30784
|
+
const name = child.name;
|
|
30785
|
+
const childRel = rel ? `${rel}/${name}` : name;
|
|
30786
|
+
const abs = path50.join(dir, name);
|
|
30787
|
+
if (isExcludedAnywhere(name)) continue;
|
|
30788
|
+
if (!rel && SOURCE_ZIP_ROOT_EXCLUDES.includes(name)) continue;
|
|
30789
|
+
if (child.isSymbolicLink()) {
|
|
30790
|
+
const target = await fs50.realpath(abs).catch(() => null);
|
|
30791
|
+
if (target !== null && !inside(root, target)) {
|
|
30792
|
+
throw new SourceZipError(
|
|
30793
|
+
`${childRel} is a link to ${target}, outside the folder being built. Copy what it points to into the project, or remove the link.`
|
|
30794
|
+
);
|
|
30795
|
+
}
|
|
30796
|
+
skippedLinks.push(childRel);
|
|
30797
|
+
continue;
|
|
30798
|
+
}
|
|
30799
|
+
if (child.isDirectory()) {
|
|
30800
|
+
kept++;
|
|
30801
|
+
await walk2(abs, childRel);
|
|
30802
|
+
} else if (child.isFile()) {
|
|
30803
|
+
kept++;
|
|
30804
|
+
const { size } = await fs50.stat(abs);
|
|
30805
|
+
entries.push({ name: childRel, abs, size });
|
|
30806
|
+
}
|
|
30807
|
+
}
|
|
30808
|
+
if (rel && kept === 0) entries.push({ name: `${rel}/`, size: 0 });
|
|
30809
|
+
};
|
|
30810
|
+
await walk2(root, "");
|
|
30811
|
+
entries.sort((a, b) => Buffer.compare(Buffer.from(a.name, "utf8"), Buffer.from(b.name, "utf8")));
|
|
30812
|
+
skippedLinks.sort();
|
|
30813
|
+
return { entries, skippedLinks };
|
|
30814
|
+
}
|
|
30815
|
+
var crcTable = (() => {
|
|
30816
|
+
const table = new Uint32Array(256);
|
|
30817
|
+
for (let n = 0; n < 256; n++) {
|
|
30818
|
+
let c2 = n;
|
|
30819
|
+
for (let k = 0; k < 8; k++) c2 = c2 & 1 ? 3988292384 ^ c2 >>> 1 : c2 >>> 1;
|
|
30820
|
+
table[n] = c2 >>> 0;
|
|
30821
|
+
}
|
|
30822
|
+
return table;
|
|
30823
|
+
})();
|
|
30824
|
+
var zlibCrc = zlib4.crc32;
|
|
30825
|
+
function crc32(data, value) {
|
|
30826
|
+
if (zlibCrc) return zlibCrc(data, value);
|
|
30827
|
+
let c2 = (value ^ U32_MAX2) >>> 0;
|
|
30828
|
+
for (let i = 0; i < data.length; i++) c2 = crcTable[(c2 ^ data[i]) & 255] ^ c2 >>> 8;
|
|
30829
|
+
return (c2 ^ U32_MAX2) >>> 0;
|
|
30830
|
+
}
|
|
30831
|
+
var Out = class {
|
|
30832
|
+
position = 0;
|
|
30833
|
+
file;
|
|
30834
|
+
constructor(file) {
|
|
30835
|
+
this.file = file;
|
|
30836
|
+
}
|
|
30837
|
+
async write(buffer) {
|
|
30838
|
+
let done = 0;
|
|
30839
|
+
while (done < buffer.length) {
|
|
30840
|
+
const { bytesWritten } = await this.file.write(buffer, done, buffer.length - done, this.position + done);
|
|
30841
|
+
done += bytesWritten;
|
|
30842
|
+
}
|
|
30843
|
+
this.position += buffer.length;
|
|
30844
|
+
}
|
|
30845
|
+
async patch(at, buffer) {
|
|
30846
|
+
await this.file.write(buffer, 0, buffer.length, at);
|
|
30847
|
+
}
|
|
30848
|
+
};
|
|
30849
|
+
async function writeSourceZip(root, zipPath, opts = {}) {
|
|
30850
|
+
const { entries, skippedLinks } = await plan(await fs50.realpath(root));
|
|
30851
|
+
const file = await fs50.open(zipPath, "w");
|
|
30852
|
+
const out = new Out(file);
|
|
30853
|
+
const central = [];
|
|
30854
|
+
let contentBytes = 0;
|
|
30855
|
+
const chunk = Buffer.alloc(CHUNK);
|
|
30856
|
+
try {
|
|
30857
|
+
for (const entry of entries) {
|
|
30858
|
+
const name = Buffer.from(entry.name, "utf8");
|
|
30859
|
+
const offset = out.position;
|
|
30860
|
+
const bigSize = opts.forceZip64 || entry.size >= U32_MAX2;
|
|
30861
|
+
const bigOffset = opts.forceZip64 || offset >= U32_MAX2;
|
|
30862
|
+
const localExtra = bigSize ? zip64Extra([entry.size, entry.size]) : Buffer.alloc(0);
|
|
30863
|
+
const local = Buffer.alloc(30);
|
|
30864
|
+
local.writeUInt32LE(67324752, 0);
|
|
30865
|
+
local.writeUInt16LE(bigSize ? VERSION_ZIP64 : VERSION_STORED, 4);
|
|
30866
|
+
local.writeUInt16LE(UTF8_FLAG, 6);
|
|
30867
|
+
local.writeUInt16LE(0, 8);
|
|
30868
|
+
local.writeUInt16LE(DOS_TIME, 10);
|
|
30869
|
+
local.writeUInt16LE(DOS_DATE2, 12);
|
|
30870
|
+
local.writeUInt32LE(0, 14);
|
|
30871
|
+
local.writeUInt32LE(bigSize ? U32_MAX2 : entry.size, 18);
|
|
30872
|
+
local.writeUInt32LE(bigSize ? U32_MAX2 : entry.size, 22);
|
|
30873
|
+
local.writeUInt16LE(name.length, 26);
|
|
30874
|
+
local.writeUInt16LE(localExtra.length, 28);
|
|
30875
|
+
await out.write(Buffer.concat([local, name, localExtra]));
|
|
30876
|
+
let crc = 0;
|
|
30877
|
+
if (entry.abs) {
|
|
30878
|
+
const source = await fs50.open(entry.abs, "r");
|
|
30879
|
+
try {
|
|
30880
|
+
let read = 0;
|
|
30881
|
+
for (; ; ) {
|
|
30882
|
+
const { bytesRead } = await source.read(chunk, 0, CHUNK, read);
|
|
30883
|
+
if (bytesRead === 0) break;
|
|
30884
|
+
read += bytesRead;
|
|
30885
|
+
if (read > entry.size) break;
|
|
30886
|
+
const piece = chunk.subarray(0, bytesRead);
|
|
30887
|
+
crc = crc32(piece, crc);
|
|
30888
|
+
await out.write(piece);
|
|
30889
|
+
}
|
|
30890
|
+
if (read !== entry.size) throw new SourceZipError(`${entry.name} changed while it was being zipped. Close the editor and run this again.`);
|
|
30891
|
+
} finally {
|
|
30892
|
+
await source.close();
|
|
30893
|
+
}
|
|
30894
|
+
contentBytes += entry.size;
|
|
30895
|
+
const crcBytes = Buffer.alloc(4);
|
|
30896
|
+
crcBytes.writeUInt32LE(crc >>> 0, 0);
|
|
30897
|
+
await out.patch(offset + 14, crcBytes);
|
|
30898
|
+
}
|
|
30899
|
+
const centralFields = [];
|
|
30900
|
+
if (bigSize) centralFields.push(entry.size, entry.size);
|
|
30901
|
+
if (bigOffset) centralFields.push(offset);
|
|
30902
|
+
const centralExtra = centralFields.length ? zip64Extra(centralFields) : Buffer.alloc(0);
|
|
30903
|
+
const needs64 = bigSize || bigOffset;
|
|
30904
|
+
const head = Buffer.alloc(46);
|
|
30905
|
+
head.writeUInt32LE(33639248, 0);
|
|
30906
|
+
head.writeUInt16LE(needs64 ? VERSION_ZIP64 : VERSION_STORED, 4);
|
|
30907
|
+
head.writeUInt16LE(needs64 ? VERSION_ZIP64 : VERSION_STORED, 6);
|
|
30908
|
+
head.writeUInt16LE(UTF8_FLAG, 8);
|
|
30909
|
+
head.writeUInt16LE(0, 10);
|
|
30910
|
+
head.writeUInt16LE(DOS_TIME, 12);
|
|
30911
|
+
head.writeUInt16LE(DOS_DATE2, 14);
|
|
30912
|
+
head.writeUInt32LE(crc >>> 0, 16);
|
|
30913
|
+
head.writeUInt32LE(bigSize ? U32_MAX2 : entry.size, 20);
|
|
30914
|
+
head.writeUInt32LE(bigSize ? U32_MAX2 : entry.size, 24);
|
|
30915
|
+
head.writeUInt16LE(name.length, 28);
|
|
30916
|
+
head.writeUInt16LE(centralExtra.length, 30);
|
|
30917
|
+
head.writeUInt16LE(0, 32);
|
|
30918
|
+
head.writeUInt16LE(0, 34);
|
|
30919
|
+
head.writeUInt16LE(0, 36);
|
|
30920
|
+
head.writeUInt32LE(entry.abs ? 0 : 16, 38);
|
|
30921
|
+
head.writeUInt32LE(bigOffset ? U32_MAX2 : offset, 42);
|
|
30922
|
+
central.push(head, name, centralExtra);
|
|
30923
|
+
}
|
|
30924
|
+
const directoryOffset = out.position;
|
|
30925
|
+
const directory = Buffer.concat(central);
|
|
30926
|
+
await out.write(directory);
|
|
30927
|
+
const count = entries.length;
|
|
30928
|
+
const end64 = opts.forceZip64 || count >= U16_MAX2 || directory.length >= U32_MAX2 || directoryOffset >= U32_MAX2;
|
|
30929
|
+
if (end64) {
|
|
30930
|
+
const recordOffset = out.position;
|
|
30931
|
+
const record = Buffer.alloc(56);
|
|
30932
|
+
record.writeUInt32LE(101075792, 0);
|
|
30933
|
+
record.writeBigUInt64LE(44n, 4);
|
|
30934
|
+
record.writeUInt16LE(VERSION_ZIP64, 12);
|
|
30935
|
+
record.writeUInt16LE(VERSION_ZIP64, 14);
|
|
30936
|
+
record.writeUInt32LE(0, 16);
|
|
30937
|
+
record.writeUInt32LE(0, 20);
|
|
30938
|
+
record.writeBigUInt64LE(BigInt(count), 24);
|
|
30939
|
+
record.writeBigUInt64LE(BigInt(count), 32);
|
|
30940
|
+
record.writeBigUInt64LE(BigInt(directory.length), 40);
|
|
30941
|
+
record.writeBigUInt64LE(BigInt(directoryOffset), 48);
|
|
30942
|
+
const locator = Buffer.alloc(20);
|
|
30943
|
+
locator.writeUInt32LE(117853008, 0);
|
|
30944
|
+
locator.writeUInt32LE(0, 4);
|
|
30945
|
+
locator.writeBigUInt64LE(BigInt(recordOffset), 8);
|
|
30946
|
+
locator.writeUInt32LE(1, 16);
|
|
30947
|
+
await out.write(Buffer.concat([record, locator]));
|
|
30948
|
+
}
|
|
30949
|
+
const end = Buffer.alloc(22);
|
|
30950
|
+
end.writeUInt32LE(101010256, 0);
|
|
30951
|
+
end.writeUInt16LE(end64 ? U16_MAX2 : count, 8);
|
|
30952
|
+
end.writeUInt16LE(end64 ? U16_MAX2 : count, 10);
|
|
30953
|
+
end.writeUInt32LE(end64 ? U32_MAX2 : directory.length, 12);
|
|
30954
|
+
end.writeUInt32LE(end64 ? U32_MAX2 : directoryOffset, 16);
|
|
30955
|
+
await out.write(end);
|
|
30956
|
+
} finally {
|
|
30957
|
+
await file.close();
|
|
30958
|
+
}
|
|
30959
|
+
return { entries: entries.length, contentBytes, skippedLinks };
|
|
30960
|
+
}
|
|
30961
|
+
function zip64Extra(values) {
|
|
30962
|
+
const extra = Buffer.alloc(4 + values.length * 8);
|
|
30963
|
+
extra.writeUInt16LE(1, 0);
|
|
30964
|
+
extra.writeUInt16LE(values.length * 8, 2);
|
|
30965
|
+
values.forEach((v, i) => extra.writeBigUInt64LE(BigInt(v), 4 + i * 8));
|
|
30966
|
+
return extra;
|
|
30967
|
+
}
|
|
30968
|
+
async function resolveZipSource(dir, cwd = process.cwd()) {
|
|
30969
|
+
const abs = path50.resolve(cwd, dir);
|
|
30970
|
+
const stat = await fs50.lstat(abs).catch(() => null);
|
|
30971
|
+
if (!stat) return { refused: `--source ${abs} does not exist.` };
|
|
30972
|
+
if (stat.isSymbolicLink()) return { refused: `--source ${abs} is a symbolic link. Pass the folder itself.` };
|
|
30973
|
+
if (!stat.isDirectory()) return { refused: `--source ${abs} is not a folder.` };
|
|
30974
|
+
return { dir: await fs50.realpath(abs) };
|
|
30975
|
+
}
|
|
30976
|
+
|
|
30517
30977
|
// src/commands/native.ts
|
|
30518
30978
|
var PROGRESS_STEPS = 10;
|
|
30519
30979
|
var MB2 = 1024 * 1024;
|
|
@@ -30557,9 +31017,9 @@ async function context3(opts, log) {
|
|
|
30557
31017
|
}
|
|
30558
31018
|
return { apiUrl, projectId: meta.id, token };
|
|
30559
31019
|
}
|
|
30560
|
-
function refusedInHostedSession(log) {
|
|
31020
|
+
function refusedInHostedSession(log, line = "Native builds are published from the creator's own computer, not from a hosted session.") {
|
|
30561
31021
|
if (!inHostedSession()) return false;
|
|
30562
|
-
log.error(
|
|
31022
|
+
log.error(line);
|
|
30563
31023
|
return true;
|
|
30564
31024
|
}
|
|
30565
31025
|
function sizeLine(bytes) {
|
|
@@ -30698,16 +31158,253 @@ async function runRemove(opts, log) {
|
|
|
30698
31158
|
log.success(`Removed the ${opts.platform} ${opts.arch} build. Copies already installed keep working.`);
|
|
30699
31159
|
return true;
|
|
30700
31160
|
}
|
|
31161
|
+
var CLOUD_HOSTED_REFUSAL = "Build on Genex is started from the creator's own computer, not from a hosted session.";
|
|
31162
|
+
var FAILURE_WORDS = {
|
|
31163
|
+
"engine-unsupported": "the project's Unreal version isn't one the build machine has",
|
|
31164
|
+
compile: "the game's code didn't compile",
|
|
31165
|
+
cook: "cooking the game's content failed",
|
|
31166
|
+
timeout: "the build ran past its time limit",
|
|
31167
|
+
quota: "this account has used its builds for the month",
|
|
31168
|
+
"source-too-big": "the project is over the size limit",
|
|
31169
|
+
"not-on-genex": "the game has no page on Genex yet",
|
|
31170
|
+
failed: "the build machine stopped or its result failed the checks"
|
|
31171
|
+
};
|
|
31172
|
+
var STATE_WORDS = {
|
|
31173
|
+
queued: "Queued",
|
|
31174
|
+
building: "Building",
|
|
31175
|
+
publishing: "Publishing",
|
|
31176
|
+
done: "Built",
|
|
31177
|
+
failed: "Failed"
|
|
31178
|
+
};
|
|
31179
|
+
function printRefusal(opts, refusal2) {
|
|
31180
|
+
if (opts.json && refusal2.body) console.log(JSON.stringify(refusal2.body));
|
|
31181
|
+
}
|
|
31182
|
+
function stateLine(build) {
|
|
31183
|
+
const state = STATE_WORDS[build.state] ?? build.state;
|
|
31184
|
+
if (build.state !== "failed") return `${state} \u2014 ${build.platform} build ${build.jobId}`;
|
|
31185
|
+
const why = build.failure ? `: ${FAILURE_WORDS[build.failure] ?? build.failure} (${build.failure})` : "";
|
|
31186
|
+
return `${state}${why} \u2014 ${build.platform} build ${build.jobId}`;
|
|
31187
|
+
}
|
|
31188
|
+
function reportCloudBuild(build, how, log) {
|
|
31189
|
+
if (how === "deduplicated") {
|
|
31190
|
+
log.success(`Already built (no change since): ${stateLine(build)}`);
|
|
31191
|
+
return;
|
|
31192
|
+
}
|
|
31193
|
+
const line = how === "running" ? `Already building: ${stateLine(build)}` : stateLine(build);
|
|
31194
|
+
if (build.state === "failed") log.warn(line);
|
|
31195
|
+
else log.success(line);
|
|
31196
|
+
if (build.state !== "done" && build.state !== "failed") {
|
|
31197
|
+
log.dim(` Follow it: genex native build-status ${build.jobId}`);
|
|
31198
|
+
log.dim(" When it is done the build is attached to the game's page (held for review unless you are a Genex admin).");
|
|
31199
|
+
}
|
|
31200
|
+
}
|
|
31201
|
+
async function runBuild(opts, log) {
|
|
31202
|
+
if (refusedInHostedSession(log, CLOUD_HOSTED_REFUSAL)) return false;
|
|
31203
|
+
if (!opts.platform || !isCloudBuildPlatform(opts.platform)) {
|
|
31204
|
+
log.error("Which platform? `genex native build --platform windows --source <folder>` (windows, linux or mac).");
|
|
31205
|
+
return false;
|
|
31206
|
+
}
|
|
31207
|
+
if (!opts.sourceDir) {
|
|
31208
|
+
log.error("Which project? `genex native build --platform windows --source <folder>` \u2014 the folder holding the .uproject.");
|
|
31209
|
+
return false;
|
|
31210
|
+
}
|
|
31211
|
+
if (opts.buildVersion !== void 0) {
|
|
31212
|
+
try {
|
|
31213
|
+
buildVersion(opts.buildVersion, void 0, "");
|
|
31214
|
+
} catch (err) {
|
|
31215
|
+
if (!(err instanceof NativeBuildError)) throw err;
|
|
31216
|
+
log.error(err.message);
|
|
31217
|
+
return false;
|
|
31218
|
+
}
|
|
31219
|
+
}
|
|
31220
|
+
const source = await resolveZipSource(opts.sourceDir);
|
|
31221
|
+
if ("refused" in source) {
|
|
31222
|
+
log.error(source.refused);
|
|
31223
|
+
return false;
|
|
31224
|
+
}
|
|
31225
|
+
const ctx = await context3(opts, log);
|
|
31226
|
+
if (!ctx) return false;
|
|
31227
|
+
const tmp = await fs51.mkdtemp(path51.join(os15.tmpdir(), "genex-cloud-build-"));
|
|
31228
|
+
const zipPath = path51.join(tmp, "source.zip");
|
|
31229
|
+
try {
|
|
31230
|
+
log.step(`Zipping ${source.dir}\u2026`);
|
|
31231
|
+
let zipped;
|
|
31232
|
+
try {
|
|
31233
|
+
zipped = await writeSourceZip(source.dir, zipPath);
|
|
31234
|
+
} catch (err) {
|
|
31235
|
+
if (!(err instanceof SourceZipError)) throw err;
|
|
31236
|
+
log.error(err.message);
|
|
31237
|
+
return false;
|
|
31238
|
+
}
|
|
31239
|
+
if (zipped.entries === 0) {
|
|
31240
|
+
log.error(`${source.dir} has nothing to build in it.`);
|
|
31241
|
+
return false;
|
|
31242
|
+
}
|
|
31243
|
+
if (zipped.skippedLinks.length > 0) {
|
|
31244
|
+
const shown = zipped.skippedLinks.slice(0, 5).join(", ");
|
|
31245
|
+
const more = zipped.skippedLinks.length > 5 ? ` and ${zipped.skippedLinks.length - 5} more` : "";
|
|
31246
|
+
log.warn(`Left out ${zipped.skippedLinks.length} link(s) inside the folder: ${shown}${more}.`);
|
|
31247
|
+
}
|
|
31248
|
+
const { sha256: sha2564, sizeBytes } = await hashFile(zipPath);
|
|
31249
|
+
log.step(`${zipped.entries} files, ${sizeLine(sizeBytes)} zipped.`);
|
|
31250
|
+
const body = {
|
|
31251
|
+
platform: opts.platform,
|
|
31252
|
+
sourceSha256: sha2564,
|
|
31253
|
+
sourceBytes: sizeBytes,
|
|
31254
|
+
...opts.buildVersion !== void 0 ? { version: opts.buildVersion } : {}
|
|
31255
|
+
};
|
|
31256
|
+
const started = await startCloudBuild(ctx, body, log);
|
|
31257
|
+
if (!started.ok) {
|
|
31258
|
+
printRefusal(opts, started.refusal);
|
|
31259
|
+
return false;
|
|
31260
|
+
}
|
|
31261
|
+
let build = started.value.build;
|
|
31262
|
+
let upload = started.value.upload;
|
|
31263
|
+
const how = started.value.deduplicated ? "deduplicated" : started.value.status === 201 ? "new" : "running";
|
|
31264
|
+
let uploaded = false;
|
|
31265
|
+
for (let round2 = 0; upload; round2++) {
|
|
31266
|
+
log.step(`Uploading ${sizeLine(sizeBytes)}\u2026`);
|
|
31267
|
+
const parts = await uploadCloudBuildSource(upload, zipPath, sizeBytes, log, progress(log, "uploaded", sizeBytes));
|
|
31268
|
+
if (!parts) return false;
|
|
31269
|
+
const done = await completeCloudBuildSource(ctx, build.jobId, parts, log);
|
|
31270
|
+
if (done.ok) {
|
|
31271
|
+
build = done.value;
|
|
31272
|
+
uploaded = true;
|
|
31273
|
+
break;
|
|
31274
|
+
}
|
|
31275
|
+
if (done.refusal.error !== UPLOAD_INCOMPLETE) {
|
|
31276
|
+
printRefusal(opts, done.refusal);
|
|
31277
|
+
return false;
|
|
31278
|
+
}
|
|
31279
|
+
if (round2 > 0) {
|
|
31280
|
+
log.error("The upload still isn't complete after sending it twice. Run this again.");
|
|
31281
|
+
printRefusal(opts, done.refusal);
|
|
31282
|
+
return false;
|
|
31283
|
+
}
|
|
31284
|
+
log.warn("Some parts did not arrive. Sending the project again\u2026");
|
|
31285
|
+
const again = await startCloudBuild(ctx, body, log);
|
|
31286
|
+
if (!again.ok) {
|
|
31287
|
+
printRefusal(opts, again.refusal);
|
|
31288
|
+
return false;
|
|
31289
|
+
}
|
|
31290
|
+
build = again.value.build;
|
|
31291
|
+
upload = again.value.upload;
|
|
31292
|
+
}
|
|
31293
|
+
reportCloudBuild(build, how === "running" && uploaded ? "new" : how, log);
|
|
31294
|
+
if (opts.json) console.log(JSON.stringify({ build }));
|
|
31295
|
+
return true;
|
|
31296
|
+
} finally {
|
|
31297
|
+
await fs51.rm(tmp, { recursive: true, force: true });
|
|
31298
|
+
}
|
|
31299
|
+
}
|
|
31300
|
+
async function runBuildStatus(opts, log) {
|
|
31301
|
+
if (refusedInHostedSession(log, CLOUD_HOSTED_REFUSAL)) return false;
|
|
31302
|
+
if (!opts.jobId || !CLOUD_BUILD_JOB_ID.test(opts.jobId)) {
|
|
31303
|
+
log.error(
|
|
31304
|
+
opts.jobId ? `"${opts.jobId}" is not a build id. \`genex native build\` prints one (cb-\u2026).` : "Which build? `genex native build-status <jobId>` \u2014 the id `genex native build` printed."
|
|
31305
|
+
);
|
|
31306
|
+
return false;
|
|
31307
|
+
}
|
|
31308
|
+
const ctx = await context3(opts, log);
|
|
31309
|
+
if (!ctx) return false;
|
|
31310
|
+
const read = await readCloudBuild(ctx, opts.jobId, log);
|
|
31311
|
+
if (!read.ok) {
|
|
31312
|
+
printRefusal(opts, read.refusal);
|
|
31313
|
+
return false;
|
|
31314
|
+
}
|
|
31315
|
+
const build = read.value;
|
|
31316
|
+
if (opts.json) {
|
|
31317
|
+
console.log(JSON.stringify({ build }));
|
|
31318
|
+
return true;
|
|
31319
|
+
}
|
|
31320
|
+
if (build.state === "failed") log.warn(stateLine(build));
|
|
31321
|
+
else log.success(stateLine(build));
|
|
31322
|
+
log.dim(` Started ${build.startedAt}${build.finishedAt ? `, finished ${build.finishedAt}` : ""}.`);
|
|
31323
|
+
return true;
|
|
31324
|
+
}
|
|
30701
31325
|
var SUBCOMMANDS2 = {
|
|
30702
31326
|
publish: runPublish2,
|
|
30703
31327
|
list: runList2,
|
|
30704
|
-
remove: runRemove
|
|
31328
|
+
remove: runRemove,
|
|
31329
|
+
build: runBuild,
|
|
31330
|
+
"build-status": runBuildStatus
|
|
30705
31331
|
};
|
|
30706
31332
|
async function runNative(opts) {
|
|
30707
31333
|
const log = loggerFor2(opts);
|
|
30708
31334
|
const run5 = opts.name ? SUBCOMMANDS2[opts.name] : void 0;
|
|
30709
31335
|
if (!run5) {
|
|
30710
|
-
log.error(
|
|
31336
|
+
log.error(
|
|
31337
|
+
"Unknown native command. Use `genex native publish <path>`, `genex native list`, `genex native remove`, `genex native build` or `genex native build-status <jobId>`."
|
|
31338
|
+
);
|
|
31339
|
+
process.exitCode = 1;
|
|
31340
|
+
return;
|
|
31341
|
+
}
|
|
31342
|
+
if (!await run5(opts, log)) process.exitCode = 1;
|
|
31343
|
+
}
|
|
31344
|
+
|
|
31345
|
+
// src/commands/web.ts
|
|
31346
|
+
function loggerFor3(opts) {
|
|
31347
|
+
if (!opts.json) return createLogger({ quiet: opts.quiet });
|
|
31348
|
+
const err = (s) => {
|
|
31349
|
+
if (!opts.quiet) writeSafely(process.stderr, `${s}
|
|
31350
|
+
`);
|
|
31351
|
+
};
|
|
31352
|
+
return {
|
|
31353
|
+
info: (m) => err(`i ${m}`),
|
|
31354
|
+
success: (m) => err(`\u2713 ${m}`),
|
|
31355
|
+
warn: (m) => err(`! ${m}`),
|
|
31356
|
+
error: (m) => writeSafely(process.stderr, `\u2717 ${m}
|
|
31357
|
+
`),
|
|
31358
|
+
step: (m) => err(`\u203A ${m}`),
|
|
31359
|
+
dim: err,
|
|
31360
|
+
plain: err
|
|
31361
|
+
};
|
|
31362
|
+
}
|
|
31363
|
+
async function runRemove2(opts, log) {
|
|
31364
|
+
if (inHostedSession()) {
|
|
31365
|
+
log.error("`genex web remove` is not available inside a Genex chat session \u2014 it changes what players get.");
|
|
31366
|
+
return false;
|
|
31367
|
+
}
|
|
31368
|
+
if (!opts.yes) {
|
|
31369
|
+
log.warn("This removes the game's web version and keeps its Mac/Windows/Linux apps.");
|
|
31370
|
+
log.dim(" The game's page goes back to showing the apps, and its play address stops serving a web build.");
|
|
31371
|
+
log.dim(" Both web builds go: the live one and the preview. They cannot be restored; publish again to bring one back.");
|
|
31372
|
+
log.dim(" The apps, the gallery listing, likes and the cover are not touched.");
|
|
31373
|
+
log.dim(" Refused when the game has no app players get \u2014 unpublish or delete such a game instead.");
|
|
31374
|
+
log.dim(` Re-run with --yes to do it: ${c.cyan("npx genex web remove --yes")}`);
|
|
31375
|
+
return false;
|
|
31376
|
+
}
|
|
31377
|
+
const meta = await readProject();
|
|
31378
|
+
if (!meta) {
|
|
31379
|
+
log.error("No genex project here. Run `genex link <game>` in this directory first.");
|
|
31380
|
+
return false;
|
|
31381
|
+
}
|
|
31382
|
+
const apiUrl = opts.apiUrl ?? getApiUrl(meta.apiUrl);
|
|
31383
|
+
const token = opts.token ?? await readUserToken(opts.envPath, apiUrl);
|
|
31384
|
+
if (!token) {
|
|
31385
|
+
log.error("Not authorized. Run `genex link <your-game>` to sign in.");
|
|
31386
|
+
return false;
|
|
31387
|
+
}
|
|
31388
|
+
log.step("Removing the web version\u2026");
|
|
31389
|
+
const result = await removeWebVersion({ apiUrl, projectId: meta.id, token }, log);
|
|
31390
|
+
if (!result) return false;
|
|
31391
|
+
if ("refused" in result) {
|
|
31392
|
+
if (opts.json) console.log(JSON.stringify({ error: result.refused }));
|
|
31393
|
+
return false;
|
|
31394
|
+
}
|
|
31395
|
+
if (opts.json) console.log(JSON.stringify(result));
|
|
31396
|
+
if (result.removed) log.success("Removed the web version. The game's page shows its apps again.");
|
|
31397
|
+
else log.success("This game has no web version on Genex \u2014 nothing to remove.");
|
|
31398
|
+
return true;
|
|
31399
|
+
}
|
|
31400
|
+
var SUBCOMMANDS3 = {
|
|
31401
|
+
remove: runRemove2
|
|
31402
|
+
};
|
|
31403
|
+
async function runWeb(opts) {
|
|
31404
|
+
const log = loggerFor3(opts);
|
|
31405
|
+
const run5 = opts.name ? SUBCOMMANDS3[opts.name] : void 0;
|
|
31406
|
+
if (!run5) {
|
|
31407
|
+
log.error("Unknown web command. Use `genex web remove [--yes] [--json]`.");
|
|
30711
31408
|
process.exitCode = 1;
|
|
30712
31409
|
return;
|
|
30713
31410
|
}
|
|
@@ -30826,6 +31523,9 @@ ${c.bold("Usage")}
|
|
|
30826
31523
|
genex promote [options] Make the previewed build live for players. No
|
|
30827
31524
|
rebuild \u2014 the exact build you previewed.
|
|
30828
31525
|
genex publish [options] Build + push + make live, then list it in the gallery.
|
|
31526
|
+
Both refuse to replace a page that shows only the
|
|
31527
|
+
game's Mac/Windows/Linux app unless given
|
|
31528
|
+
--replace-native-page.
|
|
30829
31529
|
genex accept Read the current Terms, Privacy and Content
|
|
30830
31530
|
policies and agree, so publishing is allowed.
|
|
30831
31531
|
Needs a real terminal: agreeing is a contract,
|
|
@@ -30834,10 +31534,16 @@ ${c.bold("Usage")}
|
|
|
30834
31534
|
version back, instantly \u2014 your code is not
|
|
30835
31535
|
changed, only what the play URL serves.
|
|
30836
31536
|
genex native <sub> [options] This game's Mac/Windows/Linux builds for the Genex
|
|
30837
|
-
desktop app: publish <path> | list | remove
|
|
31537
|
+
desktop app: publish <path> | list | remove |
|
|
31538
|
+
build | build-status <jobId>.
|
|
30838
31539
|
"publish" takes a .zip, a .app or the build's
|
|
30839
31540
|
folder and makes it what desktop players install
|
|
30840
31541
|
for its platform and processor, live at once.
|
|
31542
|
+
"build --platform windows --source <folder>" has
|
|
31543
|
+
Genex build the Windows app of an Unreal project.
|
|
31544
|
+
genex web remove [--yes] [--json] Remove this game's web version and keep its
|
|
31545
|
+
Mac/Windows/Linux apps: the page shows the apps
|
|
31546
|
+
again. Refused for a game with no app.
|
|
30841
31547
|
genex cover [<file>] Show this game's cover and where it came from, or
|
|
30842
31548
|
make <file> (PNG/JPEG/WebP, \u22648 MB) the cover: one
|
|
30843
31549
|
real frame of the game \u2014 its signature moment,
|
|
@@ -31189,6 +31895,9 @@ ${c.bold("Options for `preview` / `publish`")}
|
|
|
31189
31895
|
--purchase-url <url> (operator) External purchase URL \u2014 marks the game a
|
|
31190
31896
|
paid-product demo ("Get \u2026" replaces Remix). Admin-only.
|
|
31191
31897
|
--product-name <name> (operator) Product name for the "Get {name}" button.
|
|
31898
|
+
--replace-native-page (publish, promote) Let this web build replace a page that
|
|
31899
|
+
shows only the game's Mac/Windows/Linux app. Use it ONLY when
|
|
31900
|
+
this folder really is the game's web version.
|
|
31192
31901
|
--api-url <url> (publish) Override the API base URL.
|
|
31193
31902
|
--env <path> Token env file (default: ~/.genex/env).
|
|
31194
31903
|
|
|
@@ -31676,6 +32385,24 @@ ${c.bold("Content filter")}
|
|
|
31676
32385
|
a body) as body horror \u2014 describe the object or machine instead: "a
|
|
31677
32386
|
statue-like figure threaded into the wall" passes where "cables entering her
|
|
31678
32387
|
spine" fails. Rejections are never retryable; reword and re-run.
|
|
32388
|
+
`,
|
|
32389
|
+
web: `${c.bold("genex web")} \u2014 this game's web version, beside its Mac/Windows/Linux apps.
|
|
32390
|
+
|
|
32391
|
+
${c.bold("Usage")}
|
|
32392
|
+
genex web remove Explain what removing the web version does, and stop.
|
|
32393
|
+
genex web remove --yes Remove it: both web builds (live and preview) go, and
|
|
32394
|
+
the game's page shows its apps again. The apps, the
|
|
32395
|
+
gallery listing, likes and the cover stay.
|
|
32396
|
+
--json One JSON object on stdout ({"removed": true, "web": false},
|
|
32397
|
+
or {"error": "<code>"} when refused).
|
|
32398
|
+
|
|
32399
|
+
${c.bold("Refused when")}
|
|
32400
|
+
The game has no app players get (a build waiting for review does not count): removing
|
|
32401
|
+
the web version would leave nothing to play \u2014 unpublish or delete the game instead.
|
|
32402
|
+
|
|
32403
|
+
${c.bold("Notes")}
|
|
32404
|
+
\`genex publish\` and \`genex promote\` never replace a page that shows only the apps
|
|
32405
|
+
unless given --replace-native-page.
|
|
31679
32406
|
`,
|
|
31680
32407
|
native: `${c.bold("genex native")} \u2014 a Unity, Godot or Unreal build players install from the Genex desktop app.
|
|
31681
32408
|
|
|
@@ -31689,6 +32416,21 @@ ${c.bold("Usage")}
|
|
|
31689
32416
|
genex native remove --platform <p> --arch <a> --yes
|
|
31690
32417
|
Stop offering one build. Installed copies keep working.
|
|
31691
32418
|
|
|
32419
|
+
${c.bold("Build on Genex")} (an Unreal project, built for a system this computer can't build for)
|
|
32420
|
+
genex native build --platform windows --source <folder> [--build-version <v>]
|
|
32421
|
+
Zip the project folder (the same files always give
|
|
32422
|
+
the same zip, so an unchanged project is not built
|
|
32423
|
+
twice), upload it privately, and build it on a Genex
|
|
32424
|
+
machine. The result is attached to the game's page
|
|
32425
|
+
like a published build. Left out: .git, .genex,
|
|
32426
|
+
.DS_Store, .env files, and the top-level Intermediate,
|
|
32427
|
+
Saved, DerivedDataCache and Binaries folders. A link
|
|
32428
|
+
out of the folder is refused; one inside it is skipped.
|
|
32429
|
+
genex native build-status <jobId> Where that build stands: queued, building,
|
|
32430
|
+
publishing, done or failed (with why).
|
|
32431
|
+
--json One JSON object on stdout ({"build": \u2026}); a refusal
|
|
32432
|
+
prints the server's error body and exits 1.
|
|
32433
|
+
|
|
31692
32434
|
${c.bold("Read from the build, or set")}
|
|
31693
32435
|
--platform mac|windows|linux A .app at the archive's root is mac, an .exe windows.
|
|
31694
32436
|
--arch arm64|x64|universal From the app's executable header.
|
|
@@ -32032,6 +32774,12 @@ function parseArgs(argv) {
|
|
|
32032
32774
|
case "--gallery":
|
|
32033
32775
|
parsed.options.gallery = true;
|
|
32034
32776
|
break;
|
|
32777
|
+
// `genex publish|promote --replace-native-page`: the owner's word that this web
|
|
32778
|
+
// build may replace a page that shows only the game's apps. Its own flag, never
|
|
32779
|
+
// a reuse of --yes/--force, because the flag must say what it consents to.
|
|
32780
|
+
case "--replace-native-page":
|
|
32781
|
+
parsed.options.replaceNativePage = true;
|
|
32782
|
+
break;
|
|
32035
32783
|
// A pair rather than one flag taking a value: `--resellable false` reads
|
|
32036
32784
|
// as a filename to every shell user who has ever mistyped it, and the two
|
|
32037
32785
|
// states have to be distinguishable from ABSENT so `shop set` can leave
|
|
@@ -32174,6 +32922,8 @@ function parseArgs(argv) {
|
|
|
32174
32922
|
} else if (parsed.command === "native") {
|
|
32175
32923
|
if (parsed.options.name === "publish" && parsed.options.nativePath === void 0) {
|
|
32176
32924
|
parsed.options.nativePath = arg;
|
|
32925
|
+
} else if (parsed.options.name === "build-status" && parsed.options.jobId === void 0) {
|
|
32926
|
+
parsed.options.jobId = arg;
|
|
32177
32927
|
} else {
|
|
32178
32928
|
parsed.error = `Unexpected argument: ${arg}`;
|
|
32179
32929
|
return parsed;
|
|
@@ -32855,6 +33605,9 @@ async function main() {
|
|
|
32855
33605
|
case "native":
|
|
32856
33606
|
await runNative(parsed.options);
|
|
32857
33607
|
break;
|
|
33608
|
+
case "web":
|
|
33609
|
+
await runWeb(parsed.options);
|
|
33610
|
+
break;
|
|
32858
33611
|
case "cover":
|
|
32859
33612
|
await runCover({ ...parsed.options, imagePath: parsed.options.name });
|
|
32860
33613
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genex-ai/cli-demo",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.38.0-dev.1079",
|
|
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": {
|
|
@@ -104,5 +104,12 @@ Ask once per round of work, in one line, and keep working while you wait.
|
|
|
104
104
|
- **`preview` says the folder is behind another machine** (`stale_source`):
|
|
105
105
|
`npx genex pull` takes the newer draft, then redo the change — never
|
|
106
106
|
`--force` over somebody else's work.
|
|
107
|
+
- **`promote` or `publish` says the page shows the game's Mac/Windows/Linux
|
|
108
|
+
app** (`native_page_would_be_replaced`): the game ships as desktop apps, and
|
|
109
|
+
this web build would replace the page that offers them. Nothing changed. Ask
|
|
110
|
+
the user whether this folder really is the game's web version; only on a
|
|
111
|
+
clear yes, re-run the same command with `--replace-native-page`. A web
|
|
112
|
+
version that went up by mistake comes off with `npx genex web remove --yes`
|
|
113
|
+
(the apps stay).
|
|
107
114
|
- **Out of credits or a lane down**: `npx genex doctor` says which; publishing
|
|
108
115
|
itself costs nothing.
|