@genex-ai/cli-demo 1.7.3-dev.475 → 1.8.0-dev.477
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
|
@@ -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";
|
|
@@ -19833,6 +20003,7 @@ import path24 from "path";
|
|
|
19833
20003
|
import { pathToFileURL } from "url";
|
|
19834
20004
|
var EXTRA_FILES = [
|
|
19835
20005
|
"genex-asset.example.json",
|
|
20006
|
+
"public/fonts/ZalandoSansSemiExpanded-variable.woff2",
|
|
19836
20007
|
"public/fonts/Geist-variable.woff2",
|
|
19837
20008
|
"public/fonts/GeistMono-variable.woff2"
|
|
19838
20009
|
];
|
|
@@ -20094,6 +20265,9 @@ ${c.bold("Usage")}
|
|
|
20094
20265
|
genex promote [options] Make the previewed build live for players. No
|
|
20095
20266
|
rebuild \u2014 the exact build you previewed.
|
|
20096
20267
|
genex publish [options] Build + push + make live, then list it in the gallery.
|
|
20268
|
+
genex rollback [--yes] Put players back on the previous version. One
|
|
20269
|
+
version back, instantly \u2014 your code is not
|
|
20270
|
+
changed, only what the play URL serves.
|
|
20097
20271
|
genex make-remixable [options] Make THIS game remixable by everyone \u2014 migrates a
|
|
20098
20272
|
private source onto a public managed genex repo.
|
|
20099
20273
|
genex domain <sub> [host] Play this game on a domain you own:
|
|
@@ -21106,6 +21280,9 @@ async function main() {
|
|
|
21106
21280
|
case "promote":
|
|
21107
21281
|
await runPromote(parsed.options);
|
|
21108
21282
|
break;
|
|
21283
|
+
case "rollback":
|
|
21284
|
+
await runRollback(parsed.options);
|
|
21285
|
+
break;
|
|
21109
21286
|
case "publish":
|
|
21110
21287
|
await runPublish(parsed.options);
|
|
21111
21288
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genex-ai/cli-demo",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0-dev.477",
|
|
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": {
|
|
@@ -13,12 +13,23 @@
|
|
|
13
13
|
has to reach a third party for its typeface renders in the fallback for
|
|
14
14
|
the first second on every cold load, and renders in the fallback
|
|
15
15
|
forever the day that host is blocked. */
|
|
16
|
+
@font-face {
|
|
17
|
+
font-family: "Zalando Sans SemiExpanded";
|
|
18
|
+
src: url("/fonts/ZalandoSansSemiExpanded-variable.woff2") format("woff2");
|
|
19
|
+
/* The file's REAL wght axis is 200-900; claiming 100 silently clamps. */
|
|
20
|
+
font-weight: 200 900;
|
|
21
|
+
font-style: normal;
|
|
22
|
+
font-display: block;
|
|
23
|
+
}
|
|
24
|
+
/* Cyrillic carrier: the sans ships none, and `unicode-range` means the
|
|
25
|
+
file is fetched only when there is Cyrillic on screen. */
|
|
16
26
|
@font-face {
|
|
17
27
|
font-family: "Geist";
|
|
18
28
|
src: url("/fonts/Geist-variable.woff2") format("woff2");
|
|
19
29
|
font-weight: 100 900;
|
|
20
30
|
font-style: normal;
|
|
21
31
|
font-display: block;
|
|
32
|
+
unicode-range: U+0301, U+0370-03FF, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
|
22
33
|
}
|
|
23
34
|
@font-face {
|
|
24
35
|
font-family: "Geist Mono";
|
|
@@ -37,7 +48,14 @@
|
|
|
37
48
|
--ink: #f2f5f9;
|
|
38
49
|
--ink-dim: #9aa4b3;
|
|
39
50
|
--accent: #8fb6ff;
|
|
40
|
-
--sans:
|
|
51
|
+
--sans:
|
|
52
|
+
"Zalando Sans SemiExpanded", "Geist", ui-sans-serif, system-ui, -apple-system,
|
|
53
|
+
sans-serif;
|
|
54
|
+
/* The sans is a SEMI-EXPANDED face; -0.03em is the family's own
|
|
55
|
+
tracking, the same decision apps/web's `font-sans` utility makes -
|
|
56
|
+
an em rather than a px because a family value has no size of its own,
|
|
57
|
+
so a px is right at exactly one. */
|
|
58
|
+
--tracking-sans: -0.03em;
|
|
41
59
|
--mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
42
60
|
/* Nothing sits on a plate any more, so every line is read directly off
|
|
43
61
|
the render. One soft drop shadow is what keeps it legible when the
|
|
@@ -56,6 +74,9 @@
|
|
|
56
74
|
background: var(--bg);
|
|
57
75
|
color: var(--ink);
|
|
58
76
|
font: 400 13px/1.45 var(--sans);
|
|
77
|
+
/* After the shorthand, which resets letter-spacing to normal. #hud-name
|
|
78
|
+
states its own and still wins. */
|
|
79
|
+
letter-spacing: var(--tracking-sans);
|
|
59
80
|
-webkit-font-smoothing: antialiased;
|
|
60
81
|
-moz-osx-font-smoothing: grayscale;
|
|
61
82
|
}
|
|
Binary file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"templateVersion": "1.0.0",
|
|
3
3
|
"files": {
|
|
4
|
-
"index.html": "
|
|
4
|
+
"index.html": "3e213243f886ec03fd6de48d7eeac65b4406b2a2114fa4b2ccf79d7462fabfbc",
|
|
5
5
|
"vite.config.js": "255f13f0e8c202aa7c11d6e67397b9079ed771d4d2b3dd4771ce7fd2ae0d13e7",
|
|
6
6
|
"src/main.js": "ac7e2c4da2f0bac605425b7146580d59eb7f93a029ea6c5bfc7c6e1697fb0f72",
|
|
7
7
|
"src/viewer/stage.js": "dae8f765379d45a9836df901466d7376b94ae1a19107c5dcd611ff80fc09b75c",
|