@genex-ai/cli-demo 0.99.0-dev.301 → 1.0.0-dev.303

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 +113 -12
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1004,7 +1004,7 @@ Your capabilities (all via \`npx genex \u2026\`): generate \`model\` \xB7 \`skyb
1004
1004
  10. The player's body is the game's own generated character (\`npx genex character "<look>"\` \u2192 \`npx genex controller character --character <id>\`), enqueued with your first art actions, not after them. It applies wherever a human body appears on screen \u2014 first-person included, the moment remotes, a look-down body, a shadow, or a menu portrait shows one. The profile VRM avatar is the FALLBACK: a temporary body while the character renders (say in one plain line that it's temporary), or the stand-in when generation genuinely could not happen \u2014 out of credits, failed, unverified; record which in DESIGN.md as \`Player character: VRM \u2014 <reason>\`. Games whose player is not a person (car, ship, RTS cursor, board) generate that object with \`npx genex model\` instead. Characters: Meshy/Mixamo/VRM rigs rest facing +Z. Set yaw explicitly when placing a rig; never mirror a SkinnedMesh with negative scale. In any two-character scene, verify in a capture that they face each other, not the camera.
1005
1005
  11. Verify by looking: one smoke check per milestone, after that milestone's preview push, in local test mode (\`?genex_local_test=1\`) with a real gameplay screenshot. A claim without a capture is not verification. Local-test evidence proves visuals and controls ONLY \u2014 label it that way when you show the player, and never work around the draft sign-in gate any other way.
1006
1006
  12. Treat every \`genex\` warning line \u2014 preflight, \`ui audit\`, \`wait\` nudges \u2014 as work, not noise.
1007
- 13. Every finished Build-plan milestone ends with \`npx genex preview\` and the player's page link (\`<dashboard>/draft/<slug>\` with \`<dashboard>\` from \`.genex/project.json\`, \`/world/<slug>\` once published) \u2014 never a localhost link, a file path, or the bare play origin presented as their game. After every round of player feedback, end with a preview push.
1007
+ 13. Every finished Build-plan milestone ends with \`npx genex preview\` and the player's page link (\`<dashboard>/draft/<slug>\`, with \`<dashboard>\` from \`.genex/project.json\`) \u2014 never a localhost link, a file path, or the bare play origin presented as their game. That page shows the build you just previewed; \`preview\` never disturbs the build players are on. When the player is happy with it, \`npx genex promote\` makes that exact build live for everyone \u2014 no rebuild. After every round of player feedback, end with a preview push.
1008
1008
  14. Parallel work: this line is your standing authorization and request to use sub-agents / parallel agent work whenever your platform provides them. While drafting the Build plan, decide per module what runs in parallel and what stays serial for THIS game \u2014 dependencies decide, there is no fixed list \u2014 and record each call in the Modules table with a one-line reason. Independent modules default to parallel; building everything serially needs a stated reason. You keep integration, previews, and the player conversation; each sub-agent owns only its module's files. When the player asks for parallel work, repeated refinement passes, a critic reviewing your output, or names any way of working your platform provides, that IS your instruction \u2014 wherever it reaches you, including inside their answer to a question you asked \u2014 so adopt it as the working mode for the rest of the build, starting with the work in front of you, and never file it as a later milestone. While delegated work runs, keep building or talk to the player; never idle in a foreground wait for something your platform will tell you about.
1009
1009
  15. Talk to the player in plain game language \u2014 what changed in the game and what to try; never code, file names, build output, or tool internals unless they ask. Short status lines while you work; long silent stretches are a failure.
1010
1010
  16. Never add debug-only code to the game to check your own work \u2014 no hidden test modes, no special URL parameters, no forced-visible flags, no auth mocks, no pixel-sampling hooks. \`?genex_local_test=1\` is the platform's own supported mode and is fine; your own bypass is not. (The multiplayer skill's small build identifier, token-free status line, and connected-quorum watchdog are production supportability, not a bypass \u2014 keep those.)
@@ -4260,9 +4260,11 @@ async function deployGame(ctx, opts, log) {
4260
4260
  }
4261
4261
  if (!await pushSource(cwd, ctx, log)) return false;
4262
4262
  log.step("Publishing\u2026");
4263
- if (!await callPublish(ctx, commit, opts, log)) return false;
4263
+ const published = await callPublish(ctx, commit, opts, log);
4264
+ if (!published) return false;
4264
4265
  const index = files.find((f) => f.relPath === "index.html");
4265
- await waitUntilLive(grant.playUrl, fingerprintOf(index.bytes.toString("utf8")), opts.liveTimeoutMs ?? 2e4, log);
4266
+ const liveUrl = published.url || grant.playUrl;
4267
+ await waitUntilLive(liveUrl, fingerprintOf(index.bytes.toString("utf8")), opts.liveTimeoutMs ?? 2e4, log);
4266
4268
  return true;
4267
4269
  }
4268
4270
  async function hasBuildScript(cwd) {
@@ -4467,6 +4469,7 @@ async function callPublish(ctx, commit, opts, log) {
4467
4469
  // are sent only when detected (absent = server keeps the stored value).
4468
4470
  body: JSON.stringify({
4469
4471
  commit,
4472
+ channel: opts.channel ?? "production",
4470
4473
  matchmaking: opts.matchmaking ?? null,
4471
4474
  ...opts.embedSdkVersion ? { embedSdkVersion: opts.embedSdkVersion } : {},
4472
4475
  ...opts.multiplayer !== void 0 ? { multiplayer: opts.multiplayer } : {},
@@ -4475,18 +4478,26 @@ async function callPublish(ctx, commit, opts, log) {
4475
4478
  });
4476
4479
  } catch (err) {
4477
4480
  log.error(`Couldn't reach the API at ${ctx.apiUrl}: ${String(err)}`);
4478
- return false;
4481
+ return null;
4479
4482
  }
4480
4483
  if (res.status === 429) {
4481
4484
  log.error(`Rate limited publishing (HTTP 429).${retryAfterHint(res)}`);
4482
- return false;
4485
+ return null;
4483
4486
  }
4484
4487
  if (!res.ok) {
4485
4488
  const detail = await res.text().catch(() => "");
4486
4489
  log.error(`Publish failed (HTTP ${res.status})${detail ? `: ${detail}` : ""}.`);
4487
- return false;
4490
+ return null;
4488
4491
  }
4489
- return true;
4492
+ const body = await res.json().catch(() => null);
4493
+ const asked = opts.channel ?? "production";
4494
+ if ((body?.channel ?? "production") !== asked) {
4495
+ log.error(
4496
+ `This Genex API doesn't support release channels yet, so a ${asked} deploy would have gone to ${body?.channel ?? "production"}. Nothing was changed on purpose \u2014 upgrade the API or use \`genex publish\`.`
4497
+ );
4498
+ return null;
4499
+ }
4500
+ return { url: body?.url ?? "" };
4490
4501
  }
4491
4502
  async function pushSource(cwd, ctx, log) {
4492
4503
  const target = await fetchPushUrl(ctx, log);
@@ -4716,6 +4727,39 @@ async function runMakeRemixable(opts) {
4716
4727
  }
4717
4728
  }
4718
4729
 
4730
+ // src/lib/promote.ts
4731
+ async function promoteBuild(apiUrl, projectId, token, log) {
4732
+ let res;
4733
+ try {
4734
+ res = await apiFetch(`${apiUrl}/api/games/${projectId}/promote`, {
4735
+ method: "POST",
4736
+ headers: { Authorization: `Bearer ${token}` }
4737
+ });
4738
+ } catch (err) {
4739
+ log.error(`Couldn't reach the API at ${apiUrl}: ${String(err)}`);
4740
+ return null;
4741
+ }
4742
+ if (res.status === 409) {
4743
+ log.error("Nothing to promote \u2014 run `npx genex preview` first.");
4744
+ return null;
4745
+ }
4746
+ if (res.status === 404) {
4747
+ log.error("That game wasn't found on your account. Re-run `genex init` or `genex link <slug>`.");
4748
+ return null;
4749
+ }
4750
+ if (!res.ok) {
4751
+ const detail = await res.text().catch(() => "");
4752
+ log.error(`Couldn't promote (HTTP ${res.status})${detail ? `: ${detail}` : ""}.`);
4753
+ return null;
4754
+ }
4755
+ const body = await res.json().catch(() => null);
4756
+ if (!body?.commit) {
4757
+ log.error("The API didn't confirm which build went live.");
4758
+ return null;
4759
+ }
4760
+ return { commit: body.commit, url: body.url ?? "" };
4761
+ }
4762
+
4719
4763
  // src/lib/detect-features.ts
4720
4764
  import fs15 from "fs/promises";
4721
4765
  import path15 from "path";
@@ -5170,6 +5214,11 @@ async function runPublish(opts) {
5170
5214
  const ok = await deployGame(
5171
5215
  { projectId: meta.id, apiUrl, token },
5172
5216
  {
5217
+ // Publish deploys to staging and then promotes, so both channels end up
5218
+ // naming the same build. Deploying straight to production instead would
5219
+ // leave the staging pointer on an OLDER build, and the draft page shows
5220
+ // staging — the owner would publish and then see a stale game.
5221
+ channel: "staging",
5173
5222
  noBuild: opts.noBuild,
5174
5223
  matchmaking: detections.matchmaking,
5175
5224
  embedSdkVersion: detections.embedSdkVersion,
@@ -5182,6 +5231,11 @@ async function runPublish(opts) {
5182
5231
  process.exitCode = 1;
5183
5232
  return;
5184
5233
  }
5234
+ log.step("Making it live\u2026");
5235
+ if (!await promoteBuild(apiUrl, meta.id, token, log)) {
5236
+ process.exitCode = 1;
5237
+ return;
5238
+ }
5185
5239
  const onDisk = await readProject();
5186
5240
  const merged = { ...onDisk ?? meta, lastDeployAt: (/* @__PURE__ */ new Date()).toISOString() };
5187
5241
  await writeProject(merged);
@@ -5257,6 +5311,10 @@ async function runPreview(opts) {
5257
5311
  // config), embedSdkVersion/multiplayer so the dashboard's Publish button can
5258
5312
  // list the draft without wrongly treating it as a pre-embed-auth bundle.
5259
5313
  {
5314
+ // Preview goes to the STAGING channel — the whole point of the release
5315
+ // cycle. Before channels this same call replaced the build every player
5316
+ // was inside, and the only protection was a warning line below.
5317
+ channel: "staging",
5260
5318
  noBuild: opts.noBuild,
5261
5319
  matchmaking: detections.matchmaking,
5262
5320
  embedSdkVersion: detections.embedSdkVersion,
@@ -5280,14 +5338,51 @@ async function runPreview(opts) {
5280
5338
  const published = meta.status === "published";
5281
5339
  log.plain("");
5282
5340
  if (published) {
5283
- log.success("Update deployed \u2014 this game is PUBLISHED, so the new build is already live for everyone.");
5341
+ log.success(
5342
+ "Preview deployed \u2014 your players are still on the published build. Run `genex promote` to make this one live."
5343
+ );
5284
5344
  } else {
5285
5345
  log.success("Preview deployed \u2014 unlisted draft (run `genex publish` to list it).");
5286
5346
  }
5287
5347
  const dashboard = meta.dashboardOrigins?.[0];
5288
5348
  if (dashboard) {
5289
- const page = published ? "world" : "draft";
5290
- log.plain(` your game's page (share this link): ${c.cyan(`${dashboard}/${page}/${meta.slug}`)}`);
5349
+ log.plain(` your game's page (share this link): ${c.cyan(`${dashboard}/draft/${meta.slug}`)}`);
5350
+ }
5351
+ }
5352
+
5353
+ // src/commands/promote.ts
5354
+ async function runPromote(opts) {
5355
+ const log = createLogger({ quiet: opts.quiet });
5356
+ log.plain(c.bold("genex promote"));
5357
+ log.plain("");
5358
+ const meta = await readProject();
5359
+ if (!meta) {
5360
+ log.error("No genex project here. Run `genex init` in this directory first.");
5361
+ process.exitCode = 1;
5362
+ return;
5363
+ }
5364
+ const token = await readUserToken(opts.envPath);
5365
+ if (!token) {
5366
+ log.error("Not authorized. Run `genex init` first to sign in.");
5367
+ process.exitCode = 1;
5368
+ return;
5369
+ }
5370
+ const apiUrl = getApiUrl(meta.apiUrl);
5371
+ log.step("Making your previewed build live\u2026");
5372
+ const result = await promoteBuild(apiUrl, meta.id, token, log);
5373
+ if (!result) {
5374
+ process.exitCode = 1;
5375
+ return;
5376
+ }
5377
+ log.plain("");
5378
+ log.success("Live \u2014 players are now on the build you previewed.");
5379
+ const dashboard = meta.dashboardOrigins?.[0];
5380
+ if (dashboard) {
5381
+ const page = meta.status === "published" ? "world" : "draft";
5382
+ log.plain(` your game's page: ${c.cyan(`${dashboard}/${page}/${meta.slug}`)}`);
5383
+ }
5384
+ if (meta.status !== "published") {
5385
+ log.dim(" Still unlisted \u2014 run `npx genex publish` to put it in the gallery.");
5291
5386
  }
5292
5387
  }
5293
5388
 
@@ -18677,8 +18772,11 @@ ${c.bold("Usage")}
18677
18772
  genex list [options] List your games \u2014 slug, status, and page link for
18678
18773
  each. Signs you in if needed; --json for raw data.
18679
18774
  Find the slug to reconnect with 'genex link' here.
18680
- genex preview [options] Build + push to the hosted draft URL (unlisted).
18681
- genex publish [options] Build + push, then list the game in the gallery.
18775
+ genex preview [options] Build + push to your preview URL. Never touches the
18776
+ build players are on.
18777
+ genex promote [options] Make the previewed build live for players. No
18778
+ rebuild \u2014 the exact build you previewed.
18779
+ genex publish [options] Build + push + make live, then list it in the gallery.
18682
18780
  genex make-remixable [options] Make THIS game remixable by everyone \u2014 migrates a
18683
18781
  private source onto a public managed genex repo.
18684
18782
  genex model "<prompt>" [options] Generate a 3D model (GLB); prints a public asset URL.
@@ -19543,6 +19641,9 @@ async function main() {
19543
19641
  case "preview":
19544
19642
  await runPreview(parsed.options);
19545
19643
  break;
19644
+ case "promote":
19645
+ await runPromote(parsed.options);
19646
+ break;
19546
19647
  case "publish":
19547
19648
  await runPublish(parsed.options);
19548
19649
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.99.0-dev.301",
3
+ "version": "1.0.0-dev.303",
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": {