@genex-ai/cli-demo 0.66.0-dev.163 → 0.67.0-dev.167

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/README.md CHANGED
@@ -22,6 +22,7 @@ genex character finalize <preview-id> --user-approved --approve-remesh 10000
22
22
  genex character animate <id> --action <action-id> # add a same-rig Meshy action
23
23
  genex animations search "<intent>" --json # search the committed Meshy catalog locally
24
24
  genex wait <id> # attach to a --no-wait generation and print its URL(s) when done
25
+ genex wait --all # one status line per generation this project enqueued — never blocks, never bills
25
26
  genex controller <type> # character|car|drone|touch|networked-physics → src/controllers/ (touch = the standalone mobile-input kit)
26
27
  genex controller character --character <id> # install the Meshy-native adapter + manifest
27
28
  genex controller anims <sel…> # download extra character animation clips (by tag or name) → public/assets/anims/
package/dist/index.js CHANGED
@@ -2156,7 +2156,7 @@ var UNPICKED_AFTER_MS = 15 * 60 * 1e3;
2156
2156
  async function detectGenerationAudit(cwd = process.cwd()) {
2157
2157
  const ledger = await readLedger(cwd);
2158
2158
  const audited = ledger.filter((e) => WIRED_BY_URL.has(e.kind));
2159
- if (audited.length === 0) return { unwired: [], unpicked: [] };
2159
+ if (audited.length === 0) return { unwired: [], unpicked: [], unresolved: [], deadWired: [] };
2160
2160
  let haystack = "";
2161
2161
  const read = async (file) => {
2162
2162
  try {
@@ -2189,14 +2189,21 @@ async function detectGenerationAudit(cwd = process.cwd()) {
2189
2189
  prompt
2190
2190
  });
2191
2191
  const now = Date.now();
2192
+ const stale = (e) => e.status === "queued" && now - Date.parse(e.queuedAt) > UNPICKED_AFTER_MS;
2192
2193
  return {
2193
2194
  unwired: audited.filter((e) => e.status === "completed" && !haystack.includes(e.id)).map(ref),
2194
- // An id already referenced in the game was obviously picked up — a stale
2195
- // "queued" record (e.g. `genex wait` ran from another directory) must not
2196
- // nag about an asset that's wired in and working.
2197
- unpicked: audited.filter(
2198
- (e) => e.status === "queued" && !haystack.includes(e.id) && now - Date.parse(e.queuedAt) > UNPICKED_AFTER_MS
2199
- ).map(ref)
2195
+ unpicked: audited.filter((e) => stale(e) && !haystack.includes(e.id)).map(ref),
2196
+ // Wired in but never picked up: the deterministic URL let the agent wire
2197
+ // the asset before it finished, so nobody ever learned whether it DID
2198
+ // finish — a provider-side failure (credits, moderation) leaves a dead URL
2199
+ // that renders as a silently missing asset. Kept separate from `unpicked`
2200
+ // because the fix is a status check, not wiring.
2201
+ unresolved: audited.filter((e) => stale(e) && haystack.includes(e.id)).map(ref),
2202
+ // Known-failed AND still wired: the worst case, and it must keep firing
2203
+ // AFTER the status check converges the ledger to "failed" (otherwise the
2204
+ // audit self-erases the moment the agent runs the recommended command and
2205
+ // the dead URL ships silently anyway).
2206
+ deadWired: audited.filter((e) => e.status === "failed" && haystack.includes(e.id)).map(ref)
2200
2207
  };
2201
2208
  }
2202
2209
  async function detectFeatures(log, cwd = process.cwd()) {
@@ -2238,7 +2245,17 @@ function generationNudges(log, g) {
2238
2245
  const describe = (refs) => refs.map((r) => `${r.kind} ${r.id} ("${r.prompt.slice(0, 48)}")`).join("; ");
2239
2246
  if (g.unpicked.length) {
2240
2247
  log.warn(
2241
- `${g.unpicked.length} generation(s) enqueued 15+ minutes ago were never picked up: ${describe(g.unpicked)}. Run npx genex wait <id> on each \u2014 landed assets get wired in before the last push of a session, never parked for "later".`
2248
+ `${g.unpicked.length} generation(s) enqueued 15+ minutes ago were never picked up: ${describe(g.unpicked)}. Run npx genex wait <id> on each (npx genex wait --all shows every status at once) \u2014 landed assets get wired in before the last push of a session, never parked for "later".`
2249
+ );
2250
+ }
2251
+ if (g.unresolved.length) {
2252
+ log.warn(
2253
+ `${g.unresolved.length} generation(s) are wired into the game but were never picked up, so their real status is unknown: ${describe(g.unresolved)}. A generation that failed server-side (credits, moderation) leaves a dead URL \u2014 the asset just silently never appears. One call checks them all: npx genex wait --all.`
2254
+ );
2255
+ }
2256
+ if (g.deadWired.length) {
2257
+ log.warn(
2258
+ `${g.deadWired.length} FAILED generation(s) are still wired into the game: ${describe(g.deadWired)}. Those URLs will never exist \u2014 each asset silently never appears. Replace the wiring or re-generate (that bills a new one), and tell the user what changed.`
2242
2259
  );
2243
2260
  }
2244
2261
  if (g.unwired.length) {
@@ -2273,6 +2290,43 @@ async function firstPreviewNudge(log, cwd = process.cwd()) {
2273
2290
  " \u279C Draft page still the placeholder? Ship the first rough build now: npx genex preview \u2014 then keep building."
2274
2291
  );
2275
2292
  }
2293
+ async function exploreReuseNudge(log, meta, cwd = process.cwd()) {
2294
+ if (!meta.exploreRanAt || meta.reuseNudgedAt) return;
2295
+ if (await borrowEvidence(meta, cwd)) return;
2296
+ log.warn(
2297
+ "This project browsed the curated catalog (genex explore) but no borrowed catalog project is visible in the game. If the plan mapped systems to the catalog \u2014 grass, terrain, water, vehicles \u2014 land those borrows or cancel each in one plain line to the user: a planned borrow that silently becomes hand-rolled code is a scope cut. The genex-explore skill has the borrow steps."
2298
+ );
2299
+ meta.reuseNudgedAt = (/* @__PURE__ */ new Date()).toISOString();
2300
+ try {
2301
+ const onDisk = await readProject(cwd);
2302
+ if (onDisk) {
2303
+ onDisk.reuseNudgedAt = meta.reuseNudgedAt;
2304
+ await writeProject(onDisk, cwd);
2305
+ }
2306
+ } catch {
2307
+ }
2308
+ }
2309
+ async function borrowEvidence(meta, cwd) {
2310
+ let host;
2311
+ let ownPath;
2312
+ try {
2313
+ const own = new URL(meta.cloneUrl);
2314
+ host = own.host;
2315
+ ownPath = own.pathname;
2316
+ } catch {
2317
+ return true;
2318
+ }
2319
+ const gitConfig = await fs11.readFile(path12.join(cwd, ".git", "config"), "utf8").catch(() => "");
2320
+ for (const m of gitConfig.matchAll(/url\s*=\s*(\S+)/g)) {
2321
+ try {
2322
+ const u = new URL(m[1]);
2323
+ if (u.host === host && u.pathname !== ownPath) return true;
2324
+ } catch {
2325
+ }
2326
+ }
2327
+ const readme = await fs11.readFile(path12.join(cwd, "README.md"), "utf8").catch(() => "");
2328
+ return readme.includes(host) || /\b(upstream|originally by|ported from|borrowed from)\b/i.test(readme);
2329
+ }
2276
2330
 
2277
2331
  // src/commands/publish.ts
2278
2332
  async function runPublish(opts) {
@@ -2309,8 +2363,10 @@ async function runPublish(opts) {
2309
2363
  process.exitCode = 1;
2310
2364
  return;
2311
2365
  }
2312
- meta.lastDeployAt = (/* @__PURE__ */ new Date()).toISOString();
2313
- await writeProject(meta);
2366
+ const onDisk = await readProject();
2367
+ const merged = { ...onDisk ?? meta, lastDeployAt: (/* @__PURE__ */ new Date()).toISOString() };
2368
+ await writeProject(merged);
2369
+ meta.lastDeployAt = merged.lastDeployAt;
2314
2370
  } else {
2315
2371
  log.info("Skipping build + deploy (--no-push).");
2316
2372
  }
@@ -2372,6 +2428,7 @@ async function runPreview(opts) {
2372
2428
  }
2373
2429
  const detections = await detectFeatures(log);
2374
2430
  advisoryNudges(log, detections);
2431
+ await exploreReuseNudge(log, meta);
2375
2432
  const apiUrl = getApiUrl(meta.apiUrl);
2376
2433
  const ok = await deployGame(
2377
2434
  { projectId: meta.id, apiUrl, token },
@@ -2395,8 +2452,10 @@ async function runPreview(opts) {
2395
2452
  if (freshStatus && freshStatus !== meta.status) {
2396
2453
  meta.status = freshStatus;
2397
2454
  }
2398
- meta.lastDeployAt = (/* @__PURE__ */ new Date()).toISOString();
2399
- await writeProject(meta);
2455
+ const onDisk = await readProject();
2456
+ const merged = { ...onDisk ?? meta, status: meta.status, lastDeployAt: (/* @__PURE__ */ new Date()).toISOString() };
2457
+ await writeProject(merged);
2458
+ meta.lastDeployAt = merged.lastDeployAt;
2400
2459
  const published = meta.status === "published";
2401
2460
  log.plain("");
2402
2461
  if (published) {
@@ -3040,11 +3099,12 @@ function writeJson(value) {
3040
3099
  // src/commands/wait.ts
3041
3100
  var TERMINAL2 = /* @__PURE__ */ new Set(["completed", "failed"]);
3042
3101
  async function runWait(opts) {
3102
+ if (opts.all) return runWaitAll(opts);
3043
3103
  const log = createLogger({ quiet: opts.quiet || opts.json });
3044
3104
  const id = opts.id?.trim();
3045
3105
  if (!id) {
3046
3106
  log.error(
3047
- `Missing generation id. Usage: ${c.cyan("genex wait <id>")} \u2014 the id printed when you enqueued with --no-wait.`
3107
+ `Missing generation id. Usage: ${c.cyan("genex wait <id>")} \u2014 the id printed when you enqueued with --no-wait. For one status line per generation this project enqueued: ${c.cyan("genex wait --all")}.`
3048
3108
  );
3049
3109
  process.exitCode = 1;
3050
3110
  return;
@@ -3093,6 +3153,109 @@ async function runWait(opts) {
3093
3153
  }
3094
3154
  await awaitAndReport(apiUrl, token, id, kind, log, opts.open, opts.json);
3095
3155
  }
3156
+ async function runWaitAll(opts) {
3157
+ const log = createLogger({ quiet: opts.quiet || opts.json });
3158
+ const cwd = opts.cwd ?? process.cwd();
3159
+ const ledger = await readLedger(cwd);
3160
+ if (ledger.length === 0) {
3161
+ log.plain(
3162
+ "No generations recorded for this project yet \u2014 enqueue one (any genex model/image/\u2026 command) and its status shows up here."
3163
+ );
3164
+ return;
3165
+ }
3166
+ const token = opts.token ?? await readUserToken(opts.envPath);
3167
+ if (!token) {
3168
+ log.error("Not authorized. Run `genex init` first to sign in.");
3169
+ process.exitCode = 1;
3170
+ return;
3171
+ }
3172
+ const meta = await readProject(cwd);
3173
+ const apiUrl = getApiUrl(opts.apiUrl ?? meta?.apiUrl);
3174
+ const views = /* @__PURE__ */ new Map();
3175
+ try {
3176
+ const res = await apiFetch(`${apiUrl}/api/generations`, {
3177
+ headers: { Authorization: `Bearer ${token}` }
3178
+ });
3179
+ if (res.status === 401) {
3180
+ log.error("Unauthorized \u2014 your token may have expired. Re-run `genex init`.");
3181
+ process.exitCode = 1;
3182
+ return;
3183
+ }
3184
+ if (res.ok) {
3185
+ const { generations } = await res.json();
3186
+ for (const v of generations) views.set(v.id, v);
3187
+ }
3188
+ } catch {
3189
+ log.dim(" (couldn't reach the API \u2014 showing the local ledger's last known state)");
3190
+ }
3191
+ for (const e of ledger) {
3192
+ if (e.status !== "queued" || views.has(e.id)) continue;
3193
+ try {
3194
+ const res = await apiFetch(`${apiUrl}/api/generations/${e.id}`, {
3195
+ headers: { Authorization: `Bearer ${token}` }
3196
+ });
3197
+ if (res.ok) {
3198
+ const { generation } = await res.json();
3199
+ views.set(e.id, generation);
3200
+ }
3201
+ } catch {
3202
+ }
3203
+ }
3204
+ const rows = [];
3205
+ for (const e of ledger) {
3206
+ rows.push(await toRow(e, views.get(e.id), cwd));
3207
+ }
3208
+ if (opts.json) {
3209
+ writeJson({ generations: rows });
3210
+ return;
3211
+ }
3212
+ log.plain(c.bold("genex wait --all"));
3213
+ log.plain("");
3214
+ for (const r of rows) {
3215
+ const mark = r.status === "done" ? c.green("\u2713") : r.status === "failed" ? c.red("\u2717") : c.yellow("\u2026");
3216
+ const label = r.status === "running" && typeof r.progress === "number" ? `running ${r.progress}%` : r.status;
3217
+ log.plain(` ${mark} ${label.padEnd(12)} ${r.kind.padEnd(8)} ${r.id} ${c.dim(`"${r.prompt.slice(0, 56)}"`)}`);
3218
+ if (r.status === "failed" && r.error) log.plain(` ${c.red(r.error)}`);
3219
+ }
3220
+ const count = (s) => rows.filter((r) => r.status === s).length;
3221
+ log.plain("");
3222
+ log.plain(
3223
+ ` ${count("done")} done \xB7 ${count("running")} running \xB7 ${count("queued")} queued \xB7 ${count("failed")} failed`
3224
+ );
3225
+ if (count("failed") > 0) {
3226
+ log.dim(
3227
+ " A failed generation never produces its URL \u2014 anything wired to it silently won't appear. Re-plan or re-generate (that bills a new one)."
3228
+ );
3229
+ }
3230
+ if (count("running") + count("queued") > 0) {
3231
+ log.dim(` Attach to any single one with: ${c.cyan("npx genex wait <id>")}`);
3232
+ }
3233
+ }
3234
+ async function toRow(e, v, cwd) {
3235
+ const base = { id: e.id, kind: e.kind, prompt: e.prompt, queuedAt: e.queuedAt };
3236
+ if (v) {
3237
+ if (e.status === "queued" && TERMINAL2.has(v.status)) {
3238
+ await recordTerminal(
3239
+ e.id,
3240
+ v.status,
3241
+ v.files?.map((f) => f.url),
3242
+ cwd
3243
+ ).catch(() => {
3244
+ });
3245
+ }
3246
+ switch (v.status) {
3247
+ case "completed":
3248
+ return { ...base, status: "done" };
3249
+ case "failed":
3250
+ return { ...base, status: "failed", error: v.error };
3251
+ case "processing":
3252
+ return { ...base, status: "running", progress: v.progress };
3253
+ default:
3254
+ return { ...base, status: "queued" };
3255
+ }
3256
+ }
3257
+ return { ...base, status: e.status === "completed" ? "done" : e.status === "failed" ? "failed" : "queued" };
3258
+ }
3096
3259
 
3097
3260
  // src/commands/controller.ts
3098
3261
  import fs13 from "fs/promises";
@@ -13470,6 +13633,16 @@ async function runExplore(opts) {
13470
13633
  }
13471
13634
  const { items } = await res.json();
13472
13635
  const ranked = query ? rank(items, query).slice(0, 5) : [...items].sort((a, b) => b.playsCount - a.playsCount);
13636
+ if (ranked.length > 0) {
13637
+ try {
13638
+ const meta = await readProject();
13639
+ if (meta && !meta.exploreRanAt) {
13640
+ meta.exploreRanAt = (/* @__PURE__ */ new Date()).toISOString();
13641
+ await writeProject(meta);
13642
+ }
13643
+ } catch {
13644
+ }
13645
+ }
13473
13646
  if (opts.json) {
13474
13647
  console.log(JSON.stringify(ranked, null, 2));
13475
13648
  return;
@@ -14477,6 +14650,9 @@ ${c.bold("Usage")}
14477
14650
  genex wait <id> Attach to a generation enqueued with --no-wait and
14478
14651
  print its asset URL(s) when it finishes. Safe to
14479
14652
  re-run \u2014 it never creates a new generation.
14653
+ genex wait --all One status line per generation this project has
14654
+ enqueued (done/running/queued/failed) \u2014 a single
14655
+ API refresh, never blocks, never bills.
14480
14656
  genex controller <type> [--force] Install a bundled controller
14481
14657
  (character|car|drone|touch|networked-physics)
14482
14658
  into src/controllers (+ assets into public/assets).
@@ -14779,6 +14955,9 @@ function parseArgs(argv) {
14779
14955
  case "--json":
14780
14956
  parsed.options.json = true;
14781
14957
  break;
14958
+ case "--all":
14959
+ parsed.options.all = true;
14960
+ break;
14782
14961
  default: {
14783
14962
  if (needsValue.has(arg)) {
14784
14963
  const value = argv[i++];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.66.0-dev.163",
3
+ "version": "0.67.0-dev.167",
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": {
@@ -30,6 +30,13 @@ while a proven grass system sits in the catalog wasted its minutes on a
30
30
  solved problem. If the catalog comes back empty on this stand, say so in one
31
31
  line and move on — the sweep costs one command either way.
32
32
 
33
+ The clone/borrow lines bind at BUILD time too: each stays an open item until
34
+ the borrow lands or you cancel it in one plain line to the user ("build
35
+ fresh, because X" lines are already decided — nothing to re-announce). A
36
+ "borrow the grass system" that quietly ships as hand-rolled cones never went
37
+ through anyone's decision — it's a scope cut. (`genex preview` reminds you
38
+ once if it sees a browsed catalog and zero visible borrows.)
39
+
33
40
  ## When to use again — before any hard system
34
41
 
35
42
  Later in the build, before hand-writing a hard visual/physics system the plan
@@ -25,6 +25,7 @@ line.** The template:
25
25
  ```
26
26
  CONTENT CONTRACT — <game>
27
27
  world: <size class, as a number: one arena | a district (~500 m) | open world (km-class — $genex-threejs-open-world)>
28
+ ground: <the terrain as a DECISION: flat plane | rolling relief | heightfield; water: none | river/lake/sea>
28
29
  locations: <N, named: village, crypt, bandit camp, watchtower, boss lair…>
29
30
  quests: <N total — a main chain of M gated steps + side quests; each giver named>
30
31
  NPCs: <N speaking (quest givers, merchants) + M flavor villagers>
@@ -58,6 +59,18 @@ Rules that make the contract real:
58
59
  of terrain, grass, water, vehicles, combat feel, buildings, dungeons exist
59
60
  there to clone or borrow with credits (`$genex-explore` has the steps). One
60
61
  reuse line per matched system; an empty catalog is a one-line note.
62
+ - **The sweep binds at build time, not just plan time.** Every clone/borrow
63
+ line stays an open item until it is either landed or cancelled out loud:
64
+ when that system comes up for real work — in v0 or any upgrade round — load
65
+ `$genex-explore` and follow its borrow steps, or tell the user in one line
66
+ why you're now building it fresh after all. A planned borrow that silently
67
+ turns into hand-rolled primitives is a scope cut like any other. (A "build
68
+ fresh, because X" line was decided at plan time — no re-announcement.)
69
+ - **The `ground:` line is a decision, not a default.** Flat is fine when
70
+ DECIDED — but the concept frame the user approved is a promise: if it shows
71
+ rolling hills or a shoreline and the game ships a flat plane, that visible
72
+ gap goes through one plain line to the user, exactly like any other cut
73
+ ($genex-threejs-open-world owns real relief when the answer is yes).
61
74
 
62
75
  ## Data-driven, or you won't finish in one session
63
76
 
@@ -192,3 +205,8 @@ owns the authority rules.
192
205
  - The contract posted, then quietly abandoned when the first slice previewed
193
206
  well — the slice is a milestone, not the destination.
194
207
  - Contract lines silently shrunk without a structured question to the user.
208
+ - A reuse line from the sweep ("borrow the grass system from the catalog")
209
+ silently replaced by hand-rolled primitives — landed or cancelled out loud,
210
+ never quietly dropped.
211
+ - The approved concept frame shows rolling hills or water; the game ships one
212
+ flat plane and nobody said so.
@@ -39,6 +39,9 @@ Three.js release or branch, and do not blindly copy demo architecture.
39
39
  system in the plan (clone as base / borrow parts / build fresh because X);
40
40
  hand-building a system the catalog already proves is the waste this sweep
41
41
  exists to prevent. An empty catalog is a one-line note, not a blocker.
42
+ Clone/borrow lines bind at build time too: each is landed or cancelled out
43
+ loud when its system gets built — never silently swapped for hand-rolled
44
+ primitives (build-fresh lines are already decided).
42
45
  2. Wire player identity before any boot code: `$genex-threejs-embed-auth` is
43
46
  mandatory for every game (`initEmbed(...)` + the `waitForPlayer()` gate) —
44
47
  saves, leaderboards, and multiplayer auth all come from it.