@genex-ai/cli-demo 1.7.3-dev.474 → 1.8.0-dev.476

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.
Files changed (2) hide show
  1. package/dist/index.js +184 -8
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4825,16 +4825,20 @@ async function deployGame(ctx, opts, log) {
4825
4825
  log.error("Couldn't upload your game \u2014 please try again.");
4826
4826
  return false;
4827
4827
  }
4828
+ let sourceCommitSha;
4828
4829
  const pushed = await pushSource(
4829
4830
  cwd,
4830
4831
  ctx,
4831
4832
  log,
4832
4833
  opts.channel === "staging" ? "preview" : "main",
4833
- expectStagingCommit
4834
+ expectStagingCommit,
4835
+ (sha) => {
4836
+ sourceCommitSha = sha;
4837
+ }
4834
4838
  );
4835
4839
  if (pushed !== true) return false;
4836
4840
  log.step("Publishing\u2026");
4837
- const published = await callPublish(ctx, commit, opts, log);
4841
+ const published = await callPublish(ctx, commit, opts, log, sourceCommitSha);
4838
4842
  if (!published) return false;
4839
4843
  if (opts.channel === "staging") {
4840
4844
  const tree = await sourceTreeHash(cwd);
@@ -5043,7 +5047,7 @@ async function uploadPartWithRetry(base, encoded, auth, uploadId, partNumber, by
5043
5047
  await new Promise((resolve) => setTimeout(resolve, attempt === 0 ? 500 : 2e3));
5044
5048
  }
5045
5049
  }
5046
- async function callPublish(ctx, commit, opts, log) {
5050
+ async function callPublish(ctx, commit, opts, log, sourceCommitSha) {
5047
5051
  let res;
5048
5052
  try {
5049
5053
  res = await apiFetch(`${ctx.apiUrl}/api/games/${ctx.projectId}/publish`, {
@@ -5058,7 +5062,11 @@ async function callPublish(ctx, commit, opts, log) {
5058
5062
  matchmaking: opts.matchmaking ?? null,
5059
5063
  ...opts.embedSdkVersion ? { embedSdkVersion: opts.embedSdkVersion } : {},
5060
5064
  ...opts.multiplayer !== void 0 ? { multiplayer: opts.multiplayer } : {},
5061
- ...opts.mobileControls !== void 0 ? { mobileControls: opts.mobileControls } : {}
5065
+ ...opts.mobileControls !== void 0 ? { mobileControls: opts.mobileControls } : {},
5066
+ // Recorded into the release history and acted on by nothing. Absent when
5067
+ // the source push produced no commit (a user's own repo, or a failure
5068
+ // that already stopped the deploy), which the server accepts as null.
5069
+ ...sourceCommitSha ? { sourceCommitSha } : {}
5062
5070
  })
5063
5071
  });
5064
5072
  } catch (err) {
@@ -5085,21 +5093,21 @@ async function callPublish(ctx, commit, opts, log) {
5085
5093
  }
5086
5094
  return { url: body?.url ?? "" };
5087
5095
  }
5088
- async function pushSource(cwd, ctx, log, branch = "main", expectStagingCommit) {
5096
+ async function pushSource(cwd, ctx, log, branch = "main", expectStagingCommit, onCommit) {
5089
5097
  const target = await fetchPushUrl(ctx, log, expectStagingCommit);
5090
5098
  if (target === "stale") return "stale";
5091
5099
  if (!target) return false;
5092
5100
  const ref = target.managed ? branch : "main";
5093
- if (await pushWorktree(cwd, target.pushUrl, target.managed, log, ref)) return true;
5101
+ if (await pushWorktree(cwd, target.pushUrl, target.managed, log, ref, onCommit)) return true;
5094
5102
  if (!target.managed) return false;
5095
5103
  log.info("Retrying the source push\u2026");
5096
5104
  await new Promise((r) => setTimeout(r, 2e3));
5097
5105
  const fresh = await fetchPushUrl(ctx, log, expectStagingCommit);
5098
5106
  if (fresh === "stale") return "stale";
5099
5107
  if (!fresh) return false;
5100
- return pushWorktree(cwd, fresh.pushUrl, fresh.managed, log, ref);
5108
+ return pushWorktree(cwd, fresh.pushUrl, fresh.managed, log, ref, onCommit);
5101
5109
  }
5102
- async function pushWorktree(cwd, pushUrl, managed, log, branch = "main") {
5110
+ async function pushWorktree(cwd, pushUrl, managed, log, branch = "main", onCommit) {
5103
5111
  const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
5104
5112
  const failed = () => {
5105
5113
  log.error("Couldn't save your game's source \u2014 please try again.");
@@ -5155,6 +5163,7 @@ async function pushWorktree(cwd, pushUrl, managed, log, branch = "main") {
5155
5163
  return false;
5156
5164
  }
5157
5165
  const commit = (await run("git", ["commit-tree", tree, "-m", "source"], { ...base, ...ident })).out.trim();
5166
+ if (commit) onCommit?.(commit);
5158
5167
  await run("git", ["update-ref", "refs/heads/main", commit], base);
5159
5168
  if (lfs && (await run("git", ["lfs", "push", "--all", pushUrl, "main"], base)).code !== 0) {
5160
5169
  log.error("Couldn't upload your game's binary assets to the source store.");
@@ -5650,6 +5659,15 @@ async function promoteBuild(apiUrl, projectId, token, log) {
5650
5659
  return null;
5651
5660
  }
5652
5661
  if (res.status === 409) {
5662
+ const body2 = await res.json().catch(() => null);
5663
+ if (body2?.code === "live_is_newer") {
5664
+ log.error("The live build is newer than your preview \u2014 nothing was changed.");
5665
+ log.dim(" This happens after the live version was restored, or after a later publish.");
5666
+ log.dim(` Preview your changes again, then promote:`);
5667
+ log.dim(` npx genex preview`);
5668
+ log.dim(` npx genex promote`);
5669
+ return null;
5670
+ }
5653
5671
  log.error("Nothing to promote \u2014 run `npx genex preview` first.");
5654
5672
  return null;
5655
5673
  }
@@ -6347,6 +6365,158 @@ async function runPromote(opts) {
6347
6365
  await verifyGamePage({ apiUrl, slug: meta.slug, published: meta.status === "published", log });
6348
6366
  }
6349
6367
 
6368
+ // src/lib/rollback.ts
6369
+ async function readPreviousVersion(apiUrl, projectId, token, log) {
6370
+ let res;
6371
+ try {
6372
+ res = await apiFetch(`${apiUrl}/api/games/${projectId}/previous-build?channel=production`, {
6373
+ headers: { Authorization: `Bearer ${token}` }
6374
+ });
6375
+ } catch (err) {
6376
+ log.error(`Couldn't reach the API at ${apiUrl} \u2014 nothing was changed: ${String(err)}`);
6377
+ return null;
6378
+ }
6379
+ if (res.status === 404) {
6380
+ log.error("That game wasn't found on your account.");
6381
+ log.dim(` Reconnect this folder with ${c.cyan("npx genex link <slug>")}.`);
6382
+ return null;
6383
+ }
6384
+ if (!res.ok) {
6385
+ log.error(`Couldn't read this game's versions (HTTP ${res.status}).`);
6386
+ return null;
6387
+ }
6388
+ const body = await res.json().catch(() => null);
6389
+ if (body?.channel !== "production") {
6390
+ log.error("This CLI is newer than the server for this command.");
6391
+ log.dim(" Nothing was changed. Try again shortly, or update the server.");
6392
+ return null;
6393
+ }
6394
+ if (!body.available || !body.commit || !body.currentCommit) {
6395
+ log.error("This game has no previous version to go back to.");
6396
+ log.dim(" Only versions published from this account since the feature shipped are recorded.");
6397
+ return null;
6398
+ }
6399
+ return {
6400
+ commit: body.commit,
6401
+ deployedAt: body.deployedAt ?? null,
6402
+ currentCommit: body.currentCommit
6403
+ };
6404
+ }
6405
+ async function restorePreviousVersion(apiUrl, projectId, token, expectLiveCommit, log) {
6406
+ let res;
6407
+ try {
6408
+ res = await apiFetch(`${apiUrl}/api/games/${projectId}/rollback`, {
6409
+ method: "POST",
6410
+ headers: { Authorization: `Bearer ${token}`, "If-Match": expectLiveCommit }
6411
+ });
6412
+ } catch (err) {
6413
+ log.error(`Couldn't reach the API at ${apiUrl}: ${String(err)}`);
6414
+ return null;
6415
+ }
6416
+ if (res.status === 409) {
6417
+ const body2 = await res.json().catch(() => null);
6418
+ if (body2?.error === "live_moved") {
6419
+ log.error("Someone published a new version while this was open \u2014 nothing was changed.");
6420
+ if (body2.liveCommit) log.dim(` players are now on ${body2.liveCommit.slice(0, 7)}`);
6421
+ log.dim(" Run the command again to go back from the version that is live now.");
6422
+ } else if (body2?.error === "already_live") {
6423
+ log.error("The previous version is already the live one \u2014 nothing to do.");
6424
+ } else if (body2?.error === "build_gone") {
6425
+ log.error("That version's files are no longer stored, so it can't be restored.");
6426
+ log.dim(" Only the most recent versions are kept. Fix forward instead:");
6427
+ log.dim(` ${c.cyan("npx genex preview")} then ${c.cyan("npx genex promote")}`);
6428
+ } else {
6429
+ log.error("This game has no previous version to go back to.");
6430
+ }
6431
+ return null;
6432
+ }
6433
+ if (res.status === 428) {
6434
+ log.error("This CLI is out of date for this command \u2014 upgrade and try again.");
6435
+ return null;
6436
+ }
6437
+ if (res.status === 404) {
6438
+ log.error("That game wasn't found on your account.");
6439
+ log.dim(` Reconnect this folder with ${c.cyan("npx genex link <slug>")}.`);
6440
+ return null;
6441
+ }
6442
+ if (!res.ok) {
6443
+ const detail = await res.text().catch(() => "");
6444
+ log.error(`Couldn't restore (HTTP ${res.status})${detail ? `: ${detail}` : ""}.`);
6445
+ return null;
6446
+ }
6447
+ const body = await res.json().catch(() => null);
6448
+ if (!body?.commit) {
6449
+ log.error("The API didn't confirm which version went live.");
6450
+ return null;
6451
+ }
6452
+ return { commit: body.commit, url: body.url ?? "" };
6453
+ }
6454
+
6455
+ // src/commands/rollback.ts
6456
+ function when(iso) {
6457
+ if (!iso) return "at an unrecorded time";
6458
+ const ms = Date.now() - new Date(iso).getTime();
6459
+ if (!Number.isFinite(ms) || ms < 0) return "recently";
6460
+ const mins = Math.round(ms / 6e4);
6461
+ if (mins < 60) return `${mins} minute${mins === 1 ? "" : "s"} ago`;
6462
+ const hours = Math.round(mins / 60);
6463
+ if (hours < 48) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
6464
+ return `${Math.round(hours / 24)} days ago`;
6465
+ }
6466
+ async function runRollback(opts) {
6467
+ const log = createLogger({ quiet: opts.quiet });
6468
+ log.plain(c.bold("genex rollback"));
6469
+ log.plain("");
6470
+ if (inHostedSession()) {
6471
+ log.error("`genex rollback` is not available inside a Genex chat session.");
6472
+ log.dim(" It changes the version players are on, which is not yours to decide here.");
6473
+ log.dim(" Tell the user they can do it themselves in their game's workspace:");
6474
+ log.dim(" the gear menu (top right) \u2192 Restore previous version\u2026");
6475
+ log.dim(` Or, with the game checked out locally: ${c.cyan("npx genex rollback")}`);
6476
+ process.exitCode = 1;
6477
+ return;
6478
+ }
6479
+ const meta = await readProject();
6480
+ if (!meta) {
6481
+ log.error("No genex project here. Run `genex init` in this directory first.");
6482
+ process.exitCode = 1;
6483
+ return;
6484
+ }
6485
+ const token = await readUserToken(opts.envPath);
6486
+ if (!token) {
6487
+ log.error("Not authorized. Run `genex init` first to sign in.");
6488
+ process.exitCode = 1;
6489
+ return;
6490
+ }
6491
+ const apiUrl = getApiUrl(meta.apiUrl);
6492
+ const previous = await readPreviousVersion(apiUrl, meta.id, token, log);
6493
+ if (!previous) {
6494
+ process.exitCode = 1;
6495
+ return;
6496
+ }
6497
+ if (!opts.yes) {
6498
+ log.warn(`This puts players back on the version from ${when(previous.deployedAt)}.`);
6499
+ log.dim(` live now: ${previous.currentCommit.slice(0, 7)}`);
6500
+ log.dim(` goes back to: ${previous.commit.slice(0, 7)}`);
6501
+ log.dim(" Your code is NOT changed \u2014 this folder, and what a Remix copies, stay as they are.");
6502
+ log.dim(" Re-run with --yes to do it.");
6503
+ process.exitCode = 1;
6504
+ return;
6505
+ }
6506
+ log.step("Putting the previous version back\u2026");
6507
+ const result = await restorePreviousVersion(apiUrl, meta.id, token, previous.currentCommit, log);
6508
+ if (!result) {
6509
+ process.exitCode = 1;
6510
+ return;
6511
+ }
6512
+ log.plain("");
6513
+ log.success("Done \u2014 players are back on the previous version.");
6514
+ if (result.url) log.plain(` play: ${c.cyan(result.url)}`);
6515
+ log.dim(" Your code is unchanged: this folder still holds the newer version.");
6516
+ log.dim(` To ship it once it is fixed: ${c.cyan("npx genex preview")} then ${c.cyan("npx genex promote")}`);
6517
+ log.dim(` To undo this and go forward again: ${c.cyan("npx genex rollback")}`);
6518
+ }
6519
+
6350
6520
  // src/commands/generate.ts
6351
6521
  import fs20 from "fs/promises";
6352
6522
  import path19 from "path";
@@ -20094,6 +20264,9 @@ ${c.bold("Usage")}
20094
20264
  genex promote [options] Make the previewed build live for players. No
20095
20265
  rebuild \u2014 the exact build you previewed.
20096
20266
  genex publish [options] Build + push + make live, then list it in the gallery.
20267
+ genex rollback [--yes] Put players back on the previous version. One
20268
+ version back, instantly \u2014 your code is not
20269
+ changed, only what the play URL serves.
20097
20270
  genex make-remixable [options] Make THIS game remixable by everyone \u2014 migrates a
20098
20271
  private source onto a public managed genex repo.
20099
20272
  genex domain <sub> [host] Play this game on a domain you own:
@@ -21106,6 +21279,9 @@ async function main() {
21106
21279
  case "promote":
21107
21280
  await runPromote(parsed.options);
21108
21281
  break;
21282
+ case "rollback":
21283
+ await runRollback(parsed.options);
21284
+ break;
21109
21285
  case "publish":
21110
21286
  await runPublish(parsed.options);
21111
21287
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "1.7.3-dev.474",
3
+ "version": "1.8.0-dev.476",
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": {