@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 call2 of ctorArgs(content, "PointsMaterial")) {
3127
- if (/\b(map|alphaMap)\s*:/.test(call2.text)) continue;
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, call2.index)}`,
3129
+ where: `${rel}:${lineOf(content, call3.index)}`,
3130
3130
  detail: "PointsMaterial with no map/alphaMap"
3131
3131
  });
3132
3132
  }
3133
- for (const call2 of ctorArgs(content, "PerspectiveCamera")) {
3134
- const args = splitArgs(call2.text);
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, call2.index)}`,
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 call2 of dialogCalls(content)) add(found.dialogs, call2.index, `${call2.name}()`);
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(call2, skuUrl2, part) {
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) => call2(url, { method: "POST", body: JSON.stringify(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(call2, apiUrl, projectId) {
8684
- const res = await call2(`${apiUrl}/api/coin/projects/${encodeURIComponent(projectId)}/skus`).catch(() => null);
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(call2, apiUrl) {
8691
- const res = await call2(`${apiUrl}/api/store/catalog?limit=1`).catch(() => null);
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(call2, apiUrl, projectId) {
8695
- const [grid, free] = await Promise.all([readPriceGrid(call2, apiUrl, projectId), storeTakesFree(call2, apiUrl)]);
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 call2 = (url, init) => apiFetch(url, { ...init, headers: { ...auth, "Content-Type": "application/json", ...init?.headers ?? {} } });
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 call2(base);
9899
- else if (sub === "add") res = await call2(base, { method: "POST", body: JSON.stringify({ hostname }) });
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 call2(`${base}/${encodeURIComponent(hostname)}/verify`, { method: "POST" });
9902
- else res = await call2(`${base}/${encodeURIComponent(hostname)}`, { method: "DELETE" });
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: { Authorization: `Bearer ${token}` }
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 plan = [
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 plan) {
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(path50, log) {
24167
+ async function readVideo(path52, log) {
24087
24168
  let bytes;
24088
24169
  try {
24089
- bytes = await readFile(path50);
24170
+ bytes = await readFile(path52);
24090
24171
  } catch {
24091
- log.error(`Can't read ${path50}.`);
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(path50)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
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, path50, bytes, log) {
24101
- const contentType = /\.mov$/i.test(path50) ? "video/quicktime" : "video/mp4";
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(path50)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
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(plan, log, creature) {
24209
+ function printPlan(plan2, log, creature) {
24129
24210
  log.plain(c.bold(creature ? "Creature animation plan" : "Animation plan"));
24130
- for (const item of plan.items) {
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 (plan.secondsApplied === false) {
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 (plan.credits > 0) log.dim(` ${plan.totalClips} clips \xB7 ${plan.credits} credits`);
24146
- else log.dim(` ${plan.totalClips} clips`);
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 plan = m.personalPlan ? ` \xB7 personal plan: ${m.personalPlan}` : "";
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${plan}${kind}`
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 call2 = (url, init) => apiFetch(url, { ...init, headers: { ...auth, ...init?.headers ?? {} } });
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 call2(`${base}/${encodeURIComponent(target)}/cancel`, { method: "POST" }).catch(() => {
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 call2(base, {
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, call2, base, log, opts, i, results.length);
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(call2, `${base}/${encodeURIComponent(started.id)}`, opts.timeoutSec ?? DEFAULT_SAMPLE_TIMEOUT_SEC);
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, call2, base, log, opts, index, settledSoFar) {
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(call2, base);
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(call2, base) {
27413
- const res = await call2(`${base}?scope=open`).catch(() => null);
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(call2, url, timeoutSec) {
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 call2(url).catch(() => null);
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 call2 = (url, init) => apiFetch(url, { ...init, headers: { Authorization: `Bearer ${token}`, ...init?.headers ?? {} } });
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 call2(`${base}?scope=open`).catch(() => null);
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 call2 = (url, init) => apiFetch(url, { ...init, headers: { Authorization: `Bearer ${token}`, ...init?.headers ?? {} } });
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 call2(rowUrl).catch(() => null);
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 call2(`${rowUrl}/cancel`, { method: "POST" }).catch(() => null);
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(path50, init = {}, provider) {
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}${path50}`, {
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 plan = relaunchPlan(input);
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([plan.launcher, ...plan.args])}`);
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(plan.launcher, plan.args, {
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("Native builds are published from the creator's own computer, not from a hosted session.");
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("Unknown native command. Use `genex native publish <path>`, `genex native list` or `genex native remove`.");
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.36.19-dev.1076",
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.